Commit Graph

4 Commits

Author SHA1 Message Date
f499f2b72b docs(journey): phase 7 — the installation path and administration
All checks were successful
PR checks / checks (pull_request) Successful in 9m25s
Twenty documentation pages: Getting started (7) and Administration (13), the
journey no existing document owns end to end because the repositories are
organised by component and an operator is not.

Four decisions of record, taken before anything was written (D34–D37, PLAN.md
§10 "How phase 7 built the documentation journey"):

- D34 one PR for all twenty pages.
- D35 the install page is SELF-CONTAINED: it prints a complete Compose file and
  a complete .env that an operator copies without visiting another repository.
  That is a copy of somebody else's file, so it is checked rather than trusted —
  scripts/checkQuickstart.mjs re-reads website main:docker-compose.yml and
  main:.env.example over the Gitea API and fails on any disagreement, in both
  directions: a value that drifts fails, and a service or variable that appears
  upstream fails until it is either included or recorded as deliberately omitted
  with a reason. Its first run found two stale entries.
- D36 every Administration screen was walked on a real deployment before it was
  described — the rig being the quickstart itself, against the published image,
  so one run proved the install page and produced the detail the admin pages
  needed.
- D37 a thirteenth Administration page, Content, so that every admin nav row has
  a home without organising the docs by the app's menu.

What the live deployment disproved, all three now documented:

- The documented Compose deploy does not boot. SECRET_ENC_KEY is required in
  production (utils/secretBox.js throws at require time) and is missing from
  website's ROOT .env.example — the file Compose reads. It is present in
  server/.env.example, which is why dev never hits it. The quickstart carries it,
  declared as an upstream omission so the check fails the day it is fixed.
- The installer points operators at a screen that no longer exists: it prints
  <site>/admin/shard, and INSTALL.md §5 repeats it, but since the module cutover
  the screen is /admin/uo/link. Both the binary and the guide are stale.
- The admin Restart button opens a window.confirm whose text is the honest
  warning that a deployment with no supervisor does not come back — which is why
  `restart: unless-stopped` is called out as load-bearing rather than left as
  boilerplate.

And the defect only a look found, three phases running: the .env block's prose
promised that every highlighted line must be changed, while `mark` given the
variable names highlighted the names alone and left the values unmarked. Every
check passed on a page that was wrong about its own highlighting.

verify green: 890 internal links, 52 branch links, 19 facts, 59 quickstart
checks, 0 astro-check errors.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 08:35:03 -05:00
1313e748ae feat(beta): phase 5 — the app page and the closed-beta signup
All checks were successful
PR checks / checks (pull_request) Successful in 1m5s
Builds `/app/` and `/beta/`, the SQLite signup store, the rate limiting and the
export CLI of PLAN.md §8, and adds this repository's first test suite.

Four decisions of record, D26–D29 (§8, "How phase 5 built the app and the beta"):

- D26 — the screenshot slot ships empty, reserved for phase 9. §10 promised
  `/app/` "the 14 existing screenshots"; they are a July trusted-device smoke
  test against an unseeded dev instance, captured before the theming work, and
  five of the fourteen are two-factor prompts. Shipping them would break D4.
  Phase 9 already builds the rig, so it gains an emulator pass.
- D27 — the public demo is the tester target. `ConnectScreen.kt` gates the whole
  app on a validated deployment address, so a tester needs somewhere to point it.
  The beta therefore waits on the demo VM, and the page says so.
- D28 — `/beta` handles its own POST; there is no `/api/beta-signup`. An endpoint
  cannot report a validation error without JavaScript. §6's diagram is amended.
- D29 — the APK and the beta get equal billing, and the APK link is off:
  `androidApk.serviceable` is false because the published v0.5.0 build does not
  work. The panel stays and states that plainly rather than being removed.

Three mechanisms the plan did not anticipate:

- `liveBrand()` — a server-rendered page never passes through the boot rewrite,
  so `/beta` reads the mounted brand.json itself. Pasting the Play opt-in URL in
  takes effect on the next request rather than the next restart.
- `checkLinks.mjs` derives on-demand routes from `prerender = false` in the
  source. A PLANNED_ROUTES entry would have been wrong: its reverse check fires
  when a route has been built, and an on-demand route never produces a file, so
  the entry could never rot out.
- `npm test` — the five existing checks all read built output, and none of this
  logic appears there. A honeypot can stop working and leave the build identical.

Also: `checkFacts.mjs` gains the APK assets and `minSdk`, and learns that RFC 2606
reserved domains are not contact addresses; the D13 rule is otherwise unchanged.

