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

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

**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

opened this

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 MCP maintenance tool do the same. GET /api/v1/admin/maintenance reads ready once 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.ts serves a calm 503 page with Retry-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.md states 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.ts is 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.ended event and the line on /maintenance carry 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

Closes #9453

commented

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 MCP maintenance tool do the same. GET /api/v1/admin/maintenance reads ready once 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.ts serves a calm 503 page with Retry-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.md states 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.ts is 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.ended event and the line on /maintenance carry 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 requested — before the latest push

Changes needed, all in one round:

  1. 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.
  2. 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.
  3. 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.
  4. 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 any types; tidy it.
  5. Dead code. clearState is never called; remove it.
  6. 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.
  7. 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.

commented

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

  1. Terminal door. src/tui/screens/maintenance.ts is a new admin-only palette entry, placed after Resilience and listed in ENTRIES. p asks for a reason, then a confirm, then prepares. c checks again and retakes the backup. e ends maintenance after a confirm. It uses the same service as the page, API and MCP, and docs/features/maintenance.md mentions it. I added the entry to the admin-person roster in test/palette-roster.test.ts.
  2. Body cap. POST /api/v1/admin/maintenance/prepare now reads through boundedBody at 4 KB and answers 413 past that.
  3. 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."

  1. Tests. test/maintenance.test.ts is tidied (imports at the top, no any) 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.

  1. Dead code. clearState is removed.
  2. Roll while prepared. The prepare now records the host's boot id (/proc/sys/kernel/random/boot_id). resumeAtBoot resumes 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/admin prefix 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.ts exists, 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.

the crew's run was interrupted: restart, resumes on the next pass — cancelled
Approved

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.

Approved

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.

commented

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.).

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs