Website and docs grill #339

Open
opened 2026-11-04 09:52:10 +00:00 by kayg · 0 comments
Owner

Job J184 (research only, no code changed). The owner deferred this grill (D325: "needs its own grill"; DECISIONS end: "The website and docs ... are grilled later"; D317 left two items open). Each question says what the thing is, what happens today, the options, and a recommendation. Quotes are the owner's own words from DECISIONS.

Base read: origin/dev, plus the live site https://ssh.kayg.org fetched on 2026-11-04.

What exists today (facts, so the questions make sense)

  • Public docs today = https://ssh.kayg.org/docs/, which is the owner's production instance. No sign-in is needed for /docs. Nine sections, 12 markdown files, about 33 KB of text in total: Get started, Terminal (Overview + Multiplexer), Machines, Mesh, Access, You, Sharing, Appearance, Reference (Overview + Keys). Most sections are still only an overview page. The old flat address /docs/multiplexer/ redirects to /docs/terminal/multiplexer/ (D317).
  • How it is built: apps/tracer/docs-build.ts renders docs/user/**/*.md to plain static HTML (no framework, no new dependency). The same output is (a) put in the server's web root, so every instance serves the docs of its own version (D315, #298), and (b) read inside the app as the "Docs" view on the canvas, found through the search palette (⌘K) from index.json (titles and headings only, 6.7 KB). The pages load only a small doc.css and a deferred doc.js. The build fails on a skipped heading level, a missing summary, or a broken link or "?" anchor.
  • Figures (screenshots and clips): bun run docs:figures drives a production build of the app with a demo dataset and writes docs/user/figures/<name>.<desktop|phone>.<ink|paper>.webp (plus .webm clips). That folder is git-ignored: "where the media of the docs lives is not decided, so no picture is in the repository" (docs-figure.ts). A page without pictures still builds and shows the caption under a "Figure pending" box. No page uses a figure yet.
  • Public website infraternal.com: from the machine I ran on, infraternal.com does not resolve at all; builds.infraternal.com answers a Cloudflare "Not Found" page (the domain exists, nothing is served at its root). apps/website does not exist on dev. Issue #3 (S0 Forgejo Actions to Cloudflare Pages) is still open. So no public marketing or docs site exists yet; ssh.kayg.org/docs is the only public docs.
  • Decisions that apply: D44 (static SvelteKit site, docs in Astro Starlight, Cloudflare Pages + Wrangler), D45 (hook is SSH, claim only what works), D46 (no third-party trackers, no numbers on pricing; landing/roadmap/waitlist), D47 (shared UI), D220 (the site is exactly /releases, /docs, /changelog; landing, roadmap and waitlist dropped), D223 (Pages for the site, R2 bucket builds.infraternal.com for signed binaries), D298/D315 (each instance serves its own version's docs; the official ones are the same build at infraternal.com/docs), D317 (structure, addresses, one heading per setting, no page for unbuilt features), D325 (one site with docs under /docs, "agreed but needs its own grill"; versions are commit ids until releases; media in a separate media repository, "agreed"; the app bundles docs text but not media: "Media, I don't think so. They need to be pulled on demand."), D159 (standalone docs pages keep the browser's zoom).
  • Conflict to record: D44 says the docs are Astro Starlight in apps/docs. What is actually built is our own small renderer inside apps/tracer. This should be recorded as a decision (Q2).

Terms used below: Cloudflare Pages = static file hosting with deploys from a command; per-file size limit is small, which is why binaries go to R2. R2 = Cloudflare's object store (like S3), public through a custom domain. Forgejo registry = the package store built into git.kayg.org (a "generic" package is any file set under a name and version; a "container" package is an OCI image). Hash-pinned = a file is named or checked by its SHA-256, so the exact bytes are fixed and tampering is detected. Content-addressed = stored under its own hash, so the same bytes are stored once and never change. Figure = a docs screenshot or clip. Instance = one running server, for example ssh.kayg.org.


Q1. What is the public website for, and what are its pages?

What it is: infraternal.com is the public face: where a stranger learns what this is and gets the software; the docs are one part of it.
Today: nothing public is served at infraternal.com (see above). D220 says exactly three pages: /releases, /docs, /changelog. D45 says the hook is SSH ("a self-hosted terminal that feels local, on every device"). D220 dropped the landing page, so / has no decided content.
Options:

  • A. Strictly the three pages; / redirects to /docs (or to /releases). Least to build and nothing to over-claim. A first-time visitor has no pitch.
  • B. Three pages plus one calm front page at /: one sentence (D45), one screenshot (from the same figure pipeline, Q4), the install command and links to the three pages. No roadmap, no waitlist, no pricing.
  • C. Restore a full landing page, roadmap and waitlist (reverses D220).
    Recommendation: B. It keeps D220's "claim only what works" and costs one small page, and the install command and a real screenshot are the only things a visitor needs. Pricing and waitlist stay out (D46, no numbers in the repo).
    Also decide: does /changelog come from git history (generated from commit subjects with decision numbers), or is it written by hand per release? I recommend generated from tagged releases, with hand-written highlights only when there are releases.

Q2. One site, one generator: record that the docs are our own renderer, not Starlight

What it is: the tool that turns the docs source into web pages. D44 chose Astro Starlight (a ready-made docs framework). We built our own small renderer instead, so the same pages work inside the app, on every instance and on the public site with no framework.
Today: docs-build.ts is shipped on dev and tested. apps/docs and apps/website do not exist.
Options:

  • A. Keep our renderer for the docs, and build the rest of the site (releases, changelog, front page) as plain static pages from the same code. One generator, no framework, instance docs and public docs identical by construction.
  • B. Public site in SvelteKit (D44) and docs by our renderer, the site links into /docs. Two build systems and two looks.
  • C. Move everything to Starlight. A second renderer for the in-app Docs view, and the instance copy would drift from the public copy.
    Recommendation: A, and amend D44 accordingly. The docs reader is part of the app (palette, canvas window, anchors checked at build time); a framework cannot replace that, and two generators means two places for a style bug. SvelteKit stays an option only if the site later grows interactive parts. This needs your word because it changes D44, and only you can change a decision.

Q3. Who are the docs for, and how are they laid out?

What it is: the audience decides tone and what we write.
Today: D317: written "for the people who use it" (the home page says so), sections follow Settings order, nested addresses /docs/<section>/<page>/#<heading> (two levels at most), one heading per setting with a quick-facts block (the only place a default and range are written), no page for unbuilt features (only a "Planned" callout). Not covered by D317: contributors, operators who install and run server init, and API/MCP/CLI users.
Options:

  • A. One audience: people using the app. Keep D317 as is. Operator, CLI, API and MCP pages are added later as sections of the same tree when those exist (D229 already says server init is documented on the website).
  • B. Two docs: user docs (/docs) and developer docs (/dev, from docs/ARCHITECTURE.md, protocol, contributing).
  • C. Three audiences from the start: users, operators (install, update, backup, mesh, reverse proxy) and developers.
    Recommendation: A now, with "Install and run a server" added to Get started (the first thing a stranger needs, and it does exist: server init). Developer material stays in the repo, since the repo is public anyway and is not product documentation. Add an operator section when there is more than the install page. This keeps D317's rule of no pages for unbuilt things.

Q4. Where do docs media (screenshots, clips) live? (your git-size worry)

What it is: the pictures in the docs. Per figure the capture makes 2 sizes x 2 colour schemes = 4 stills, and 4 clips if it has one. I did not run the capture, so the sizes are my estimate: a still is roughly 100 to 300 KB as webp and a clip 1 to 3 MB, so one full recapture of about 40 figures is of the order of tens of MB. If every recapture were committed into git, 100 recaptures would add several GB of history that every clone downloads for ever. That is the worry, and it is a real one.
Today: D325 says media lives in a separate media repository ("agreed"), the instance bundles text but not media, and media is fetched on demand. Nothing exists yet; the folder is git-ignored. R2 is already decided for signed binaries at builds.infraternal.com (D223).
Options:

  • A. Separate git repo with plain files (what D325 says literally). Simple to understand. History still grows with every recapture (the problem moves to another repo; clones of that repo get slow, but main stays small). Can be tamed by periodically squashing history, which breaks old hashes.
  • B. Separate git repo with Git LFS (large file storage; git keeps only a pointer, the bytes sit in Forgejo's LFS store). Git stays small; the store still grows, and LFS needs a client everywhere.
  • C. Forgejo generic registry, hash-pinned. The main repo holds only a tiny media.lock (figure name, file name, SHA-256, size). CI uploads the bytes to a registry package; the docs build downloads and verifies them against the hash. Nothing large is in any git history. Old versions are deleted by removing old package versions. Access needs the Forgejo host to be up when you build docs.
  • D. R2, content-addressed. Blobs stored under their own SHA-256 at, say, media.infraternal.com/<hash>.webp, served publicly and cached for ever (the bytes can never change, so they cache perfectly). The same media.lock in the repo pins them. Downloads never depend on the homelab (the reason for D223).
  • E. Container registry (OCI images). Works, but media is not an image of a filesystem; adds tools for no gain.
    Recommendation: C + D together, and say that this is how D325's "separate media repository" is carried out: a media lock file in git (hash-pinned), the bytes in the Forgejo generic registry as the source of record, and the docs deploy publishes the hash-named blobs to R2 for visitors and for instances. That keeps git small, builds reproducible and verifiable, and public serving off the homelab. If you prefer the literal reading of D325 (a separate git repo), choose A with LFS (B); I would then still keep the hash lock in the main repo. This is a choice of storage and delivery, not of a foundational library, but it extends D223's R2 use to a second bucket, so I ask.
    Sub-questions:
    1. Privacy: an instance that shows a figure fetches it from infraternal.com's media domain, so that domain sees the instance's visitor IP. Options: allow it by default; allow it only after a one-line setting; or let an instance mirror media (an offline instance shows the caption box, which already works). I recommend: fetch on demand by default with clear text in the docs, a setting to switch it off, and the caption box when off or offline.
    2. Clips and phone/desktop/ink/paper variants: keep all four, or store only the ink pair and let paper be re-captured when needed? I recommend keep all four; the capture is automatic and the page picks by size and scheme.

Q5. How do screenshots stay current with the UI?

What it is: a screenshot goes stale the day the UI changes and nobody notices.
Today: the capture is already automatic and tied to the real UI (docs:figures drives a production build with a demo dataset). Nothing runs it, and nothing says that a figure is out of date.
Options:

  • A. Run by hand when someone remembers. Will rot.
  • B. CI recaptures all figures on every merge to dev, uploads only changed blobs (the hash stays equal when a figure did not change, so no churn), and the deploy uses the new lock file.
  • C. Recapture only at release time. Cheaper, but dev docs lie between releases.
  • D. B plus a staleness gate: each figure records the commit it was captured from and a fingerprint of the screen it shows; a gate fails if the fingerprint changed without a recapture.
    Recommendation: B for capture, with the owner seeing a contact sheet (all new figures on one page) as a screenshot review on the production build before they ship, per CLAUDE.md ("Codex jobs attach the screenshots; they do not judge their own quality"). Add D's gate only if B proves flaky. Failure mode to decide: if a capture fails, the old picture stays and the deploy shows "Figure pending" rather than a wrong picture (my recommendation: keep the old one and fail the job loudly).

Q6. Are the docs detached from the app? (open item in D317)

What it is: "detached" = the docs can change and be published without releasing a new app, and the app reads the newest docs.
Today: attached. The docs text is part of the build of the app, so the version a person runs and the docs they read always agree (D298), and ? links never break. D325 already leans: bundle the text ("text is cheap enough to include", 33 KB now), pull media on demand.
Options:

  • A. Attached for text, media on demand (D325). Docs fixes ship with the next app build; the public site at infraternal.com is always the newest.
  • B. Fully detached: the app opens the public docs. Smaller binary, newest text, but it needs the internet, can describe a feature the instance does not have, and a ? link can point at a heading that is gone.
  • C. Hybrid: A, plus the Docs view shows a quiet row "Newer docs available" that opens the public page. Never replaces the local text.
    Recommendation: A, and close the item in D317. Your own words in D325 already point there, and a self-hosted server must work offline. C can be added later with no change to what is shipped. Binary size effect is small (text is tens of KB; I will report the measured delta if you choose it).

Q7. Versioning of the docs

What it is: which version's docs a reader sees.
Today: D325: commit ids until there are releases. Each instance serves the docs of the version it runs. The public site has not been built, so there is no policy for old versions.
Options:

  • A. Public site shows only the newest (/docs); each instance shows its own; the footer prints the short commit id. No archive.
  • B. Public site keeps one frozen copy per commit: /docs/<commit>/. Unbounded growth (and Pages has a per-deploy file count limit), nobody navigates by commit.
  • C. A (now); at the first release add /docs/latest and /docs/v<N>/ for the current release lines, and keep only supported ones.
    Recommendation: A now, planning for C at the first release. Since each instance serves its own version, a reader on an older server already gets matching docs, so a public archive is only for people with no server; they need the latest. Plus a rule: a page records since and changed (release or commit) only when releases exist, never earlier.
    Also decide: should deep links from the app (?) use the instance's own address (today, D298) or the public one when the instance is offline from the internet? Recommendation: the instance's own, never the public one.

What it is: finding a page or setting.
Today: inside the app, ⌘K already finds a section, page or heading by title (not body text), forgiving typos and word order (TASTE rule). The public pages have no search box; the home page says "search from the app".
Options:

  • A. Leave titles and headings only (today). Fine for 12 pages and one heading per setting.
  • B. On the standalone docs pages (instance and public) add the same ⌘K capsule, reusing the matcher and index.json. No dependency, no third party (D46), same behaviour as the app.
  • C. B plus body text: add each page's words to the index so "passkey" finds a page that only mentions it. A small index of 33 KB of text is tens of KB; later grows.
  • D. A hosted search service. Rejected up front: third-party tracker (D46) and an external dependency.
    Recommendation: B now, and C when the docs pass roughly 50 pages or when body-only matches are missed in real use (measure the index size and the first-keystroke latency: TASTE requires results instantly, and the index loads on demand, not at page load). Show the rank of exact and prefix matches first, as the app does.

Q9. How should it look, measured against TASTE?

What it is: docs/design/TASTE.md is the owner's list of rules. The ones that touch a docs site are: alignment to 0.5 px, an icon and its text share a centre line, nothing modal that does not need to be, every tap answers, text never collides at 375 px, search forgives typos and is instant, scroll bars appear only while scrolling, the wheel scrolls what is under it, descriptions say what a thing does, not its state (defaults live in the docs, which is us), and standalone docs keep the browser's zoom (D159). The docs today use their own two small style sheets with ink and paper following the system.
Options:

  • A. Docs-only styling (today) that uses the same colour tokens and font as the app, with a plain header.
  • B. A: and the site chrome is the app's own capsule top bar (title left, ⌘K search, theme follows system), so the public site feels like the product.
  • C. A separate marketing look. Costs a second design to keep in step with the app, and TASTE already refuses "a second door to the same room".
    Recommendation: B. One look, shared tokens (D47: until @kayg/ui exists, vendor them with a sync script), reused components from packages/ui rather than new ones (CLAUDE.md). Before it goes public: screenshots of a production build at 1440x900, 393x852 and 375x667, ink and paper, including the "Figure pending", offline and 404 states, checked row by row against TASTE and attached to the issue for you to look at. Any annoyance you spot goes into TASTE the same day.

Q10. Deploy readiness and ownership of the domain

What it is: getting infraternal.com live. D325 agreed: built by us, deployed to Cloudflare Pages with Wrangler.
Today: infraternal.com did not resolve from here; builds.infraternal.com does. Issue #3 is open. A deploy needs a Cloudflare token scoped to Pages, and one for the media bucket if Q4 picks R2 (D223: the owner provides it; it never enters the repo, logs or argv).
Questions for you: (1) Is infraternal.com's DNS pointed at Pages yet, or should the first deploy use the default *.pages.dev name for review before the domain is switched? (2) May the docs deploy job, not only you, deploy the site, with a token that can only deploy Pages and write one bucket? Recommendation: first deploy to a *.pages.dev preview you review, then switch the domain; a scoped token held in Forgejo Actions secrets for that job only, as D223 already sets for the pipeline.
Also: the docs' own licence. Code is AGPL-3.0-only; prose is not code. Options: the same AGPL-3.0-only (simplest; matches CLAUDE.md "every crate and package states it"), or a documentation licence such as CC BY-SA 4.0. Recommendation: same AGPL-3.0-only until you decide otherwise, stated in the page footer.

Q11. Order of work

Proposed slices if the answers above are accepted: (1) record Q2, Q4, Q6, Q7 in DECISIONS; (2) apps/website: /releases, /changelog, front page, built by the same generator; (3) media lock + capture in CI + first six real figures (Get started, Terminal, Machines); (4) docs search on standalone pages; (5) first deploy to *.pages.dev, review screenshots, switch the domain. Which of these should go first? Recommendation: (1), (3), (2), (5), (4).


Reply per question with the option letter (or your own words); I will record each answer verbatim in DECISIONS as a new numbered decision, as D317 did. No code was changed by this job and the gate does not apply.

Job J184 (research only, no code changed). The owner deferred this grill (D325: "needs its own grill"; DECISIONS end: "The website and docs ... are grilled later"; D317 left two items open). Each question says what the thing is, what happens today, the options, and a recommendation. Quotes are the owner's own words from DECISIONS. Base read: origin/dev, plus the live site https://ssh.kayg.org fetched on 2026-11-04. ## What exists today (facts, so the questions make sense) - **Public docs today = `https://ssh.kayg.org/docs/`**, which is the owner's production instance. No sign-in is needed for `/docs`. Nine sections, 12 markdown files, about 33 KB of text in total: Get started, Terminal (Overview + Multiplexer), Machines, Mesh, Access, You, Sharing, Appearance, Reference (Overview + Keys). Most sections are still only an overview page. The old flat address `/docs/multiplexer/` redirects to `/docs/terminal/multiplexer/` (D317). - **How it is built:** `apps/tracer/docs-build.ts` renders `docs/user/**/*.md` to plain static HTML (no framework, no new dependency). The same output is (a) put in the server's web root, so every instance serves the docs of its own version (D315, #298), and (b) read inside the app as the "Docs" view on the canvas, found through the search palette (⌘K) from `index.json` (titles and headings only, 6.7 KB). The pages load only a small `doc.css` and a deferred `doc.js`. The build fails on a skipped heading level, a missing summary, or a broken link or "?" anchor. - **Figures (screenshots and clips):** `bun run docs:figures` drives a production build of the app with a demo dataset and writes `docs/user/figures/<name>.<desktop|phone>.<ink|paper>.webp` (plus `.webm` clips). That folder is **git-ignored**: "where the media of the docs lives is not decided, so no picture is in the repository" (`docs-figure.ts`). A page without pictures still builds and shows the caption under a "Figure pending" box. No page uses a figure yet. - **Public website `infraternal.com`:** from the machine I ran on, `infraternal.com` does not resolve at all; `builds.infraternal.com` answers a Cloudflare "Not Found" page (the domain exists, nothing is served at its root). `apps/website` does not exist on dev. Issue #3 (S0 Forgejo Actions to Cloudflare Pages) is still open. So no public marketing or docs site exists yet; ssh.kayg.org/docs is the only public docs. - **Decisions that apply:** D44 (static SvelteKit site, docs in Astro Starlight, Cloudflare Pages + Wrangler), D45 (hook is SSH, claim only what works), D46 (no third-party trackers, no numbers on pricing; landing/roadmap/waitlist), D47 (shared UI), D220 (the site is exactly /releases, /docs, /changelog; landing, roadmap and waitlist dropped), D223 (Pages for the site, R2 bucket `builds.infraternal.com` for signed binaries), D298/D315 (each instance serves its own version's docs; the official ones are the same build at infraternal.com/docs), D317 (structure, addresses, one heading per setting, no page for unbuilt features), D325 (one site with docs under /docs, "agreed but needs its own grill"; versions are commit ids until releases; media in a separate media repository, "agreed"; the app bundles docs text but not media: "Media, I don't think so. They need to be pulled on demand."), D159 (standalone docs pages keep the browser's zoom). - **Conflict to record:** D44 says the docs are Astro Starlight in `apps/docs`. What is actually built is our own small renderer inside `apps/tracer`. This should be recorded as a decision (Q2). Terms used below: **Cloudflare Pages** = static file hosting with deploys from a command; per-file size limit is small, which is why binaries go to R2. **R2** = Cloudflare's object store (like S3), public through a custom domain. **Forgejo registry** = the package store built into git.kayg.org (a "generic" package is any file set under a name and version; a "container" package is an OCI image). **Hash-pinned** = a file is named or checked by its SHA-256, so the exact bytes are fixed and tampering is detected. **Content-addressed** = stored under its own hash, so the same bytes are stored once and never change. **Figure** = a docs screenshot or clip. **Instance** = one running server, for example ssh.kayg.org. --- ## Q1. What is the public website for, and what are its pages? **What it is:** infraternal.com is the public face: where a stranger learns what this is and gets the software; the docs are one part of it. **Today:** nothing public is served at infraternal.com (see above). D220 says exactly three pages: /releases, /docs, /changelog. D45 says the hook is SSH ("a self-hosted terminal that feels local, on every device"). D220 dropped the landing page, so `/` has no decided content. **Options:** - A. Strictly the three pages; `/` redirects to `/docs` (or to /releases). Least to build and nothing to over-claim. A first-time visitor has no pitch. - B. Three pages plus one calm front page at `/`: one sentence (D45), one screenshot (from the same figure pipeline, Q4), the install command and links to the three pages. No roadmap, no waitlist, no pricing. - C. Restore a full landing page, roadmap and waitlist (reverses D220). **Recommendation:** B. It keeps D220's "claim only what works" and costs one small page, and the install command and a real screenshot are the only things a visitor needs. Pricing and waitlist stay out (D46, no numbers in the repo). **Also decide:** does /changelog come from git history (generated from commit subjects with decision numbers), or is it written by hand per release? I recommend generated from tagged releases, with hand-written highlights only when there are releases. ## Q2. One site, one generator: record that the docs are our own renderer, not Starlight **What it is:** the tool that turns the docs source into web pages. D44 chose Astro Starlight (a ready-made docs framework). We built our own small renderer instead, so the same pages work inside the app, on every instance and on the public site with no framework. **Today:** `docs-build.ts` is shipped on dev and tested. `apps/docs` and `apps/website` do not exist. **Options:** - A. Keep our renderer for the docs, and build the rest of the site (releases, changelog, front page) as plain static pages from the same code. One generator, no framework, instance docs and public docs identical by construction. - B. Public site in SvelteKit (D44) and docs by our renderer, the site links into `/docs`. Two build systems and two looks. - C. Move everything to Starlight. A second renderer for the in-app Docs view, and the instance copy would drift from the public copy. **Recommendation:** A, and amend D44 accordingly. The docs reader is part of the app (palette, canvas window, anchors checked at build time); a framework cannot replace that, and two generators means two places for a style bug. SvelteKit stays an option only if the site later grows interactive parts. This needs your word because it changes D44, and only you can change a decision. ## Q3. Who are the docs for, and how are they laid out? **What it is:** the audience decides tone and what we write. **Today:** D317: written "for the people who use it" (the home page says so), sections follow Settings order, nested addresses `/docs/<section>/<page>/#<heading>` (two levels at most), one heading per setting with a quick-facts block (the only place a default and range are written), no page for unbuilt features (only a "Planned" callout). Not covered by D317: contributors, operators who install and run `server init`, and API/MCP/CLI users. **Options:** - A. One audience: people using the app. Keep D317 as is. Operator, CLI, API and MCP pages are added later as sections of the same tree when those exist (D229 already says `server init` is documented on the website). - B. Two docs: user docs (/docs) and developer docs (/dev, from `docs/ARCHITECTURE.md`, protocol, contributing). - C. Three audiences from the start: users, operators (install, update, backup, mesh, reverse proxy) and developers. **Recommendation:** A now, with "Install and run a server" added to Get started (the first thing a stranger needs, and it does exist: `server init`). Developer material stays in the repo, since the repo is public anyway and is not product documentation. Add an operator section when there is more than the install page. This keeps D317's rule of no pages for unbuilt things. ## Q4. Where do docs media (screenshots, clips) live? (your git-size worry) **What it is:** the pictures in the docs. Per figure the capture makes 2 sizes x 2 colour schemes = 4 stills, and 4 clips if it has one. I did not run the capture, so the sizes are my estimate: a still is roughly 100 to 300 KB as webp and a clip 1 to 3 MB, so one full recapture of about 40 figures is of the order of tens of MB. If every recapture were committed into git, 100 recaptures would add several GB of history that every clone downloads for ever. That is the worry, and it is a real one. **Today:** D325 says media lives in a separate media repository ("agreed"), the instance bundles text but not media, and media is fetched on demand. Nothing exists yet; the folder is git-ignored. R2 is already decided for signed binaries at `builds.infraternal.com` (D223). **Options:** - A. Separate git repo with plain files (what D325 says literally). Simple to understand. History still grows with every recapture (the problem moves to another repo; clones of that repo get slow, but main stays small). Can be tamed by periodically squashing history, which breaks old hashes. - B. Separate git repo with Git LFS (large file storage; git keeps only a pointer, the bytes sit in Forgejo's LFS store). Git stays small; the store still grows, and LFS needs a client everywhere. - C. **Forgejo generic registry, hash-pinned.** The main repo holds only a tiny `media.lock` (figure name, file name, SHA-256, size). CI uploads the bytes to a registry package; the docs build downloads and verifies them against the hash. Nothing large is in any git history. Old versions are deleted by removing old package versions. Access needs the Forgejo host to be up when you build docs. - D. **R2, content-addressed.** Blobs stored under their own SHA-256 at, say, `media.infraternal.com/<hash>.webp`, served publicly and cached for ever (the bytes can never change, so they cache perfectly). The same `media.lock` in the repo pins them. Downloads never depend on the homelab (the reason for D223). - E. Container registry (OCI images). Works, but media is not an image of a filesystem; adds tools for no gain. **Recommendation:** C + D together, and say that this is how D325's "separate media repository" is carried out: a media lock file in git (hash-pinned), the bytes in the Forgejo generic registry as the source of record, and the docs deploy publishes the hash-named blobs to R2 for visitors and for instances. That keeps git small, builds reproducible and verifiable, and public serving off the homelab. If you prefer the literal reading of D325 (a separate git repo), choose A with LFS (B); I would then still keep the hash lock in the main repo. This is a choice of storage and delivery, not of a foundational library, but it extends D223's R2 use to a second bucket, so I ask. **Sub-questions:** 1. Privacy: an instance that shows a figure fetches it from infraternal.com's media domain, so that domain sees the instance's visitor IP. Options: allow it by default; allow it only after a one-line setting; or let an instance mirror media (an offline instance shows the caption box, which already works). I recommend: fetch on demand by default with clear text in the docs, a setting to switch it off, and the caption box when off or offline. 2. Clips and phone/desktop/ink/paper variants: keep all four, or store only the ink pair and let paper be re-captured when needed? I recommend keep all four; the capture is automatic and the page picks by size and scheme. ## Q5. How do screenshots stay current with the UI? **What it is:** a screenshot goes stale the day the UI changes and nobody notices. **Today:** the capture is already automatic and tied to the real UI (`docs:figures` drives a production build with a demo dataset). Nothing runs it, and nothing says that a figure is out of date. **Options:** - A. Run by hand when someone remembers. Will rot. - B. CI recaptures all figures on every merge to dev, uploads only changed blobs (the hash stays equal when a figure did not change, so no churn), and the deploy uses the new lock file. - C. Recapture only at release time. Cheaper, but dev docs lie between releases. - D. B plus a **staleness gate**: each figure records the commit it was captured from and a fingerprint of the screen it shows; a gate fails if the fingerprint changed without a recapture. **Recommendation:** B for capture, with the owner seeing a contact sheet (all new figures on one page) as a screenshot review on the production build before they ship, per CLAUDE.md ("Codex jobs attach the screenshots; they do not judge their own quality"). Add D's gate only if B proves flaky. Failure mode to decide: if a capture fails, the old picture stays and the deploy shows "Figure pending" rather than a wrong picture (my recommendation: keep the old one and fail the job loudly). ## Q6. Are the docs detached from the app? (open item in D317) **What it is:** "detached" = the docs can change and be published without releasing a new app, and the app reads the newest docs. **Today:** attached. The docs text is part of the build of the app, so the version a person runs and the docs they read always agree (D298), and `?` links never break. D325 already leans: bundle the text ("text is cheap enough to include", 33 KB now), pull media on demand. **Options:** - A. Attached for text, media on demand (D325). Docs fixes ship with the next app build; the public site at infraternal.com is always the newest. - B. Fully detached: the app opens the public docs. Smaller binary, newest text, but it needs the internet, can describe a feature the instance does not have, and a `?` link can point at a heading that is gone. - C. Hybrid: A, plus the Docs view shows a quiet row "Newer docs available" that opens the public page. Never replaces the local text. **Recommendation:** A, and close the item in D317. Your own words in D325 already point there, and a self-hosted server must work offline. C can be added later with no change to what is shipped. Binary size effect is small (text is tens of KB; I will report the measured delta if you choose it). ## Q7. Versioning of the docs **What it is:** which version's docs a reader sees. **Today:** D325: commit ids until there are releases. Each instance serves the docs of the version it runs. The public site has not been built, so there is no policy for old versions. **Options:** - A. Public site shows only the newest (`/docs`); each instance shows its own; the footer prints the short commit id. No archive. - B. Public site keeps one frozen copy per commit: `/docs/<commit>/`. Unbounded growth (and Pages has a per-deploy file count limit), nobody navigates by commit. - C. A (now); at the first release add `/docs/latest` and `/docs/v<N>/` for the current release lines, and keep only supported ones. **Recommendation:** A now, planning for C at the first release. Since each instance serves its own version, a reader on an older server already gets matching docs, so a public archive is only for people with no server; they need the latest. Plus a rule: a page records `since` and `changed` (release or commit) only when releases exist, never earlier. **Also decide:** should deep links from the app (`?`) use the instance's own address (today, D298) or the public one when the instance is offline from the internet? Recommendation: the instance's own, never the public one. ## Q8. Search **What it is:** finding a page or setting. **Today:** inside the app, ⌘K already finds a section, page or heading by title (not body text), forgiving typos and word order (TASTE rule). The public pages have no search box; the home page says "search from the app". **Options:** - A. Leave titles and headings only (today). Fine for 12 pages and one heading per setting. - B. On the standalone docs pages (instance and public) add the same ⌘K capsule, reusing the matcher and `index.json`. No dependency, no third party (D46), same behaviour as the app. - C. B plus body text: add each page's words to the index so "passkey" finds a page that only mentions it. A small index of 33 KB of text is tens of KB; later grows. - D. A hosted search service. Rejected up front: third-party tracker (D46) and an external dependency. **Recommendation:** B now, and C when the docs pass roughly 50 pages or when body-only matches are missed in real use (measure the index size and the first-keystroke latency: TASTE requires results instantly, and the index loads on demand, not at page load). Show the rank of exact and prefix matches first, as the app does. ## Q9. How should it look, measured against TASTE? **What it is:** `docs/design/TASTE.md` is the owner's list of rules. The ones that touch a docs site are: alignment to 0.5 px, an icon and its text share a centre line, nothing modal that does not need to be, every tap answers, text never collides at 375 px, search forgives typos and is instant, scroll bars appear only while scrolling, the wheel scrolls what is under it, descriptions say what a thing does, not its state (defaults live in the docs, which is us), and standalone docs keep the browser's zoom (D159). The docs today use their own two small style sheets with ink and paper following the system. **Options:** - A. Docs-only styling (today) that uses the same colour tokens and font as the app, with a plain header. - B. A: and the site chrome is the app's own capsule top bar (title left, ⌘K search, theme follows system), so the public site feels like the product. - C. A separate marketing look. Costs a second design to keep in step with the app, and TASTE already refuses "a second door to the same room". **Recommendation:** B. One look, shared tokens (D47: until `@kayg/ui` exists, vendor them with a sync script), reused components from `packages/ui` rather than new ones (CLAUDE.md). Before it goes public: screenshots of a production build at 1440x900, 393x852 and 375x667, ink and paper, including the "Figure pending", offline and 404 states, checked row by row against TASTE and attached to the issue for you to look at. Any annoyance you spot goes into TASTE the same day. ## Q10. Deploy readiness and ownership of the domain **What it is:** getting infraternal.com live. D325 agreed: built by us, deployed to Cloudflare Pages with Wrangler. **Today:** infraternal.com did not resolve from here; builds.infraternal.com does. Issue #3 is open. A deploy needs a Cloudflare token scoped to Pages, and one for the media bucket if Q4 picks R2 (D223: the owner provides it; it never enters the repo, logs or argv). **Questions for you:** (1) Is infraternal.com's DNS pointed at Pages yet, or should the first deploy use the default `*.pages.dev` name for review before the domain is switched? (2) May the docs deploy job, not only you, deploy the site, with a token that can only deploy Pages and write one bucket? Recommendation: first deploy to a `*.pages.dev` preview you review, then switch the domain; a scoped token held in Forgejo Actions secrets for that job only, as D223 already sets for the pipeline. **Also:** the docs' own licence. Code is AGPL-3.0-only; prose is not code. Options: the same AGPL-3.0-only (simplest; matches CLAUDE.md "every crate and package states it"), or a documentation licence such as CC BY-SA 4.0. Recommendation: same AGPL-3.0-only until you decide otherwise, stated in the page footer. ## Q11. Order of work Proposed slices if the answers above are accepted: (1) record Q2, Q4, Q6, Q7 in DECISIONS; (2) `apps/website`: /releases, /changelog, front page, built by the same generator; (3) media lock + capture in CI + first six real figures (Get started, Terminal, Machines); (4) docs search on standalone pages; (5) first deploy to `*.pages.dev`, review screenshots, switch the domain. Which of these should go first? Recommendation: (1), (3), (2), (5), (4). --- Reply per question with the option letter (or your own words); I will record each answer verbatim in DECISIONS as a new numbered decision, as D317 did. No code was changed by this job and the gate does not apply.
Sign in to join this conversation.
No labels
launch-qa
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
kayg/infraternal#339
No description provided.