Landing page at / with animated concept GIFs; owners list to /explore #187

Closed
opened 2026-09-08 02:43:58 +00:00 by crueber · 6 comments
Owner

Landing page: / becomes marketing, owners list moves to /explore, animated concept GIFs

Routes

  • / stops being the owners list. It becomes a marketing/landing page: what walhub is, how it works at a superficially technical level. This page draws people in — design bar is high.
  • The owners list moves to /explore (for now). All existing owners-page behavior (caps, order, star counts, activity stamps, intro card — the intro card should probably slim down or move since / now carries the message; decide in the change) keeps working at the new path. Old / links (nav "owners" entry?) point at /explore.

Animated concept GIFs (procedurally generated, in-repo)

  • A small number (3–4) of procedurally generated animated GIFs that simply explain core concepts, e.g.: (1) push → objects land in the bucket; (2) the bucket is the only database (instances disposable); (3) fetch/clone reads objects back; (4) collaboration (issues/PRs/checks) as objects alongside git data. Final concept list decided in planning.
  • Slow enough for the average person to follow: generous frame delays, hold key frames, few moving elements at once, captions on frames. This is load-bearing acceptance, not polish.
  • Generation must be reproducible in-repo (checked-in generator + script/make target, deterministic output) — no binary blobs from outside. Suggested shape: a Go program using stdlib image/gif (zero new deps) plus embedded font/raster text handling decided in planning; GIFs served as static assets (or generated at build — decide).
  • Dark-theme-friendly palette; reasonable file sizes (measure + record).

Acceptance criteria

  • / marketing page (copy + GIFs + CTAs), /explore owners list unchanged in behavior.
  • GIFs slow/followable (frame-timing table in the PR), accessible (alt text describing each animation; prefers-reduced-motion respected — static first frame fallback).
  • node --test green; browser pass both themes, zero console errors; no new npm deps; Go gates if a generator is added.
# Landing page: `/` becomes marketing, owners list moves to `/explore`, animated concept GIFs ## Routes - `/` stops being the owners list. It becomes a **marketing/landing page**: what walhub is, how it works at a superficially technical level. This page draws people in — design bar is high. - The owners list moves to **`/explore`** (for now). All existing owners-page behavior (caps, order, star counts, activity stamps, intro card — the intro card should probably slim down or move since `/` now carries the message; decide in the change) keeps working at the new path. Old `/` links (nav "owners" entry?) point at `/explore`. ## Animated concept GIFs (procedurally generated, in-repo) - A small number (3–4) of **procedurally generated animated GIFs** that simply explain core concepts, e.g.: (1) push → objects land in the bucket; (2) the bucket is the only database (instances disposable); (3) fetch/clone reads objects back; (4) collaboration (issues/PRs/checks) as objects alongside git data. Final concept list decided in planning. - **Slow enough for the average person to follow**: generous frame delays, hold key frames, few moving elements at once, captions on frames. This is load-bearing acceptance, not polish. - Generation must be reproducible in-repo (checked-in generator + script/make target, deterministic output) — no binary blobs from outside. Suggested shape: a Go program using stdlib `image/gif` (zero new deps) plus embedded font/raster text handling decided in planning; GIFs served as static assets (or generated at build — decide). - Dark-theme-friendly palette; reasonable file sizes (measure + record). ## Acceptance criteria - [ ] `/` marketing page (copy + GIFs + CTAs), `/explore` owners list unchanged in behavior. - [ ] GIFs slow/followable (frame-timing table in the PR), accessible (`alt` text describing each animation; `prefers-reduced-motion` respected — static first frame fallback). - [ ] `node --test` green; browser pass both themes, zero console errors; no new npm deps; Go gates if a generator is added.
Author
Owner

Landing page plan — Forgejo issue crueber/walhub#187

Scope: / becomes a marketing/landing page with 3–4 procedurally generated
animated concept GIFs; the owners list moves to /explore with behavior
unchanged. Planning only — no production code written.

Laws binding this change (AGENTS.md): dependency budget (no new Go modules,
no new npm runtime deps — generator is stdlib-only, page uses existing
solid-js/@solidjs/router/Tailwind), bucket-is-repo (landing page adds zero
store round trips; it makes zero API calls), seams (no new backend routes
beyond the SPA shell + static asset entries; no new API endpoints, no new SDK
methods), coverage (generator lives in internal/devtools/, which make cover
already excludes — unit tests still required, golden determinism test included),
docs-with-code (06 + 12 updated in the same change).

Canonical language reused verbatim or near-verbatim (README.md L3–5, Owners
intro card, footer):
"git over smart HTTP (v0/v2), LFS, bundle-uri, a JSON
API with SSE, and a web UI — where the object store is the only database";
"every repository's refs, packs, config, and policy live as objects in a bucket
(filesystem, S3, or GCS)"; "Instances are disposable; wipe one and you lose
nothing but warmth"; "the bucket is the repository" (footer); walgit
object-protocol compliance note (README "Inspired by walgit").

Text-rendering risk — RESOLVED in planning by spike (see §3.5): two
/tmp-only programs proved stdlib image/gif + a hand-authored 5x7 bitmap
font at 2x scale on 640×360 is legible and tiny (3 frames = 6.3 KB animated,
2.2 KB static still). Nothing from the spike was written to the repo.


1. Route changes

1.1 Frontend (web/src/index.jsx)

Path Before After
/ Owners Landing (new web/src/pages/Landing.jsx)
/explore — (would hit /:owner) Owners (moved, component unchanged)
* fallback Owners Landing (safe default: no API calls, no confusing empty state for unmatched deep paths)

Owners.jsx header comment (route "/") updated to route "/explore". No
logic change inside Owners.jsx, lib/owners.js, or any data path.

