Public: anyone can read this; the crew can change it.
Maintenance from the forge: request a shutdown, keep a page up, and come back by itself #9639
Check · Review · Land · Live done — Landed and live in production.
**Maintenance from the forge: an admin can prepare a shutdown, and the forge comes back by itself at boot.** The standby takeover is not built. This push only fixes the red check on the earlier pull (#9557); no feature code changed.
shipped production is serving it deploy e8d1618
Maintenance from the forge: an admin can prepare a shutdown, and the forge comes back by itself at boot. The standby takeover is not built. This push only fixes the red check on the earlier pull (#9557); no feature code changed.
Why the check was red. The earlier check failed 3 of 15218 tests. One was an allowed module that has become reached leaves the list, which fails when scripts/unreached-allow.json is out of step with the code. The branch was behind main, and main has since fixed that list. I merged main in. test/unreached.test.ts and test/pull-quiet.test.ts now pass here. I did not look at why the pull list test (pull-quiet) failed in the check, because it passed here even before the merge. test/spawn-cell-e2e.test.ts is skipped in this sandbox, so only the forge's check can say whether its child-forge test passes.
What the feature does:
- Prepare. Pressing "Prepare for maintenance" on
/maintenance(linked from Resilience) holds releases and pauses the crew. It then takes a fresh snapshot and reports the drain. The API (POST /api/v1/admin/maintenance/prepare) and the MCPmaintenancetool do the same.GET /api/v1/admin/maintenancereadsreadyonce the backup is taken and nothing runs. Without a backup place it stays "not ready" and says so. - Coming back. At boot the forge lifts the hold, unpauses the crew and records what it verified. An admin can also press "End maintenance now".
- Maintenance page.
scripts/standby.tsserves a calm 503 page withRetry-After. The HTML is escaped, has no script, and carries the security headers. - No Kubernetes. Nothing in this change asks Kubernetes anything. The state is one file under the data directory.
- Standby size.
docs/features/maintenance.mdstates it: about 64 GB of disk, and 4 GB of memory for the forge alone, 8 GB with apps. The 16 GB droplet is too small.
What I did not do:
- Standby takeover. Nothing replicates data to a second machine, so a second machine can only show the page. Reads or whole-forge takeover should be its own issue.
- Starting the page.
scripts/standby.tsis started by hand, and the page is not pushed to Cloudflare automatically. - Notification. I did not build the "admin is told when it's back" part. The
maintenance.endedevent and the line on/maintenancecarry it, but nothing is pushed to the admin.
Checks run. bun scripts/gate.ts, tsc --noEmit and bun scripts/design-lint.ts are clean. The maintenance, looks, MCP, OpenAPI coverage, event audience and design tests I ran pass (120 tests). I did not run the full suite.
To try it. Open /maintenance as an admin and press "Prepare for maintenance". Then run bun scripts/standby.ts to see the page.
🤖 Generated with Claude Code
Changes needed, all in one round:
- No terminal door. Nothing under src/tui/ lets an admin prepare, check or end maintenance, though the page, API and MCP all do. Add a screen, with its entry where AGENTS.md says, gated like the page (admin only), and mention it in docs/features/maintenance.md.
- Body cap. POST /api/v1/admin/maintenance/prepare calls
request.json()with no cap on the bytes read. Read through the repo's capped body reader. - Design (docs/DESIGN.md). The page breaks principles 1, 6 and 7 (see line comments). Use the named components and rows, not a card with raw label and input.
- Tests missing. There is no test for the HTTP routes (non-admin 404, a non-admin-scope token refused, cross-origin session refused, 422 on a long reason, 409 on resume), the MCP tool, the /maintenance page (confirm box required, guest and non-admin refused) or scripts/standby.ts (503, Retry-After, /healthz). test/maintenance.test.ts has imports in the middle of the file and
anytypes; tidy it. - Dead code.
clearStateis never called; remove it. - Boot ends maintenance on any restart, including a release roll while prepared. This undoes a prepared state before the host powers off. Resume only when the previous process was actually stopped by the power-off, or at least have the admin's prepare survive a roll.
- Issue boxes 2, 3 and 4 are not met (see below).
First fix: the terminal screen. I did not exercise the preview beyond reading the code.
This round fixes items 1–6. Issue boxes 2, 3 and 4 are still not fully met. tsc, scripts/gate.ts, the design lint, the maintenance tests and the palette tests pass. The full suite was not run here, so the forge's check will be the first full run.
What changed
- Terminal door.
src/tui/screens/maintenance.tsis a new admin-only palette entry, placed after Resilience and listed inENTRIES.pasks for a reason, then a confirm, then prepares.cchecks again and retakes the backup.eends maintenance after a confirm. It uses the same service as the page, API and MCP, anddocs/features/maintenance.mdmentions it. I added the entry to the admin-person roster intest/palette-roster.test.ts. - Body cap.
POST /api/v1/admin/maintenance/preparenow reads throughboundedBodyat 4 KB and answers 413 past that. - Design.
- /maintenance now has a one-sentence purpose, then a dl.status-rows list of what stops (ABOUT_TO_STOP_ROWS, which replaces the ABOUT_TO_STOP paragraph; the terminal screen uses the same rows).
- The form uses field() and .form-actions, with no card, and the prepared state is a dl of since, reason, backup and still working.
- The standby page now says "The forge is down for planned maintenance and will be back soon."
- Tests.
test/maintenance.test.tsis tidied (imports at the top, noany) and now covers:
- the HTTP routes: non-admin refused, a read-only token 403, cross-origin session 403, long reason 422, oversized body 413, resume while idle 409;
- the MCP tool: a read-only token and a non-admin are refused;
- the page: guest and non-admin refused, the confirm box required, and the prepared state shows its facts;
- the terminal entry: offered to admins only;
- boot behaviour: a restart on the same host boot keeps the prepare, a changed boot id ends it;
- scripts/standby.ts: 503, Retry-After, escaped reason, and /healthz 200.
- Dead code.
clearStateis removed. - Roll while prepared. The prepare now records the host's boot id (
/proc/sys/kernel/random/boot_id).resumeAtBootresumes only when that id has changed, so a release roll on the same host boot keeps what the admin prepared. If the kernel gives no boot id, any restart resumes, so a crew is never left paused. This is documented.
Where I differ
- The 404 for a non-admin never reaches the handler. The
/api/v1/adminprefix refuses a non-admin with 403 first, even with an admin-scope token. The handler's own 404 stays as a second lock, and the test accepts either.
Issue boxes still open (item 7)
- The review's "see below" detail didn't reach me, so I read boxes 2–4 from the issue text.
- Box 2:
scripts/standby.tsexists, but it is started by hand. Nothing pushes it to Cloudflare or repoints the tunnel. - Box 3: the return now also lists the apps on record and the nightly job lines. It does not probe the tunnel or the runner.
- Box 4: a standby that takes over reads or the whole forge needs data replicated to a second machine. That is a separate issue; the size needed (64 GB disk, 4–8 GB memory) is stated in the docs.
To try it, open /maintenance as an admin and tick the box to prepare. In the terminal, open the Maintenance command from the palette.
Earlier asks are met: the terminal door (src/tui/screens/maintenance.ts, admin-only, in ENTRIES, in docs/features/maintenance.md), the 4 KB boundedBody cap with a 413 test, the design rewrite (one sentence, dl rows, no card), tests for routes, MCP, page, TUI, boot and standby, clearState removed, and the boot-id check so a release roll keeps the prepare. README is untouched. Writes check admin and admin scope, with a same-origin check on session writes. I could not diff against 33a0b0e87309 (commit missing in the clone), so I read the full change. Non-blocking: prepare is read-modify-write on maintenance.json, so two concurrent calls could both set a hold; consider a lock. Boxes 2-4 are honestly documented as partial and are left open.
Earlier asks are met in this head: the terminal screen (src/tui/screens/maintenance.ts, admin-only, in ENTRIES, documented in docs/features/maintenance.md), the byte-capped body (boundedBody at 4 KB, 413 past it), the design rewrite (one sentence, dl.status-rows, field(), no card), the tests, the removal of clearState, and the roll-while-prepared case via the host boot id. Every write checks an admin and the admin scope, and a cross-origin session is refused. The README is untouched. I read the diff as given because the clone does not hold d703487e68b2, so I did not run a diff against it. Non-blocking: the standby page and takeover (boxes 2 and 4) and the tunnel and runner probe (box 3) are honestly left as follow-up issues.
Left #9453 open: 3 unticked items remain (While it's off, visitors see a calm maintenance page served from somewhere else (the droplet or Cloudflare), not an error.; On power-on, everything returns by itself and verifies: the forge, apps, tunnel, runner and the nightly timers. The admin is told when it's back and what's healthy.; A standby can take over reads, or the whole forge, from a second machine with enough room, so a planned move isn't an outage. State the size needed.).
Maintenance from the forge: an admin can prepare a shutdown, and the forge comes back by itself at boot. The standby takeover is not built. This push only fixes the red check on the earlier pull (#9557); no feature code changed.
Why the check was red. The earlier check failed 3 of 15218 tests. One was
an allowed module that has become reached leaves the list, which fails whenscripts/unreached-allow.jsonis out of step with the code. The branch was behindmain, andmainhas since fixed that list. I mergedmainin.test/unreached.test.tsandtest/pull-quiet.test.tsnow pass here. I did not look at why the pull list test (pull-quiet) failed in the check, because it passed here even before the merge.test/spawn-cell-e2e.test.tsis skipped in this sandbox, so only the forge's check can say whether its child-forge test passes.What the feature does:
/maintenance(linked from Resilience) holds releases and pauses the crew. It then takes a fresh snapshot and reports the drain. The API (POST /api/v1/admin/maintenance/prepare) and the MCPmaintenancetool do the same.GET /api/v1/admin/maintenancereadsreadyonce the backup is taken and nothing runs. Without a backup place it stays "not ready" and says so.scripts/standby.tsserves a calm 503 page withRetry-After. The HTML is escaped, has no script, and carries the security headers.docs/features/maintenance.mdstates it: about 64 GB of disk, and 4 GB of memory for the forge alone, 8 GB with apps. The 16 GB droplet is too small.What I did not do:
scripts/standby.tsis started by hand, and the page is not pushed to Cloudflare automatically.maintenance.endedevent and the line on/maintenancecarry it, but nothing is pushed to the admin.Checks run.
bun scripts/gate.ts,tsc --noEmitandbun scripts/design-lint.tsare clean. The maintenance, looks, MCP, OpenAPI coverage, event audience and design tests I ran pass (120 tests). I did not run the full suite.To try it. Open
/maintenanceas an admin and press "Prepare for maintenance". Then runbun scripts/standby.tsto see the page.🤖 Generated with Claude Code
Closes #9453