Verified end to end against the built server: every outcome renders with no
JavaScript, cross-origin POSTs are refused, a mounted opt-in URL appears without
a restart, and the export CLI round-trips.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 03:50:51 -05:00
fe4abe0ebf feat(brand): phase 2 — the branding pipeline
All checks were successful
PR checks / checks (pull_request) Successful in 9m9s
PLAN.md §7: swapping a logo or recolouring the site is a file copy and a
container restart, never a rebuild. Phase 2 builds the mechanism and the
checks that keep it true.

GET /brand/* resolves every file against the mount first and the baked-in
defaults second, per file, at stable unhashed URLs with an ETag and a five
minute TTL. Nothing goes through Vite, which would fingerprint the names out
of the mount's reach. An X-Brand-Source header says which step answered.

Three decisions were taken with the org lead (recorded as D14-D16 in §7):

D14 — one raster in, every size out. brand-default holds a single logo.png;
the header mark at three pixel ratios, both install icons, the apple-touch
icon, the favicons and a real multi-resolution favicon.ico are derived on
request from whichever logo.png is in force, cached, and limited to an
allowlist of sizes. Shipping fifteen precomputed files would have meant an
operator producing fifteen to change a mark — and getting a new header with
the old favicon.

D15 — brand text is applied at boot. Pages are prerendered, so §7's promise
about the site name, tagline and links could not hold at render time.
npm start now runs scripts/applyBrand.mjs first, rewriting the built HTML
from what it last applied to what the mount says. It rewrites from a record
in dist/.brand-applied.json rather than from the defaults, because the naive
version works exactly once and then silently ignores every later edit. An
empty mount is a no-op; removing a mount restores the stock build byte for
byte. Verified both ways, plus a second rename.

D16 — the header shows the real emblem, replacing phase 1's placeholder
glyph, so the site, the product and the Android launcher icon are one mark.
It is raster art, so theme.css cannot recolour it; replacing logo.png is how
the mark changes.

Two defects found and fixed while proving it:

The mounted theme.css did not win. Astro emits its own stylesheet after the
head markup, so linking the operator's last was not enough and every override
was silently a no-op. tokens.css now lives in @layer tokens and the mounted
file is unlayered, which takes order out of the mechanism entirely.

The documentation was a different site. Starlight builds its own head, so the
docs linked a Starlight default /favicon.svg that does not exist here, carried
no manifest or OG card, and never loaded the brand stylesheet — a mounted
theme recoloured the marketing pages and left the docs stock. A Head override
fixes it; half a rebrand looks like a product bug rather than a missed step.

brand-default/wordmark.svg and og-image.png are generated by
scripts/buildBrandAssets.mjs from the emblem and Cinzel's outlines and are
committed, so CI needs neither the artwork nor a font. Type is converted to
paths, because an SVG in an <img> can see neither the page's @font-face rules
nor fontconfig — the same isolation that broke currentColor in phase 1. Its
glyphs are drawn at the origin and translated: opentype.js emits NaN
coordinates at a non-zero origin for some glyphs, and a path parser stops at
the first malformed command, so the first lockup read "Runic Gate" and looked
like a typo rather than a bug.

scripts/checkBrand.mjs is the mechanism for the two failures that are
otherwise silent: it puts every literal /brand/... URL in the source through
the route's own classifier, so a size that is not on the allowlist fails the
build instead of 404ing in a browser, and it rejects a brand string short
enough that a blind replacement at boot could corrupt a page. Negative-tested
three ways before being trusted. It runs in CI ahead of the type check.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 23:14:17 -05:00
66187dde5d feat(site): phase 1 — the foundation
Some checks failed
PR checks / checks (pull_request) Failing after 4m19s
Astro 7 with the Node adapter, Starlight mounted at /docs, the token file, both
self-hosted typefaces, the layout shell, and the two build-time checks from §12.

The palette's gold and cyan are sampled from runic-emblem.png rather than
guessed, per §11: 494,059 opaque pixels binned by hue, each value annotated with
its measured contrast against the ground, and restricted rather than brightened
where a ratio fails.

- checkTokens.mjs fails the build on any colour literal outside tokens.css,
  which is what keeps §7's "recolouring is a file copy" promise true.
- checkFacts.mjs re-reads all 14 externally-sourced facts from their authorities
  over the Gitea API and fails on disagreement. It also enforces D13: no email
  address in the source outside brand-default/brand.json.
- Both were negative-tested; neither has ever been allowed to pass by default.

§6 asks for output:'server' with per-page prerender=true. Astro 7 expresses the
same runtime shape as output:'static' with an adapter, opting individual routes
out — so the default is static rather than accidentally server-rendered.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 19:08:52 -05:00