diff --git a/backend/cli/src/snapshot/index.ts b/backend/cli/src/snapshot/index.ts index 1c15390..bce9be0 100644 --- a/backend/cli/src/snapshot/index.ts +++ b/backend/cli/src/snapshot/index.ts @@ -47,6 +47,29 @@ export namespace Snapshot { log.info("cleanup", { prune }) } + // A failed `git add` must never be swallowed: write-tree would then snapshot a + // stale (or empty) index, and a later revert() against that tree deletes every + // file the tree is missing. Retry once for transient failures (index.lock + // contention), then report the failure to the caller. + async function stageAll(git: string) { + let result = await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .` + .quiet() + .cwd(Instance.directory) + .nothrow() + if (result.exitCode !== 0) { + log.warn("add failed, retrying", { exitCode: result.exitCode, stderr: result.stderr.toString() }) + result = await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .` + .quiet() + .cwd(Instance.directory) + .nothrow() + } + if (result.exitCode !== 0) { + log.error("add failed", { exitCode: result.exitCode, stderr: result.stderr.toString() }) + return false + } + return true + } + export async function track() { if (Instance.project.vcs !== "git") return const cfg = await Config.get() @@ -65,14 +88,18 @@ export namespace Snapshot { await $`git --git-dir ${git} config core.autocrlf false`.quiet().nothrow() log.info("initialized") } - await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow() - const hash = await $`git --git-dir ${git} --work-tree ${Instance.worktree} write-tree` + if (!(await stageAll(git))) return + const result = await $`git --git-dir ${git} --work-tree ${Instance.worktree} write-tree` .quiet() .cwd(Instance.directory) .nothrow() - .text() + if (result.exitCode !== 0) { + log.error("write-tree failed", { exitCode: result.exitCode, stderr: result.stderr.toString() }) + return + } + const hash = result.text().trim() log.info("tracking", { hash, cwd: Instance.directory, git }) - return hash.trim() + return hash } export const Patch = z.object({ @@ -83,7 +110,7 @@ export namespace Snapshot { export async function patch(hash: string): Promise { const git = gitdir() - await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow() + await stageAll(git) const result = await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-only ${hash} -- .` .quiet() @@ -131,6 +158,33 @@ export namespace Snapshot { const files = new Set() const git = gitdir() for (const item of patches) { + // One full listing per snapshot instead of a per-file ls-tree pathspec: + // membership is checked in code on quotepath=false output, the same + // mechanism patch() uses, so unusual filenames behave identically on + // every platform. If the listing itself fails we know nothing about the + // snapshot, and deleting on an unverified miss would be destructive — + // keep files in that case. + const listing = await $`git -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} ls-tree -r --name-only ${item.hash}` + .quiet() + .cwd(Instance.worktree) + .nothrow() + const snapshotFiles = + listing.exitCode === 0 + ? new Set( + listing + .text() + .split("\n") + .map((x) => x.trim()) + .filter(Boolean) + .map((x) => path.join(Instance.worktree, x)), + ) + : undefined + if (!snapshotFiles) + log.warn("could not list snapshot tree", { + hash: item.hash, + exitCode: listing.exitCode, + stderr: listing.stderr.toString(), + }) for (const file of item.files) { if (files.has(file)) continue log.info("reverting", { file, hash: item.hash }) @@ -139,13 +193,9 @@ export namespace Snapshot { .cwd(Instance.worktree) .nothrow() if (result.exitCode !== 0) { - const relativePath = path.relative(Instance.worktree, file) - const checkTree = - await $`git --git-dir ${git} --work-tree ${Instance.worktree} ls-tree ${item.hash} -- ${relativePath}` - .quiet() - .cwd(Instance.worktree) - .nothrow() - if (checkTree.exitCode === 0 && checkTree.text().trim()) { + if (!snapshotFiles) { + log.warn("could not verify file against snapshot, keeping", { file }) + } else if (snapshotFiles.has(file)) { log.info("file existed in snapshot but checkout failed, keeping", { file, }) @@ -161,7 +211,7 @@ export namespace Snapshot { export async function diff(hash: string) { const git = gitdir() - await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow() + await stageAll(git) const result = await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff ${hash} -- .` .quiet() diff --git a/frontend/landing/index.html b/frontend/landing/index.html index 257b90c..9aad8cb 100644 --- a/frontend/landing/index.html +++ b/frontend/landing/index.html @@ -14,17 +14,9 @@ - - - - - - - + - - diff --git a/frontend/landing/public/og.png b/frontend/landing/public/og.png deleted file mode 100644 index f2b3ebe..0000000 Binary files a/frontend/landing/public/og.png and /dev/null differ diff --git a/frontend/landing/src/assets/hero.webp b/frontend/landing/src/assets/hero.webp new file mode 100644 index 0000000..8623ec5 Binary files /dev/null and b/frontend/landing/src/assets/hero.webp differ diff --git a/frontend/landing/src/pages/Landing.tsx b/frontend/landing/src/pages/Landing.tsx index 33da0cd..ed32a5a 100644 --- a/frontend/landing/src/pages/Landing.tsx +++ b/frontend/landing/src/pages/Landing.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import workspaceShot from "@/assets/workspace.png"; import modelPickerShot from "@/assets/model-picker.png"; +import heroPlate from "@/assets/hero.webp"; /* OpenScience. CMU Concrete, warm dark, coral accents. Same design family as the Atlas landing page. @@ -259,100 +260,25 @@ function Section({ ); } -/* --------------------------- Hero constellation ------------------------- */ -/* A slow, drifting field of faint points joined into a constellation. - Cream nodes, a few coral. Pure canvas, no library. */ - -function Constellation() { - const ref = useRef(null); - useEffect(() => { - const canvas = ref.current; - if (!canvas) return; - const ctx = canvas.getContext("2d"); - if (!ctx) return; - const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches; - const dpr = Math.min(window.devicePixelRatio || 1, 2); - let raf = 0; - let w = 0; - let h = 0; - type Pt = { x: number; y: number; vx: number; vy: number; r: number; hot: boolean }; - let pts: Pt[] = []; - - const resize = () => { - const rect = canvas.getBoundingClientRect(); - w = rect.width; - h = rect.height; - canvas.width = Math.round(w * dpr); - canvas.height = Math.round(h * dpr); - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - const count = Math.min(120, Math.max(50, Math.floor((w * h) / 15000))); - pts = Array.from({ length: count }, () => ({ - x: Math.random() * w, - y: Math.random() * h, - vx: (Math.random() - 0.5) * 0.14, - vy: (Math.random() - 0.5) * 0.14, - r: Math.random() < 0.14 ? 1.7 : 1.05, - hot: Math.random() < 0.055, - })); - }; - - const LINK = 120; - const draw = () => { - ctx.clearRect(0, 0, w, h); - for (let i = 0; i < pts.length; i++) { - const a = pts[i]; - for (let j = i + 1; j < pts.length; j++) { - const b = pts[j]; - const dx = a.x - b.x; - const dy = a.y - b.y; - const d2 = dx * dx + dy * dy; - if (d2 < LINK * LINK) { - const t = 1 - Math.sqrt(d2) / LINK; - ctx.strokeStyle = `hsla(42, 32%, 88%, ${(t * 0.13).toFixed(3)})`; - ctx.lineWidth = 0.6; - ctx.beginPath(); - ctx.moveTo(a.x, a.y); - ctx.lineTo(b.x, b.y); - ctx.stroke(); - } - } - } - for (const p of pts) { - ctx.beginPath(); - ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); - ctx.fillStyle = p.hot ? "hsla(14, 70%, 64%, 0.75)" : "hsla(42, 32%, 88%, 0.4)"; - ctx.fill(); - } - }; - - const step = () => { - for (const p of pts) { - p.x += p.vx; - p.y += p.vy; - if (p.x < -8) p.x = w + 8; - if (p.x > w + 8) p.x = -8; - if (p.y < -8) p.y = h + 8; - if (p.y > h + 8) p.y = -8; - } - draw(); - raf = window.requestAnimationFrame(step); - }; - - resize(); - draw(); - if (!reduced) raf = window.requestAnimationFrame(step); - const onResize = () => { - resize(); - draw(); - }; - window.addEventListener("resize", onResize); - return () => { - window.cancelAnimationFrame(raf); - window.removeEventListener("resize", onResize); - }; - }, []); - return ; -} +/* ----------------------------- Hero plate ------------------------------- */ +/* The Pharos of Alexandria engraving — the beam sweeps from the tower at the + right down to a small ship steering by its light at bottom-left. The plate + is already monochrome warm sepia, the same hue family as the site's cream, + so it ships unfiltered. Anchored right; on wide viewports the background + color fills the left edge and the veil blends the seam. */ + +const HERO_NOISE = + "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\")"; + +/* Veil: a light uniform scrim softens the plate; a soft top-left shield sits + under the wordmark and a bottom-right shield under the copy, so the beam + and the ship at bottom-left stay in view. */ +const HERO_VEIL = [ + "linear-gradient(hsl(30 14% 7% / 0.2), hsl(30 14% 7% / 0.2))", + "radial-gradient(ellipse 55% 45% at 5% 6%, hsl(30 14% 7% / 0.8) 0%, hsl(30 14% 7% / 0.4) 55%, transparent 85%)", + "radial-gradient(ellipse 85% 75% at 96% 94%, hsl(var(--background)) 0%, hsl(30 14% 7% / 0.84) 38%, hsl(30 14% 7% / 0.28) 66%, transparent 90%)", + "linear-gradient(180deg, hsl(28 18% 4% / 0.5) 0%, transparent 15%)", +].join(", "); /* ------------------------------ Hero ----------------------------------- */ @@ -389,46 +315,44 @@ function Hero() { return (
-
- -
+ className="absolute inset-0 bg-background bg-no-repeat [background-position:62%_14%] [background-size:cover] sm:[background-position:right_center] sm:[background-size:auto_114%]" + style={{ backgroundImage: `url(${heroPlate})` }} + /> +
+
-
+
openscience
-
by Synthetic Sciences
+
by Synthetic Sciences
-
+

The open-source AI workbench for scientists.

-

+

One environment for the whole loop: literature, code, experiments, figures, and the write-up. In your browser, on any model, with your keys.

-
+
Install OpenScience