1.2 Backend (internal/server/router.go, health.go)

  • Add explicit r.Get("/explore", s.gated(s.explorePage)) next to the existing
    r.Get("/", ...) / r.Get("/setup", ...) lines. explorePage = serveSPA
    (same as ownerPage). Must be explicit: otherwise /explore falls into
    the /* wildcard → repoDispatch → treated as owner "explore".
  • spaHome (GET /) keeps serving the shell for browsers. Its
    ?format=text/text-Accept branch (plain one-per-line owner list, documented
    in docs/go/06_server_http.md L122/L212) is kept unchanged at / — it
    is machine surface, not the marketing page. Extract the branch into a shared
    helper so /explore answers it too (scripts can move; old scripts keep
    working at /).
  • Setup-only mode (mountSetupOnly): no /explore registration — it 503s
    like every non-setup route. Nothing to do.
  • Static assets: extend uiAsset (health.go ~L346) with a concepts/ prefix
    mapping to dist/concepts/, and add image/gif MIME (hasSuffixFold(name, ".gif") → image/gif). Caching class: no-cache + ETag (same as
    index.html), NOT immutable — filenames are stable (push.gif,
    push-still.gif, …) and content changes on regeneration, so immutable
    would be wrong. serveUIAssets already ETags everything; only the allowlist
    • MIME lines are new.

1.3 Nav (web/src/App.jsx)

Nav entry owners → / becomes explore → /explore (label "explore"). Brand
link stays /. Footer unchanged ("walhub — the bucket is the repository").

1.4 Name collision: owner literally named explore

Precedent already exists: /import, /api, /keys, /setup all shadow those
owner names via explicit routes. /explore joins that set — an owner named
explore loses its /:owner UI page (git/API paths for that owner are
unaffected; only the single-segment UI page is shadowed). Document the
reservation in docs/go/06_server_http.md §3.3 alongside the existing list.
No code beyond the explicit route.

1.5 Old anchors / redirects

None needed: the owners page has no #id deep-link scheme (unlike Settings),
and in-app links are updated in the same change. External bookmarks to /
land on the landing page, which carries a prominent "Browse repositories →
/explore" CTA — that is the migration path, no server redirect table.

Concurrency

No new concurrency. The generator is a single-threaded CLI; the landing page
makes zero API calls (fully static marketing + static GIFs), so no new fetch,
SSE, or invalidation paths. (AGENTS.md law 3: nothing concurrent, nothing
to lock.)


2. Landing page structure (web/src/pages/Landing.jsx)

Static page, zero API calls, zero Solid data hooks. Works in both themes (dark
default). New component web/src/components/ConceptGif.jsx (see §4) used
4×. New node --test file pins CTA hrefs + alt texts (source pins, no DOM).

2.1 Hero

  • H1: "walhub — a git host whose only database is an object store." (canonical
    README/intro-card language, tightened to one line.)
  • Sub (1–2 lines, from README L5): "Every repository's refs, packs, config,
    and policy live as objects in a bucket — filesystem, S3, or GCS. Instances
    are disposable; wipe one and you lose nothing but warmth."
  • CTAs (primary + secondary, .btn.primary / .btn):
    • "Browse repositories" → /explore
    • "Push in 30 seconds" → anchor #quickstart (same-page push snippet)
    • Tertiary link: "Configure" → /setup
  • Compat one-liner (small, muted): "Object-protocol compliant with walgit —
    bucket layout, protobuf wire encoding, and git wire behavior follow walgit's
    formats." (README "Inspired by walgit" compressed.)

2.2 Concept sections (alternating layout, each: GIF + caption + 2–3 sentences)

  1. Push (push.gif) — "Push over smart HTTP; objects land in the bucket."
    Copy: smart HTTP v0/v2, auto-create on push, manifest CAS is the only commit
    point, never ACKs before the bucket ACKs (law 4, superficial).
  2. The bucket is the database (bucket.gif) — "No SQL, no Redis. Kill the
    instance; the bucket doesn't notice." Copy: refs/packs/config/policy as
    objects; new instance serves immediately; disk and memory are caches.
  3. Fetch / clone (fetch.gif) — "Clone reads objects back." Copy: warm
    refs in one round trip, stock-git fetch path, bundle-uri for large hosts.
  4. Collaboration as objects (collab.gif) — "Issues, PRs, reviews, and
    checks live next to git data." Copy: docs/features/README.md P1 family
    (meta/, issues/, checks/, releases/, shared issue/PR numbering)
    with the one architectural law stated plainly: the WAL stays git-only;
    collaboration is a parallel object family that never gates a push.

2.3 Quickstart strip (#quickstart)

The README push block verbatim (zero-config ./walhub, git push -u origin main, browse at /you/demo), rendered as a code block. Plus the auth-modes
one-liner: "none for dev, token for static bearers, oidc for real
users — then /setup." Plus SSH one-liner (port 2222, /keys).

2.4 Owners page at /explore — what changes

Behavior preserved exactly: caps (MAX_OWNERS 50, MAX_REPOS_PER_OWNER 10),
newest-first ordering, star counts, activity stamps, +N more → overflow,
import button, empty states. Only change: the intro card slims to one line
since / now carries the message:

"walhub is a git host whose only database is an object store. What is
walhub? → /
"

(one sentence + link; the full paragraph moves to the landing page.)


3. GIF generator design (the hard part)

3.1 Exact technique (stdlib only — zero new deps, both ecosystems)

  • Go program, stdlib image, image/color, image/gif, image/png
    (stills) only.
    No golang.org/x/image (not in the Go budget — ruling C-1
    spirit, AGENTS.md law 1). No npm involvement — GIFs are opaque bytes to vite.
  • Shapes: filled rectangles, 1px box outlines, horizontal/vertical arrows,
    all drawn by hand loops (spike-proved, ~30 lines).
  • Text: embedded hand-authored monospace 5x7 bitmap font covering full
    printable ASCII (32–126)
    , stored as Go source (~95 glyphs × 7 rows of
    5-bit strings, ≈ 2–3 KB), rendered at 2× scale (10×14 px per glyph).
    • Why hand-authored, not golang.org/x/image/font/basicfont: x/image is
      outside the dependency budget and would need a written amendment; a
      5x7 font table is trivially small to own and has zero license surface
      (shapes typed from scratch, verified by rendering).
    • Why 2×: the spike rendered 1× at 480×270 — legible but small; 2× at
      640×360 is comfortably readable (verified by viewing decoded frames).
  • Palette (dark-theme-friendly, 5 entries): bg zinc-950 #09090b, fg
    zinc-200 #e4e4e7, accent emerald-500 #10b981, in-flight amber #f59e0b,
    dim zinc-500 #71717a. GIFs ship in dark-framed rounded cards in BOTH
    themes (README screenshot precedent) — one asset set, no light variants.
  • Dimensions: 640×360 for all four GIFs (16:9, fits content column without
    layout shift; width/height attributes set).
  • Determinism: fixed palette order, no timestamps, no map iteration in the
    encode path, fixed frame order; golden test regenerates into a buffer and
    byte-compares against checked-in files (bytes.Equal, fail on any drift —
    regeneration is then always intentional + reviewed).
  • Stills: generator also emits <name>-still.gif (frame 0) for the
    reduced-motion fallback (§4).

3.2 Storyboards with timings (acceptance: slow enough to follow)

Timing convention: Delay units are 100ths of a second. Key frames hold
180–250 (1.8–2.5 s); motion steps 40–60 (0.4–0.6 s); total loop 6–9 s; ≤ 3
moving elements per frame; every frame carries a numbered caption
("1. YOU PUSH." …) baked at bottom-center + mirrored in adjacent text.

GIF 1 — push.gif: push → objects land in the bucket (7 frames, ≈ 8 s)

# Visual Caption Delay
1 Laptop box (emerald) left, bucket box (dim) right, nothing between 1. YOU PUSH. 200
2 Amber arrow grows laptop→bucket, "PACK" label above 2. PACKS TRAVEL. 50
3 Arrow full-length, packet block mid-flight 2. PACKS TRAVEL. 50
4 Packet arrives at bucket edge 3. BUCKET WRITES. 60
5 Bucket emerald, "+ PACK" row appears inside 3. BUCKET WRITES. 180
6 Green check path bucket→laptop (ACK arrow back) 4. BUCKET ACKS FIRST. 180
7 Rest state: both emerald, "+ PACK" persisted 4. BUCKET ACKS FIRST. 200

GIF 2 — bucket.gif: the bucket is the only database (6 frames, ≈ 8 s)

# Visual Caption Delay
1 Bucket (emerald) center with rows: REFS / PACKS / CONFIG / POLICY; instance box A (dim) above connected by thin line 1. ONE BUCKET. 200
2 Instance A flashes amber then grays out, connection line breaks 2. INSTANCE DIES. 60
3 Instance A box removed (blank space), bucket unchanged, rows intact 2. INSTANCE DIES. 200
4 Fresh instance box B appears (emerald), line reconnects 3. NEW INSTANCE. 60
5 B reads rows (each row highlights amber in turn — 2 sub-frames ×50) 4. NOTHING LOST. 100
6 Rest: B emerald, bucket emerald, "WARMTH ONLY" note 4. NOTHING LOST. 220

GIF 3 — fetch.gif: fetch/clone reads objects back (6 frames, ≈ 7 s)

# Visual Caption Delay
1 Bucket (emerald) left with REFS/PACKS rows; empty clone box (dim) right 1. EMPTY CLONE. 200
2 Thin arrow bucket→clone labeled "REFS (1)" 2. REFS FIRST. 60
3 Clone gains ref labels (amber, then emerald) 2. REFS FIRST. 150
4 Thick arrow bucket→clone labeled "PACKS" with moving block 3. PACKS FOLLOW. 50
5 Clone fills (file-tree glyph rows appear), both emerald 4. WORKING COPY. 200
6 Rest hold 4. WORKING COPY. 200

GIF 4 — collab.gif: collaboration as objects alongside git data (7 frames, ≈ 9 s)

# Visual Caption Delay
1 Bucket with GIT lane (REFS/PACKS, emerald) 1. GIT LIVES HERE. 200
2 Second lane slides in below: ISSUES / PRS / CHECKS (dim → amber) 2. COLLAB MOVES IN. 60
3 Issue card "#7 OPENED" lands in ISSUES row 3. ISSUE 7 OPENS. 150
4 "PR 8" card lands, thread line connects PR 8 ↔ ISSUE 7 ("FIXES 7") 4. PR 8 FIXES 7. 150
5 Check chip "CI: GREEN" stamps onto PR 8 5. CHECKS REPORT. 150
6 WAL badge on GIT lane pulses; caption emphasizes separation 6. WAL STAYS GIT-ONLY. 200
7 Rest hold, all emerald 6. WAL STAYS GIT-ONLY. 200

3.3 Target file sizes (measured basis, not estimates)

Spike measurements (640×360, 2× type, 5-color palette): 3-frame animated =
6,343 bytes; single-frame still = 2,187 bytes. Per-frame marginal cost
≈ 1.5–2 KB (flat-color LZW). Budgets:

Asset Frames Budget Expected (from spike rate)
push.gif 7 ≤ 30 KB ~12–15 KB
bucket.gif 6–7 ≤ 30 KB ~12–15 KB
fetch.gif 6 ≤ 30 KB ~10–14 KB
collab.gif 7 ≤ 40 KB ~14–20 KB
4 stills 1 each ≤ 4 KB each ~2.2 KB each
Total GIF page weight ≤ 150 KB ~60–75 KB

For scale: the shipped JS bundle delta for D-WEB-7 alone was +22.2 KB gzip —
all four GIFs + stills cost roughly 3× that, one time, lazy-loaded below the
fold (loading="lazy" on all concept images). Sizes recorded in EVIDENCE.md
(§5).

3.4 Make target + checked-in artifacts vs build-time generation

Decision: generator checked in + artifacts checked in + make target to
regenerate. No build-time generation.

  • Generator: internal/devtools/landinggif/ — main.go (scene definitions),
    font.go (full ASCII 5x7 table), draw.go (boxes/arrows/text/blits),
    main_test.go (font coverage: every caption char used by every scene has a
    glyph — fail otherwise; determinism golden: encode-to-buffer twice →
    identical; asset freshness: regenerate → byte-equal to checked-in files).
    Placement in internal/devtools/ is deliberate: make cover already
    excludes devtools (and e2e) from the ≥ 95% gate, so the pixel-pushing
    code doesn't distort package coverage; it still gets real unit tests.
  • Artifacts: web/public/concepts/{push,bucket,fetch,collab}.gif +
    {...}-still.gif — 8 files checked in. web/public/ is new (vite
    copies public/ → dist/ verbatim, surviving emptyOutDir: true; the
    SDK bundle step runs after and is unaffected).
  • Make: make landing-gifs → go run ./internal/devtools/landinggif -out web/public/concepts/; make web gains it as a prerequisite (order: gifs
    first, then pnpm build, so vite copies them into dist/). go:embed all:dist picks them up with no embed changes; make clean (rm -rf web/dist) stays safe because the sources of truth live in web/public/.
  • Rejected alternative — generate at Go/vite build time: hides bytes from
    review, makes make web depend on Go, complicates the Docker web stage;
    checked-in bytes are diffable and the golden test makes staleness loud.

3.5 Spike results (the resolved risk, evidence)

  • /tmp/opencode/gifspike/main.go (stdlib image/gif only, 30-glyph 5x7
    subset, boxes + arrow + captions, 480×270): 3 frames = 4,111 bytes,
    still = 1,387 bytes.
  • 2×-type 640×360 variant: 3 frames = 6,343 bytes, still = 2,187 bytes.
  • Decoded frames viewed as PNG: 1× readable-but-small; 2× comfortably
    legible
    at both body and caption sizes.
  • Failure mode found and designed out: the subset font silently dropped
    3 ("3. BUCKET ACKS. DONE." rendered as ". BUCKET ACKS. DONE."). Hence the
    full-ASCII requirement + the font-coverage test that fails the build on any
    missing glyph. Never a silent space.

4. Accessibility

  • Alt text (baked into ConceptGif.jsx call sites, pinned by test):
    • push: "Animation: a laptop pushes a pack of git objects to a bucket; the
      bucket writes them, then acknowledges. The client only finishes after the
      bucket's acknowledgement."
    • bucket: "Animation: one bucket holds refs, packs, config, and policy while
      a server instance above it disappears and a fresh instance connects to the
      same bucket with nothing lost."
    • fetch: "Animation: an empty clone first receives ref names, then pack
      data, from the bucket until it holds a working copy."
    • collab: "Animation: alongside the git lane, issue 7 opens, pull request 8
      links to it, a green check result stamps the PR — while the write-ahead
      log stays git-only."
  • Reduced motion: ConceptGif renders the -still image by default
    (also the <noscript> output); JS swaps in the animated .gif only when
    matchMedia("(prefers-reduced-motion: reduce)").matches === false. Listens
    for change events. No autoplay for users who asked for none.
  • Every GIF has an adjacent text caption + the section prose restating the
    point — no information is GIF-only. Focus/contrast: captions are real text
    (not images); keyboard: nothing interactive inside figures (role="img"
    • aria-label on the figure, or plain img alt).
  • Both themes: dark-framed cards; caption/body text uses existing .muted
    tokens (already contrast-checked in both themes).

5. EVIDENCE / perf

  • New EVIDENCE.md entry (landing-asset class, not a scaling claim — follows
    the "measured, not modeled" standard with the generator as harness):
    per-file byte sizes, total GIF weight, landing page weight (shell HTML +
    hashed JS/CSS + 4 GIFs lazy + 4 stills), make landing-gifs wall time,
    backend named (filesystem store irrelevant — static assets; note embed).
  • No hot-path impact to assert: landing makes zero API/store calls;
    /explore keeps the documented 1 + MAX_OWNERS GET shape (owners.list
    • one repos(owner) per shown owner) — unchanged code, no new budget test
      needed, existing owners.test.js untouched.
  • No new npm deps (package.json runtime stays exactly the four amended
    packages); no new Go modules (go.mod untouched — generator is stdlib).
    make vet/make build unaffected beyond the new uiAsset prefix + MIME
    lines (covered by existing x_health_test.go-class table tests, extended
    with a gif row).

6. Acceptance criteria

  • / renders the landing page (hero + 4 concept sections + quickstart),
    /explore renders the owners list with behavior unchanged (caps, order,
    stars, activity, slimmed intro card).
  • Nav entry points at /explore; brand still /; * fallback renders
    landing; ?format=text still answers the owner list at / (and now
    also at /explore).
  • GIFs followable: frame-timing table (§3.2) reproduced in the PR
    description; key-frame holds ≥ 1.8 s, motion steps ≥ 0.4 s, ≤ 3 moving
    elements per frame, numbered captions on every frame.
  • Deterministic: make landing-gifs output byte-identical to checked-in
    files (golden test); full-ASCII font + coverage test (the missing-3
    class can never recur silently).
  • Accessible: per-GIF alt text, still-first + prefers-reduced-motion
    swap, <noscript> still, adjacent text captions.
  • node --test web/test/unit/*.test.js green (incl. new landing +
    ConceptGif source-pin tests); make vet, make test-go, make cover
    green; no new npm/Go deps (git diff package.json go.mod empty).
  • Browser pass (real Chromium via the :9222 CDP daemon against the
    canonical host per AGENTS.md field lesson): / + /explore in dark
    AND light, zero console errors; GIFs animate, stills show under
    emulated reduced-motion; screenshots attached to the PR.
  • Docs updated in the same change: docs/go/06_server_http.md (§3 route
    tree + ?format=text + explore name reservation), docs/go/12_*
    frontend doc (route table row, web/public/concepts/ pipeline, make landing-gifs), EVIDENCE.md asset entry. No DEVIATIONS amendment needed
    (no budget change). Commit message names the sections.

7. Open questions / risks

  1. RESOLVED (§3.5): text without font libraries. Hand-authored full-ASCII
    5x7 at 2× is legible and tiny. Residual risk is aesthetic, not technical:
    bitmap type reads deliberately lo-fi/diagrammatic. Mitigation: strict
    palette discipline, generous whitespace, numbered captions; the design bar
    comes from composition, not font fidelity. If reviewers judge it cheap,
    the fallback is NOT a library (budget) but larger 3× display type for
    headlines inside frames — same technique, no new decision.
  2. Owner named explore loses its /:owner UI page (§1.4). Precedent
    (import, api, keys, setup) makes this acceptable; documented, not
    coded around. Alternative (content-negotiate the single segment) rejected:
    wire-contract smell for one name.
  3. ?format=text at / — keep or move? Plan keeps it at / (compat) and
    mirrors at /explore. If maintainers prefer a clean break, the alternative
    is 308 / → /explore?format=text for text-Accept requests — one line, but
    it changes documented behavior; keep is the conservative default.
  4. Single dark GIF set in light theme. Cards carry their own dark frame in
    both themes (screenshot precedent). Risk: a light-theme purist objects.
    Alternative (dual light/dark GIF sets) doubles weight and generator scenes
    for zero information gain — not recommended.
  5. GIF vs modern formats. Issue mandates GIFs; APNG/WebP would need
    non-stdlib encoders (out of budget) or browser-side SMIL (not a GIF).
    GIF it is — LZW on flat colors is near-optimal for this content anyway
    (~2 KB/frame measured).
  6. Infinite loop vs finite. LoopCount: 0 (infinite) is standard for
    concept loops; reduced-motion users never see it (§4). A finite 3-loop
    alternative strands sighted users mid-explanation on long dwell — not
    recommended.
  7. Scope guard: this plan adds no backend behavior (no policy, no auth, no
    store, no WAL touch). Any urge to make the landing page "live" (repo
    counts, activity feeds) is out of scope — it would spend store round trips
    on the front door and violate the static-page decision. /explore is the
    live page.
# Landing page plan — Forgejo issue crueber/walhub#187 **Scope:** `/` becomes a marketing/landing page with 3–4 procedurally generated animated concept GIFs; the owners list moves to `/explore` with behavior unchanged. Planning only — no production code written. **Laws binding this change (AGENTS.md):** dependency budget (no new Go modules, no new npm runtime deps — generator is stdlib-only, page uses existing `solid-js`/`@solidjs/router`/Tailwind), bucket-is-repo (landing page adds zero store round trips; it makes zero API calls), seams (no new backend routes beyond the SPA shell + static asset entries; no new API endpoints, no new SDK methods), coverage (generator lives in `internal/devtools/`, which `make cover` already excludes — unit tests still required, golden determinism test included), docs-with-code (06 + 12 updated in the same change). **Canonical language reused verbatim or near-verbatim (README.md L3–5, Owners intro card, footer):** "git over smart HTTP (v0/v2), LFS, bundle-uri, a JSON API with SSE, and a web UI — where the object store is the only database"; "every repository's refs, packs, config, and policy live as objects in a bucket (filesystem, S3, or GCS)"; "Instances are disposable; wipe one and you lose nothing but warmth"; "the bucket is the repository" (footer); walgit object-protocol compliance note (README "Inspired by walgit"). **Text-rendering risk — RESOLVED in planning by spike (see §3.5):** two `/tmp`-only programs proved stdlib `image/gif` + a hand-authored 5x7 bitmap font at 2x scale on 640×360 is legible and tiny (3 frames = 6.3 KB animated, 2.2 KB static still). Nothing from the spike was written to the repo. --- ## 1. Route changes ### 1.1 Frontend (`web/src/index.jsx`) | Path | Before | After | |---|---|---| | `/` | `Owners` | `Landing` (new `web/src/pages/Landing.jsx`) | | `/explore` | — (would hit `/:owner`) | `Owners` (moved, component unchanged) | | `*` fallback | `Owners` | `Landing` (safe default: no API calls, no confusing empty state for unmatched deep paths) | `Owners.jsx` header comment (`route "/"`) updated to `route "/explore"`. No logic change inside `Owners.jsx`, `lib/owners.js`, or any data path. ### 1.2 Backend (`internal/server/router.go`, `health.go`) - Add explicit `r.Get("/explore", s.gated(s.explorePage))` next to the existing `r.Get("/", ...)` / `r.Get("/setup", ...)` lines. `explorePage` = `serveSPA` (same as `ownerPage`). **Must be explicit:** otherwise `/explore` falls into the `/*` wildcard → `repoDispatch` → treated as owner `"explore"`. - `spaHome` (`GET /`) keeps serving the shell for browsers. Its `?format=text`/text-Accept branch (plain one-per-line owner list, documented in `docs/go/06_server_http.md` L122/L212) is **kept unchanged at `/`** — it is machine surface, not the marketing page. Extract the branch into a shared helper so `/explore` answers it too (scripts can move; old scripts keep working at `/`). - Setup-only mode (`mountSetupOnly`): no `/explore` registration — it 503s like every non-setup route. Nothing to do. - Static assets: extend `uiAsset` (`health.go` ~L346) with a `concepts/` prefix mapping to `dist/concepts/`, and add `image/gif` MIME (`hasSuffixFold(name, ".gif")` → `image/gif`). Caching class: **no-cache + ETag** (same as `index.html`), NOT immutable — filenames are stable (`push.gif`, `push-still.gif`, …) and content changes on regeneration, so `immutable` would be wrong. `serveUIAssets` already ETags everything; only the allowlist + MIME lines are new. ### 1.3 Nav (`web/src/App.jsx`) Nav entry `owners → /` becomes `explore → /explore` (label "explore"). Brand link stays `/`. Footer unchanged ("walhub — the bucket is the repository"). ### 1.4 Name collision: owner literally named `explore` Precedent already exists: `/import`, `/api`, `/keys`, `/setup` all shadow those owner names via explicit routes. `/explore` joins that set — an owner named `explore` loses its `/:owner` UI page (git/API paths for that owner are unaffected; only the single-segment UI page is shadowed). Document the reservation in `docs/go/06_server_http.md` §3.3 alongside the existing list. No code beyond the explicit route. ### 1.5 Old anchors / redirects None needed: the owners page has no `#id` deep-link scheme (unlike Settings), and in-app links are updated in the same change. External bookmarks to `/` land on the landing page, which carries a prominent "Browse repositories → /explore" CTA — that is the migration path, no server redirect table. ### Concurrency No new concurrency. The generator is a single-threaded CLI; the landing page makes zero API calls (fully static marketing + static GIFs), so no new fetch, SSE, or invalidation paths. (`AGENTS.md` law 3: nothing concurrent, nothing to lock.) --- ## 2. Landing page structure (`web/src/pages/Landing.jsx`) Static page, zero API calls, zero Solid data hooks. Works in both themes (dark default). New component `web/src/components/ConceptGif.jsx` (see §4) used 4×. New `node --test` file pins CTA hrefs + alt texts (source pins, no DOM). ### 2.1 Hero - H1: "walhub — a git host whose only database is an object store." (canonical README/intro-card language, tightened to one line.) - Sub (1–2 lines, from README L5): "Every repository's refs, packs, config, and policy live as objects in a bucket — filesystem, S3, or GCS. Instances are disposable; wipe one and you lose nothing but warmth." - CTAs (primary + secondary, `.btn.primary` / `.btn`): - "Browse repositories" → `/explore` - "Push in 30 seconds" → anchor `#quickstart` (same-page push snippet) - Tertiary link: "Configure" → `/setup` - Compat one-liner (small, muted): "Object-protocol compliant with walgit — bucket layout, protobuf wire encoding, and git wire behavior follow walgit's formats." (README "Inspired by walgit" compressed.) ### 2.2 Concept sections (alternating layout, each: GIF + caption + 2–3 sentences) 1. **Push** (`push.gif`) — "Push over smart HTTP; objects land in the bucket." Copy: smart HTTP v0/v2, auto-create on push, manifest CAS is the only commit point, never ACKs before the bucket ACKs (law 4, superficial). 2. **The bucket is the database** (`bucket.gif`) — "No SQL, no Redis. Kill the instance; the bucket doesn't notice." Copy: refs/packs/config/policy as objects; new instance serves immediately; disk and memory are caches. 3. **Fetch / clone** (`fetch.gif`) — "Clone reads objects back." Copy: warm refs in one round trip, stock-git fetch path, bundle-uri for large hosts. 4. **Collaboration as objects** (`collab.gif`) — "Issues, PRs, reviews, and checks live next to git data." Copy: `docs/features/README.md` P1 family (`meta/`, `issues/`, `checks/`, `releases/`, shared issue/PR numbering) with the one architectural law stated plainly: the WAL stays git-only; collaboration is a parallel object family that never gates a push. ### 2.3 Quickstart strip (`#quickstart`) The README push block verbatim (zero-config `./walhub`, `git push -u origin main`, browse at `/you/demo`), rendered as a code block. Plus the auth-modes one-liner: "`none` for dev, `token` for static bearers, `oidc` for real users — then `/setup`." Plus SSH one-liner (port 2222, `/keys`). ### 2.4 Owners page at `/explore` — what changes Behavior preserved exactly: caps (`MAX_OWNERS` 50, `MAX_REPOS_PER_OWNER` 10), newest-first ordering, star counts, activity stamps, `+N more →` overflow, import button, empty states. Only change: the intro card slims to one line since `/` now carries the message: > "walhub is a git host whose only database is an object store. **What is > walhub? → /**" (one sentence + link; the full paragraph moves to the landing page.) --- ## 3. GIF generator design (the hard part) ### 3.1 Exact technique (stdlib only — zero new deps, both ecosystems) - **Go program, stdlib `image`, `image/color`, `image/gif`, `image/png` (stills) only.** No `golang.org/x/image` (not in the Go budget — ruling C-1 spirit, AGENTS.md law 1). No npm involvement — GIFs are opaque bytes to vite. - **Shapes:** filled rectangles, 1px box outlines, horizontal/vertical arrows, all drawn by hand loops (spike-proved, ~30 lines). - **Text:** embedded hand-authored **monospace 5x7 bitmap font covering full printable ASCII (32–126)**, stored as Go source (~95 glyphs × 7 rows of 5-bit strings, ≈ 2–3 KB), rendered at **2× scale** (10×14 px per glyph). - Why hand-authored, not `golang.org/x/image/font/basicfont`: x/image is outside the dependency budget and would need a written amendment; a 5x7 font table is trivially small to own and has zero license surface (shapes typed from scratch, verified by rendering). - Why 2×: the spike rendered 1× at 480×270 — legible but small; 2× at 640×360 is comfortably readable (verified by viewing decoded frames). - **Palette (dark-theme-friendly, 5 entries):** bg zinc-950 `#09090b`, fg zinc-200 `#e4e4e7`, accent emerald-500 `#10b981`, in-flight amber `#f59e0b`, dim zinc-500 `#71717a`. GIFs ship in dark-framed rounded cards in BOTH themes (README screenshot precedent) — one asset set, no light variants. - **Dimensions:** 640×360 for all four GIFs (16:9, fits content column without layout shift; `width`/`height` attributes set). - **Determinism:** fixed palette order, no timestamps, no map iteration in the encode path, fixed frame order; golden test regenerates into a buffer and byte-compares against checked-in files (`bytes.Equal`, fail on any drift — regeneration is then always intentional + reviewed). - **Stills:** generator also emits `<name>-still.gif` (frame 0) for the reduced-motion fallback (§4). ### 3.2 Storyboards with timings (acceptance: slow enough to follow) Timing convention: `Delay` units are 100ths of a second. Key frames hold 180–250 (1.8–2.5 s); motion steps 40–60 (0.4–0.6 s); total loop 6–9 s; ≤ 3 moving elements per frame; every frame carries a numbered caption ("1. YOU PUSH." …) baked at bottom-center + mirrored in adjacent text. **GIF 1 — `push.gif`: push → objects land in the bucket (7 frames, ≈ 8 s)** | # | Visual | Caption | Delay | |---|---|---|---| | 1 | Laptop box (emerald) left, bucket box (dim) right, nothing between | 1. YOU PUSH. | 200 | | 2 | Amber arrow grows laptop→bucket, "PACK" label above | 2. PACKS TRAVEL. | 50 | | 3 | Arrow full-length, packet block mid-flight | 2. PACKS TRAVEL. | 50 | | 4 | Packet arrives at bucket edge | 3. BUCKET WRITES. | 60 | | 5 | Bucket emerald, "+ PACK" row appears inside | 3. BUCKET WRITES. | 180 | | 6 | Green check path bucket→laptop (ACK arrow back) | 4. BUCKET ACKS FIRST. | 180 | | 7 | Rest state: both emerald, "+ PACK" persisted | 4. BUCKET ACKS FIRST. | 200 | **GIF 2 — `bucket.gif`: the bucket is the only database (6 frames, ≈ 8 s)** | # | Visual | Caption | Delay | |---|---|---|---| | 1 | Bucket (emerald) center with rows: REFS / PACKS / CONFIG / POLICY; instance box A (dim) above connected by thin line | 1. ONE BUCKET. | 200 | | 2 | Instance A flashes amber then grays out, connection line breaks | 2. INSTANCE DIES. | 60 | | 3 | Instance A box removed (blank space), bucket unchanged, rows intact | 2. INSTANCE DIES. | 200 | | 4 | Fresh instance box B appears (emerald), line reconnects | 3. NEW INSTANCE. | 60 | | 5 | B reads rows (each row highlights amber in turn — 2 sub-frames ×50) | 4. NOTHING LOST. | 100 | | 6 | Rest: B emerald, bucket emerald, "WARMTH ONLY" note | 4. NOTHING LOST. | 220 | **GIF 3 — `fetch.gif`: fetch/clone reads objects back (6 frames, ≈ 7 s)** | # | Visual | Caption | Delay | |---|---|---|---| | 1 | Bucket (emerald) left with REFS/PACKS rows; empty clone box (dim) right | 1. EMPTY CLONE. | 200 | | 2 | Thin arrow bucket→clone labeled "REFS (1)" | 2. REFS FIRST. | 60 | | 3 | Clone gains ref labels (amber, then emerald) | 2. REFS FIRST. | 150 | | 4 | Thick arrow bucket→clone labeled "PACKS" with moving block | 3. PACKS FOLLOW. | 50 | | 5 | Clone fills (file-tree glyph rows appear), both emerald | 4. WORKING COPY. | 200 | | 6 | Rest hold | 4. WORKING COPY. | 200 | **GIF 4 — `collab.gif`: collaboration as objects alongside git data (7 frames, ≈ 9 s)** | # | Visual | Caption | Delay | |---|---|---|---| | 1 | Bucket with GIT lane (REFS/PACKS, emerald) | 1. GIT LIVES HERE. | 200 | | 2 | Second lane slides in below: ISSUES / PRS / CHECKS (dim → amber) | 2. COLLAB MOVES IN. | 60 | | 3 | Issue card "#7 OPENED" lands in ISSUES row | 3. ISSUE 7 OPENS. | 150 | | 4 | "PR 8" card lands, thread line connects PR 8 ↔ ISSUE 7 ("FIXES 7") | 4. PR 8 FIXES 7. | 150 | | 5 | Check chip "CI: GREEN" stamps onto PR 8 | 5. CHECKS REPORT. | 150 | | 6 | WAL badge on GIT lane pulses; caption emphasizes separation | 6. WAL STAYS GIT-ONLY. | 200 | | 7 | Rest hold, all emerald | 6. WAL STAYS GIT-ONLY. | 200 | ### 3.3 Target file sizes (measured basis, not estimates) Spike measurements (640×360, 2× type, 5-color palette): 3-frame animated = **6,343 bytes**; single-frame still = **2,187 bytes**. Per-frame marginal cost ≈ 1.5–2 KB (flat-color LZW). Budgets: | Asset | Frames | Budget | Expected (from spike rate) | |---|---|---|---| | `push.gif` | 7 | ≤ 30 KB | ~12–15 KB | | `bucket.gif` | 6–7 | ≤ 30 KB | ~12–15 KB | | `fetch.gif` | 6 | ≤ 30 KB | ~10–14 KB | | `collab.gif` | 7 | ≤ 40 KB | ~14–20 KB | | 4 stills | 1 each | ≤ 4 KB each | ~2.2 KB each | | **Total GIF page weight** | | **≤ 150 KB** | **~60–75 KB** | For scale: the shipped JS bundle delta for D-WEB-7 alone was +22.2 KB gzip — all four GIFs + stills cost roughly 3× that, one time, lazy-loaded below the fold (`loading="lazy"` on all concept images). Sizes recorded in EVIDENCE.md (§5). ### 3.4 Make target + checked-in artifacts vs build-time generation **Decision: generator checked in + artifacts checked in + `make` target to regenerate. No build-time generation.** - Generator: `internal/devtools/landinggif/` — `main.go` (scene definitions), `font.go` (full ASCII 5x7 table), `draw.go` (boxes/arrows/text/blits), `main_test.go` (font coverage: every caption char used by every scene has a glyph — fail otherwise; determinism golden: encode-to-buffer twice → identical; asset freshness: regenerate → byte-equal to checked-in files). Placement in `internal/devtools/` is deliberate: `make cover` already excludes `devtools` (and `e2e`) from the ≥ 95% gate, so the pixel-pushing code doesn't distort package coverage; it still gets real unit tests. - Artifacts: `web/public/concepts/{push,bucket,fetch,collab}.gif` + `{...}-still.gif` — **8 files checked in**. `web/public/` is new (vite copies `public/` → `dist/` verbatim, surviving `emptyOutDir: true`; the SDK bundle step runs after and is unaffected). - Make: `make landing-gifs` → `go run ./internal/devtools/landinggif -out web/public/concepts/`; `make web` gains it as a prerequisite (order: gifs first, then `pnpm build`, so vite copies them into `dist/`). `go:embed all:dist` picks them up with no embed changes; `make clean` (`rm -rf web/dist`) stays safe because the sources of truth live in `web/public/`. - Rejected alternative — generate at Go/vite build time: hides bytes from review, makes `make web` depend on Go, complicates the Docker web stage; checked-in bytes are diffable and the golden test makes staleness loud. ### 3.5 Spike results (the resolved risk, evidence) - `/tmp/opencode/gifspike/main.go` (stdlib `image/gif` only, 30-glyph 5x7 subset, boxes + arrow + captions, 480×270): 3 frames = **4,111 bytes**, still = **1,387 bytes**. - 2×-type 640×360 variant: 3 frames = **6,343 bytes**, still = **2,187 bytes**. - Decoded frames viewed as PNG: 1× readable-but-small; **2× comfortably legible** at both body and caption sizes. - **Failure mode found and designed out:** the subset font silently dropped `3` ("3. BUCKET ACKS. DONE." rendered as ". BUCKET ACKS. DONE."). Hence the full-ASCII requirement + the font-coverage test that fails the build on any missing glyph. Never a silent space. --- ## 4. Accessibility - **Alt text** (baked into `ConceptGif.jsx` call sites, pinned by test): - push: "Animation: a laptop pushes a pack of git objects to a bucket; the bucket writes them, then acknowledges. The client only finishes after the bucket's acknowledgement." - bucket: "Animation: one bucket holds refs, packs, config, and policy while a server instance above it disappears and a fresh instance connects to the same bucket with nothing lost." - fetch: "Animation: an empty clone first receives ref names, then pack data, from the bucket until it holds a working copy." - collab: "Animation: alongside the git lane, issue 7 opens, pull request 8 links to it, a green check result stamps the PR — while the write-ahead log stays git-only." - **Reduced motion:** `ConceptGif` renders the `-still` image by default (also the `<noscript>` output); JS swaps in the animated `.gif` only when `matchMedia("(prefers-reduced-motion: reduce)").matches === false`. Listens for change events. No autoplay for users who asked for none. - Every GIF has an adjacent text caption + the section prose restating the point — no information is GIF-only. Focus/contrast: captions are real text (not images); keyboard: nothing interactive inside figures (`role="img"` + `aria-label` on the figure, or plain `img alt`). - Both themes: dark-framed cards; caption/body text uses existing `.muted` tokens (already contrast-checked in both themes). --- ## 5. EVIDENCE / perf - New EVIDENCE.md entry (landing-asset class, not a scaling claim — follows the "measured, not modeled" standard with the generator as harness): per-file byte sizes, total GIF weight, landing page weight (shell HTML + hashed JS/CSS + 4 GIFs lazy + 4 stills), `make landing-gifs` wall time, backend named (filesystem store irrelevant — static assets; note embed). - No hot-path impact to assert: landing makes **zero API/store calls**; `/explore` keeps the documented `1 + MAX_OWNERS` GET shape (`owners.list` + one `repos(owner)` per shown owner) — unchanged code, no new budget test needed, existing `owners.test.js` untouched. - No new npm deps (`package.json` runtime stays exactly the four amended packages); no new Go modules (`go.mod` untouched — generator is stdlib). `make vet`/`make build` unaffected beyond the new `uiAsset` prefix + MIME lines (covered by existing `x_health_test.go`-class table tests, extended with a gif row). --- ## 6. Acceptance criteria - [ ] `/` renders the landing page (hero + 4 concept sections + quickstart), `/explore` renders the owners list with behavior unchanged (caps, order, stars, activity, slimmed intro card). - [ ] Nav entry points at `/explore`; brand still `/`; `*` fallback renders landing; `?format=text` still answers the owner list at `/` (and now also at `/explore`). - [ ] GIFs followable: frame-timing table (§3.2) reproduced in the PR description; key-frame holds ≥ 1.8 s, motion steps ≥ 0.4 s, ≤ 3 moving elements per frame, numbered captions on every frame. - [ ] Deterministic: `make landing-gifs` output byte-identical to checked-in files (golden test); full-ASCII font + coverage test (the missing-`3` class can never recur silently). - [ ] Accessible: per-GIF alt text, still-first + `prefers-reduced-motion` swap, `<noscript>` still, adjacent text captions. - [ ] `node --test web/test/unit/*.test.js` green (incl. new landing + ConceptGif source-pin tests); `make vet`, `make test-go`, `make cover` green; no new npm/Go deps (`git diff package.json go.mod` empty). - [ ] Browser pass (real Chromium via the :9222 CDP daemon against the canonical host per AGENTS.md field lesson): `/` + `/explore` in dark AND light, zero console errors; GIFs animate, stills show under emulated reduced-motion; screenshots attached to the PR. - [ ] Docs updated in the same change: `docs/go/06_server_http.md` (§3 route tree + `?format=text` + `explore` name reservation), `docs/go/12_*` frontend doc (route table row, `web/public/concepts/` pipeline, `make landing-gifs`), EVIDENCE.md asset entry. No DEVIATIONS amendment needed (no budget change). Commit message names the sections. --- ## 7. Open questions / risks 1. **RESOLVED (§3.5): text without font libraries.** Hand-authored full-ASCII 5x7 at 2× is legible and tiny. Residual risk is aesthetic, not technical: bitmap type reads deliberately lo-fi/diagrammatic. Mitigation: strict palette discipline, generous whitespace, numbered captions; the design bar comes from composition, not font fidelity. If reviewers judge it cheap, the fallback is NOT a library (budget) but larger 3× display type for headlines inside frames — same technique, no new decision. 2. **Owner named `explore` loses its `/:owner` UI page** (§1.4). Precedent (`import`, `api`, `keys`, `setup`) makes this acceptable; documented, not coded around. Alternative (content-negotiate the single segment) rejected: wire-contract smell for one name. 3. **`?format=text` at `/` — keep or move?** Plan keeps it at `/` (compat) and mirrors at `/explore`. If maintainers prefer a clean break, the alternative is 308 `/ → /explore?format=text` for text-Accept requests — one line, but it changes documented behavior; keep is the conservative default. 4. **Single dark GIF set in light theme.** Cards carry their own dark frame in both themes (screenshot precedent). Risk: a light-theme purist objects. Alternative (dual light/dark GIF sets) doubles weight and generator scenes for zero information gain — not recommended. 5. **GIF vs modern formats.** Issue mandates GIFs; APNG/WebP would need non-stdlib encoders (out of budget) or browser-side SMIL (not a GIF). GIF it is — LZW on flat colors is near-optimal for this content anyway (~2 KB/frame measured). 6. **Infinite loop vs finite.** `LoopCount: 0` (infinite) is standard for concept loops; reduced-motion users never see it (§4). A finite 3-loop alternative strands sighted users mid-explanation on long dwell — not recommended. 7. **Scope guard:** this plan adds no backend behavior (no policy, no auth, no store, no WAL touch). Any urge to make the landing page "live" (repo counts, activity feeds) is out of scope — it would spend store round trips on the front door and violate the static-page decision. `/explore` is the live page.
Author
Owner

Review: Landing page plan (issue #187)

Verdict: proceed-with-fixes — 3 BLOCKING items (all small, all must be resolved in the implementation change, none requires re-planning). Design is sound, law analysis is honest, spike de-risked the hard part.

What I verified against code (all plan claims hold unless noted)

  • Explicit /explore backend route: NEEDED — but the stated reason is slightly off. Without it, GET /explore hits r.HandleFunc("/*", s.repoDispatch) (router.go L127), len(segs)<2 → s.gated(s.ownerPage) → still serves the SPA shell (200). So the shell would load either way; the explicit route is needed to (a) serve the ?format=text branch at /explore (that branch lives only in spaHome, health.go L148-166 — ownerPage L169 never checks it), and (b) document the name reservation. Fix the justification; the route itself is correct, and chi static-beats-wildcard ordering is safe placed next to r.Get("/").
  • ?format=text location cited correctly (doc 06 L122 route tree + L212 gated list; code health.go L148-166). Extract-to-shared-helper plan is right.
  • uiAsset allowlist + MIME: correct. Only index.html + assets/* pass (health.go L346-352); .gif would fall to application/octet-stream (L285). Both planned lines are needed. Caching class no-cache + ETag (not immutable) is the right call for stable filenames.
  • go:embed all:dist (web/embed.go L12) picks up dist/concepts/ with zero embed changes. Confirmed.
  • Vite public/ handling: correct. vite.config.mjs has emptyOutDir: true and default publicDir; vite empties dist/ at start of build:ui, then copies public/ → dist/, then build:sdk appends dist/repos.js. Order is safe. .gitignore ignores web/dist/* but NOT web/public/ — checked-in web/public/concepts/*.gif survive make clean. Correct.
  • Docker web stage needs NO change (COPY web/ ./ carries public/; pnpm run build copies it). Correct.
  • make cover excludes devtools: correct (Makefile L36: grep -v devtools). internal/devtools/landinggif keeps the 95% gate clean while its tests still run under go test ./.... Correct.
  • Owners behavior to preserve: quoted accurately — MAX_OWNERS 50 / MAX_REPOS_PER_OWNER 10 (lib/owners.js), newest-first proxy, +N more overflow, import button, stars, activity stamps (commits?n=1 source), two-column RepoRow grid. All present in Owners.jsx.
  • README language: near-verbatim, one imprecision (nit N3 below).
  • Collab copy matches features/README architectural law (WAL git-only; meta/, issues/, checks/, releases/, shared numbering). Correct.
  • Fallback * → Owners today (index.jsx L83), nav owners → / (App.jsx L52), footer text (App.jsx L81), Owners header comment route "/" (Owners.jsx L1). All citations check out.
  • No seam concerns: no new API endpoints, no SDK methods, no registry touches — law 8 clean. internal/server-only backend diff.
  • Concurrency section adequate: generator single-threaded CLI, landing zero-fetch. Nothing to lock.

BLOCKING

B1 — Pin the served GIF URL prefix: /_ui/concepts/<name>.gif. The plan says uiAsset maps concepts/ → dist/concepts/ but never states what src ConceptGif renders. With base: "/_ui/", vite copies public/concepts/push.gif → dist/concepts/push.gif, served at /_ui/concepts/push.gif. A src="/concepts/push.gif" implementation 404s (no such backend route; repoDispatch would try owner concepts). The change must pin src="/_ui/concepts/..." (and the test must assert the /_ui/ prefix).

B2 — compress middleware will gzip the GIFs; bypass image/*. Doc 12 section 3 claims gzip applies to text assets, but the CODE compresses everything except SSE/precompressed (compressWriter.WriteHeader, middleware.go L596-607 — no content-type gate). Since /_ui is wrapped in s.compress (router.go L112-114), every GIF response would pay a pointless gzip pass (LZW bytes; CPU per request, Vary/Content-Length churn) unless bypassed. Add image/ to the bypass condition and note the pre-existing doc/code drift in the same change. One-line fix, must not be forgotten.

B3 — Do NOT make make web depend on make landing-gifs. The plan (section 3.4) wires gif regeneration as a prerequisite of make web, which directly contradicts its own No build-time generation decision two paragraphs up — and make vet, test-go, and race ALL depend on web, so every contributor build would pay a Go-run regen step (and risk dirty-tree diffs if the generator ever drifts). Keep landing-gifs a MANUAL target run only when scenes change; vite copies the checked-in public/ files on every normal build, and the golden freshness test (regenerate → byte-equal) already makes staleness loud in CI. Decouple.

Should-fix

S1 — ?format=text at BOTH / and /explore violates the pre-1.0 no-alias law (AGENTS.md: no aliases, shims, deprecated flags, or still accepted for branches). The text list is the machine twin of the owners page, which is moving to /explore — its canonical home is /explore. Either move it or record a written exception in 06 Decisions. Keeping both silently is the one place the plan bends a law without saying so. Section 7.3 already frames the question — make the call before code lands.

S2 — Verify /:owner/:name/<badsub> still renders the repo shell, not Landing. Server-side, most unmatched deep paths 404 as plain text via repoDispatch (never reach the SPA), so the frontend * → Landing fallback fires far less often than the plan implies. The real risk is Solid nested-router behavior for /o/r/<unknown>: parent Repo matches with no child — confirm it renders the shell with empty outlet (current behavior) rather than falling through to *. Add to acceptance; Landing-as-fallback is otherwise safe (zero API calls — agreed).

S3 — Collab GIF is the tightest scene: review it first. 7 frames with the most distinct elements (GIT lane + collab lane + issue/PR/check cards + WAL badge) against a 40KB budget and the 3-moving-elements rule. If any scene breaks the followability budget, it is this one. Suggest implementing/reviewing collab.gif frames first; fallback is splitting into two GIFs, not enlarging.

Nits

  • N1 — role="img" + aria-label on <figure> AND alt on <img> double-announces. Plan says or — enforce exactly one in code review (prefer plain img alt; drop the figure role).
  • N2 — /explore self-link loop for an owner literally named explore. Its section link points at /explore, which now renders the list, not its repos. Harmless, but name it in the 06 reservation note alongside the shadowing.
  • N3 — README quote imprecision. README L5 says entire state — refs, packs, config, policy, events, web UI — lives as objects; the plan compresses to refs, packs, config, and policy. Either quote exactly or keep the full set — the landing hero should not narrow the claim the README makes.
  • N4 — GIF delay floor. 40-unit (0.4s) motion steps are above browser minimum-delay clamping (~50ms), so timings reproduce faithfully. No change; just do not tune any step below 40.
  • N5 — Font provenance. Hand-authored 5x7 keeps zero license surface only if glyphs are actually drawn from scratch, not traced from a font file. Golden tests cannot check this — reviewer eyeball at implementation time.
  • N6 — Consider decoding="async" on concept images alongside loading="lazy" + explicit width/height (already planned).

Scope judgment

Nothing missing that matters; nothing to cut except process weight. The open questions (section 7) are all resolved in the recommended direction except 7.3 (S1 above). EVIDENCE.md asset entry, 06 section 3.3 reservation doc, 12 route-table row, x_health_test-style gif row, and source-pin node --test for CTAs/alts are all proportionate. The ?format=text 308 alternative should stay rejected (it changes documented behavior for zero benefit while S1 is pending).

# Review: Landing page plan (issue #187) **Verdict: proceed-with-fixes** — 3 BLOCKING items (all small, all must be resolved in the implementation change, none requires re-planning). Design is sound, law analysis is honest, spike de-risked the hard part. ## What I verified against code (all plan claims hold unless noted) - **Explicit `/explore` backend route: NEEDED — but the stated reason is slightly off.** Without it, `GET /explore` hits `r.HandleFunc("/*", s.repoDispatch)` (router.go L127), `len(segs)<2` → `s.gated(s.ownerPage)` → still serves the SPA shell (200). So the shell would load either way; the explicit route is needed to (a) serve the `?format=text` branch at `/explore` (that branch lives only in `spaHome`, health.go L148-166 — `ownerPage` L169 never checks it), and (b) document the name reservation. Fix the justification; the route itself is correct, and chi static-beats-wildcard ordering is safe placed next to `r.Get("/")`. - **`?format=text` location cited correctly** (doc 06 L122 route tree + L212 gated list; code health.go L148-166). Extract-to-shared-helper plan is right. - **`uiAsset` allowlist + MIME: correct.** Only `index.html` + `assets/*` pass (health.go L346-352); `.gif` would fall to `application/octet-stream` (L285). Both planned lines are needed. Caching class `no-cache + ETag` (not immutable) is the right call for stable filenames. - **`go:embed all:dist` (web/embed.go L12) picks up `dist/concepts/` with zero embed changes.** Confirmed. - **Vite `public/` handling: correct.** vite.config.mjs has `emptyOutDir: true` and default `publicDir`; vite empties `dist/` at start of `build:ui`, then copies `public/` → `dist/`, then `build:sdk` appends `dist/repos.js`. Order is safe. `.gitignore` ignores `web/dist/*` but NOT `web/public/` — checked-in `web/public/concepts/*.gif` survive `make clean`. Correct. - **Docker web stage needs NO change** (`COPY web/ ./` carries `public/`; `pnpm run build` copies it). Correct. - **`make cover` excludes devtools: correct** (Makefile L36: `grep -v devtools`). `internal/devtools/landinggif` keeps the 95% gate clean while its tests still run under `go test ./...`. Correct. - **Owners behavior to preserve: quoted accurately** — `MAX_OWNERS` 50 / `MAX_REPOS_PER_OWNER` 10 (lib/owners.js), newest-first proxy, `+N more` overflow, import button, stars, activity stamps (commits?n=1 source), two-column `RepoRow` grid. All present in Owners.jsx. - **README language: near-verbatim, one imprecision** (nit N3 below). - **Collab copy matches features/README architectural law** (WAL git-only; `meta/`, `issues/`, `checks/`, `releases/`, shared numbering). Correct. - **Fallback `*` → Owners today** (index.jsx L83), nav `owners → /` (App.jsx L52), footer text (App.jsx L81), Owners header comment `route "/"` (Owners.jsx L1). All citations check out. - **No seam concerns**: no new API endpoints, no SDK methods, no registry touches — law 8 clean. `internal/server`-only backend diff. - **Concurrency section adequate**: generator single-threaded CLI, landing zero-fetch. Nothing to lock. ## BLOCKING **B1 — Pin the served GIF URL prefix: `/_ui/concepts/<name>.gif`.** The plan says uiAsset maps `concepts/` → `dist/concepts/` but never states what `src` ConceptGif renders. With `base: "/_ui/"`, vite copies `public/concepts/push.gif` → `dist/concepts/push.gif`, served at `/_ui/concepts/push.gif`. A `src="/concepts/push.gif"` implementation 404s (no such backend route; repoDispatch would try owner `concepts`). The change must pin `src="/_ui/concepts/..."` (and the test must assert the `/_ui/` prefix). **B2 — `compress` middleware will gzip the GIFs; bypass `image/*`.** Doc 12 section 3 claims gzip applies to text assets, but the CODE compresses everything except SSE/precompressed (`compressWriter.WriteHeader`, middleware.go L596-607 — no content-type gate). Since `/_ui` is wrapped in `s.compress` (router.go L112-114), every GIF response would pay a pointless gzip pass (LZW bytes; CPU per request, Vary/Content-Length churn) unless bypassed. Add `image/` to the bypass condition and note the pre-existing doc/code drift in the same change. One-line fix, must not be forgotten. **B3 — Do NOT make `make web` depend on `make landing-gifs`.** The plan (section 3.4) wires gif regeneration as a prerequisite of `make web`, which directly contradicts its own No build-time generation decision two paragraphs up — and `make vet`, `test-go`, and `race` ALL depend on `web`, so every contributor build would pay a Go-run regen step (and risk dirty-tree diffs if the generator ever drifts). Keep `landing-gifs` a MANUAL target run only when scenes change; vite copies the checked-in `public/` files on every normal build, and the golden freshness test (regenerate → byte-equal) already makes staleness loud in CI. Decouple. ## Should-fix **S1 — `?format=text` at BOTH `/` and `/explore` violates the pre-1.0 no-alias law** (AGENTS.md: no aliases, shims, deprecated flags, or still accepted for branches). The text list is the machine twin of the owners page, which is moving to `/explore` — its canonical home is `/explore`. Either move it or record a written exception in 06 Decisions. Keeping both silently is the one place the plan bends a law without saying so. Section 7.3 already frames the question — make the call before code lands. **S2 — Verify `/:owner/:name/<badsub>` still renders the repo shell, not Landing.** Server-side, most unmatched deep paths 404 as plain text via repoDispatch (never reach the SPA), so the frontend `*` → Landing fallback fires far less often than the plan implies. The real risk is Solid nested-router behavior for `/o/r/<unknown>`: parent `Repo` matches with no child — confirm it renders the shell with empty outlet (current behavior) rather than falling through to `*`. Add to acceptance; Landing-as-fallback is otherwise safe (zero API calls — agreed). **S3 — Collab GIF is the tightest scene: review it first.** 7 frames with the most distinct elements (GIT lane + collab lane + issue/PR/check cards + WAL badge) against a 40KB budget and the 3-moving-elements rule. If any scene breaks the followability budget, it is this one. Suggest implementing/reviewing `collab.gif` frames first; fallback is splitting into two GIFs, not enlarging. ## Nits - **N1 — `role="img" + aria-label` on `<figure>` AND `alt` on `<img>` double-announces.** Plan says or — enforce exactly one in code review (prefer plain `img alt`; drop the figure role). - **N2 — `/explore` self-link loop for an owner literally named `explore`.** Its section link points at `/explore`, which now renders the list, not its repos. Harmless, but name it in the 06 reservation note alongside the shadowing. - **N3 — README quote imprecision.** README L5 says entire state — refs, packs, config, policy, events, web UI — lives as objects; the plan compresses to refs, packs, config, and policy. Either quote exactly or keep the full set — the landing hero should not narrow the claim the README makes. - **N4 — GIF delay floor.** 40-unit (0.4s) motion steps are above browser minimum-delay clamping (~50ms), so timings reproduce faithfully. No change; just do not tune any step below 40. - **N5 — Font provenance.** Hand-authored 5x7 keeps zero license surface only if glyphs are actually drawn from scratch, not traced from a font file. Golden tests cannot check this — reviewer eyeball at implementation time. - **N6 — Consider `decoding="async"` on concept images** alongside `loading="lazy"` + explicit `width`/`height` (already planned). ## Scope judgment Nothing missing that matters; nothing to cut except process weight. The open questions (section 7) are all resolved in the recommended direction except 7.3 (S1 above). EVIDENCE.md asset entry, 06 section 3.3 reservation doc, 12 route-table row, `x_health_test`-style gif row, and source-pin `node --test` for CTAs/alts are all proportionate. The `?format=text` 308 alternative should stay rejected (it changes documented behavior for zero benefit while S1 is pending).
Author
Owner

Plan revision R1 (review findings — R1 wins on conflict)

Blocking (normative)

  • B1 — served GIF URL prefix pinned: /_ui/concepts/<name>.gif. ConceptGif renders src="/_ui/concepts/…", never /concepts/…; tests assert the prefix.
  • B2 — bypass image/* in the compress middleware (it currently gzips everything except SSE/precompressed). One-line fix + note the pre-existing doc/code drift in the same change.
  • B3 — make landing-gifs stays MANUAL (run only when scenes change); NOT a prerequisite of make web. Freshness enforced by the golden byte-equality test in CI.

Should-fix adoptions

  • S1 — ?format=text MOVES to /explore (canonical home of the owners list; the machine twin follows the page). / keeps no text branch. No 308, no silent alias (pre-1.0 no-alias law); record the move in 06 §Decisions.
  • S2 — verify /o/r/<badsub> behavior (repo shell with empty outlet, not Landing); add to acceptance.
  • S3 — implement/review collab.gif frames first (tightest scene); fallback is splitting, not enlarging.

Nits adopted

N1 exactly one of img alt (preferred) vs figure role; N2 name the explore-owner self-link loop in the reservation note; N3 quote README exactly (full set incl. events + web UI); N4 no step below 40 units; N5 glyphs drawn from scratch (reviewer eyeball); N6 decoding="async" + lazy + explicit dimensions.

# Plan revision R1 (review findings — R1 wins on conflict) ## Blocking (normative) - **B1 — served GIF URL prefix pinned: `/_ui/concepts/<name>.gif`.** ConceptGif renders `src="/_ui/concepts/…"`, never `/concepts/…`; tests assert the prefix. - **B2 — bypass `image/*` in the compress middleware** (it currently gzips everything except SSE/precompressed). One-line fix + note the pre-existing doc/code drift in the same change. - **B3 — `make landing-gifs` stays MANUAL** (run only when scenes change); NOT a prerequisite of `make web`. Freshness enforced by the golden byte-equality test in CI. ## Should-fix adoptions - **S1 — `?format=text` MOVES to `/explore`** (canonical home of the owners list; the machine twin follows the page). `/` keeps no text branch. No 308, no silent alias (pre-1.0 no-alias law); record the move in 06 §Decisions. - **S2 — verify `/o/r/<badsub>` behavior** (repo shell with empty outlet, not Landing); add to acceptance. - **S3 — implement/review `collab.gif` frames first** (tightest scene); fallback is splitting, not enlarging. ## Nits adopted N1 exactly one of `img alt` (preferred) vs figure role; N2 name the `explore`-owner self-link loop in the reservation note; N3 quote README exactly (full set incl. events + web UI); N4 no step below 40 units; N5 glyphs drawn from scratch (reviewer eyeball); N6 `decoding="async"` + lazy + explicit dimensions.
Author
Owner

Implemented by PR #188 (branch feat/issue-187): #188 — landing page at / with 4 animated concept GIFs (82,618 bytes total, all budgets ~45% headroom), owners list moved to /explore with ?format=text, compress bypasses image/*, make landing-gifs manual. All gates green: gofmt/vet, go test -race (server), make cover (48 pkgs ≥95%), node --test 386 pass, real-Chromium 4/4 clean with zero console errors (screenshots verified locally — attachment upload is not available from this session, happy to attach on request). Review notes: R1 adopted throughout (B1/B2/B3/S1/S2/S3/N1–N6); one nit-level call — bucket.gif ships 7 frames (inside the 6–7 budget row). Do NOT merge from my side.

Implemented by PR #188 (branch feat/issue-187): https://git.packden.us/crueber/walhub/pulls/188 — landing page at / with 4 animated concept GIFs (82,618 bytes total, all budgets ~45% headroom), owners list moved to /explore with ?format=text, compress bypasses image/*, make landing-gifs manual. All gates green: gofmt/vet, go test -race (server), make cover (48 pkgs ≥95%), node --test 386 pass, real-Chromium 4/4 clean with zero console errors (screenshots verified locally — attachment upload is not available from this session, happy to attach on request). Review notes: R1 adopted throughout (B1/B2/B3/S1/S2/S3/N1–N6); one nit-level call — bucket.gif ships 7 frames (inside the 6–7 budget row). Do NOT merge from my side.
Author
Owner

Review: PR #188 vs issue #187 plan + R1 (verified in scratch worktree at origin/feat/issue-187, web built, dist/concepts/ confirmed copied by vite).

VERDICT: ready to merge. One factual nit found and fixed by me (pushed a0dc425 to feat/issue-187): loop-time annotations understated the delay sums. No delay/byte changes — GIFs untouched.

R1 compliance, point by point (all hold):

  • B1 prefix pinned + asserted: ConceptGif.jsx renders /_ui/concepts/.gif (conceptSrc/conceptStillSrc); landing.test.js asserts the prefix and rejects bare /concepts/. Backend uiAsset maps concepts/*.gif -> dist/concepts/ with image/gif MIME, no-cache + ETag (health.go).
  • B2 image bypass + drift note: middleware.go bypasses image/* alongside SSE/precompressed with an inline comment; 06 Decisions + 12 compression paragraph record the pre-existing doc/code drift. x_health_test.go TestCompressBypassesImages pins it (gif bypasses, html still gzips).
  • B3 manual target, NOT a web prerequisite — verified in Makefile: landing-gifs is a standalone target; the web: target is still just pnpm run build. Freshness enforced by TestFreshness instead.
  • S1 text MOVED not aliased: spaHome is now serveSPA unconditionally; serveSPA has no Accept/query branch (pure shell); the only text branch lives in explorePage. TestSPAHome asserts /?format=text serves the shell; TestExplorePage asserts the list + content-type + html-accept + 503 paths. No branch serves both. No 308.
  • S2 badsub: repoDispatch untouched, so /o/r/ still 404s plain text server-side and never reaches the SPA; frontend * fallback only fires on unmatched top-level paths. Author's live check (repo shell + empty outlet) accepted; nothing in this diff can change it.
  • S3 collab-first: scenes() lists collab first with an S3 comment; collab.gif is the richest scene and still fits (22,912 of 40KB).
  • N1 alt-only: no role=img / aria-label anywhere; plain img alt from call sites, test-asserted absent.
  • N2 reservation note: 06 gated section names the explore-owner self-link loop explicitly.
  • N3 exact README quote: hero sub carries the full set (refs, packs, config, policy, events, web UI) + disposable-instances line, matching README L5.
  • N4 floor: TestFrameTimingFloor fails below 40; shipped minimum step is 50 (decoded below).
  • N5 provenance: font.go glyphs are hand-typed 5x7 shape literals with a from-scratch header; eyeball agrees, no font-file trace.
  • N6: loading=lazy + decoding=async + width=640 height=360 in ConceptGif, all test-pinned.

Route correctness: * -> Landing is safe (zero API calls, test-pinned: no useData/SDK/fetch/EventSource); /explore explicit next to / with the shadowing rationale in situ; setup-only 503s /explore (mountSetupOnly registers no /explore route -> 503 NotFound). Owners.jsx diff is header comment + slimmed intro only; caps/order/stars/activity/overflow logic untouched.

GIF delays decoded by me (GCE walk, stdlib python): push [200 50 50 60 180 180 200]=9.2s; bucket [200 60 200 60 50 50 220]=8.4s; fetch [200 60 150 50 200 200]=8.6s; collab [200 60 150 150 150 200 200]=11.1s. Frame counts match storyboard; every GIF has holds >=180; min step 50 >= 40 floor. Corrected totals (my commit): EVIDENCE E12 said 8.0/7.1/9.1 for push/fetch/collab; scene comments said ~8/~7/~9s. The delays were always right; only the derived totals were off. PR description timing line still carries the old totals — worth editing to 9.2/8.4/8.6/11.1.

Budgets (file bytes): push 16,773 / bucket 17,793 / fetch 15,874 / collab 22,912 / stills 2227+2569+2493+1977 / total 82,618 — all inside ceilings. Determinism genuine: TestDeterministic (re-encode byte-equal) + TestFreshness (regen byte-equal to checked-in files) both pass. Generator imports are stdlib-only (bytes/flag/fmt/image(+color/gif)/os/path/filepath/time); git diff on go.mod/go.sum/package.json/pnpm-lock.yaml is empty.

Tests (scratch, after my fix): go test -race ./internal/server/ ok; go test ./internal/devtools/landinggif/ 6/6 pass; node --test web/test/unit/*.test.js 386 pass 0 fail; gofmt clean; go vet clean; server coverage 95.6% (>=95% gate). No browser from my side per review instructions (node+go+reasoning); author reports Chromium 4/4 clean — build output matches EVIDENCE sizes (JS 430.37kB/CSS 71.79kB), so the weighed page is as documented.

MERGE RECOMMENDATION: ready to merge (do not merge from review side per author note).

Review: PR #188 vs issue #187 plan + R1 (verified in scratch worktree at origin/feat/issue-187, web built, dist/concepts/ confirmed copied by vite). VERDICT: ready to merge. One factual nit found and fixed by me (pushed a0dc425 to feat/issue-187): loop-time annotations understated the delay sums. No delay/byte changes — GIFs untouched. R1 compliance, point by point (all hold): - B1 prefix pinned + asserted: ConceptGif.jsx renders /_ui/concepts/<name>.gif (conceptSrc/conceptStillSrc); landing.test.js asserts the prefix and rejects bare /concepts/. Backend uiAsset maps concepts/*.gif -> dist/concepts/ with image/gif MIME, no-cache + ETag (health.go). - B2 image bypass + drift note: middleware.go bypasses image/* alongside SSE/precompressed with an inline comment; 06 Decisions + 12 compression paragraph record the pre-existing doc/code drift. x_health_test.go TestCompressBypassesImages pins it (gif bypasses, html still gzips). - B3 manual target, NOT a web prerequisite — verified in Makefile: landing-gifs is a standalone target; the web: target is still just pnpm run build. Freshness enforced by TestFreshness instead. - S1 text MOVED not aliased: spaHome is now serveSPA unconditionally; serveSPA has no Accept/query branch (pure shell); the only text branch lives in explorePage. TestSPAHome asserts /?format=text serves the shell; TestExplorePage asserts the list + content-type + html-accept + 503 paths. No branch serves both. No 308. - S2 badsub: repoDispatch untouched, so /o/r/<badsub> still 404s plain text server-side and never reaches the SPA; frontend * fallback only fires on unmatched top-level paths. Author's live check (repo shell + empty outlet) accepted; nothing in this diff can change it. - S3 collab-first: scenes() lists collab first with an S3 comment; collab.gif is the richest scene and still fits (22,912 of 40KB). - N1 alt-only: no role=img / aria-label anywhere; plain img alt from call sites, test-asserted absent. - N2 reservation note: 06 gated section names the explore-owner self-link loop explicitly. - N3 exact README quote: hero sub carries the full set (refs, packs, config, policy, events, web UI) + disposable-instances line, matching README L5. - N4 floor: TestFrameTimingFloor fails below 40; shipped minimum step is 50 (decoded below). - N5 provenance: font.go glyphs are hand-typed 5x7 shape literals with a from-scratch header; eyeball agrees, no font-file trace. - N6: loading=lazy + decoding=async + width=640 height=360 in ConceptGif, all test-pinned. Route correctness: * -> Landing is safe (zero API calls, test-pinned: no useData/SDK/fetch/EventSource); /explore explicit next to / with the shadowing rationale in situ; setup-only 503s /explore (mountSetupOnly registers no /explore route -> 503 NotFound). Owners.jsx diff is header comment + slimmed intro only; caps/order/stars/activity/overflow logic untouched. GIF delays decoded by me (GCE walk, stdlib python): push [200 50 50 60 180 180 200]=9.2s; bucket [200 60 200 60 50 50 220]=8.4s; fetch [200 60 150 50 200 200]=8.6s; collab [200 60 150 150 150 200 200]=11.1s. Frame counts match storyboard; every GIF has holds >=180; min step 50 >= 40 floor. Corrected totals (my commit): EVIDENCE E12 said 8.0/7.1/9.1 for push/fetch/collab; scene comments said ~8/~7/~9s. The delays were always right; only the derived totals were off. PR description timing line still carries the old totals — worth editing to 9.2/8.4/8.6/11.1. Budgets (file bytes): push 16,773 / bucket 17,793 / fetch 15,874 / collab 22,912 / stills 2227+2569+2493+1977 / total 82,618 — all inside ceilings. Determinism genuine: TestDeterministic (re-encode byte-equal) + TestFreshness (regen byte-equal to checked-in files) both pass. Generator imports are stdlib-only (bytes/flag/fmt/image(+color/gif)/os/path/filepath/time); git diff on go.mod/go.sum/package.json/pnpm-lock.yaml is empty. Tests (scratch, after my fix): go test -race ./internal/server/ ok; go test ./internal/devtools/landinggif/ 6/6 pass; node --test web/test/unit/*.test.js 386 pass 0 fail; gofmt clean; go vet clean; server coverage 95.6% (>=95% gate). No browser from my side per review instructions (node+go+reasoning); author reports Chromium 4/4 clean — build output matches EVIDENCE sizes (JS 430.37kB/CSS 71.79kB), so the weighed page is as documented. MERGE RECOMMENDATION: ready to merge (do not merge from review side per author note).
Author
Owner

Implemented in PR #188 (review: all 12 R1 rulings verified + GIF timings independently decoded; 386/386 + 95.6%), merged. Closing.

Implemented in PR #188 (review: all 12 R1 rulings verified + GIF timings independently decoded; 386/386 + 95.6%), merged. Closing.
crueber added this to the v1 milestone 2026-09-10 22:27:15 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#187
No description provided.