Adds the global re-authentication modal for guarded routes and the
owner/admin MFA controls on the settings page.
request() in lib/api.ts now intercepts a 403 step_up_required response,
awaits re-authentication through a callback registered by StepUpModal
(lib/stepup.ts), and retries the original request exactly once. The
modal offers TOTP, recovery code and password, since the webauthn
step-up routes (/me/step-up/webauthn/begin and /finish) are not
registered server-side yet; it omits the passkey option rather than
calling a route that does not exist.
me.stepUp posts one factor to /api/me/step-up. The settings page gains
an owner-only "Require MFA" toggle and the members table gains an MFA
column and a "Reset MFA" action, both routed through the existing
PUT /api/settings and DELETE /api/org/users/:id/mfa.
apt lists phased updates as upgradable while an upgrade defers them until
Ubuntu selects the host, so a freshly patched server kept reporting pending
updates. The agent now flags them; the server stores the flag and leaves them
out of patch run counts, and the server page shows them in their own section.
- no dispatch in the last 15 minutes of a window; no-result timeout from dispatch time
- per-server output moves to patch_run_outputs (16MB document limit)
- reboot proven by a changed boot time; RebootTimeout 45m, ResultGrace 20m
- window update and delete are server-scoped against the policies using them
- scheduler puts the claim back on an error after it, so the next tick retries
- cancelled runs with failures alert; MCP apply_updates audits per server
- apply-updates 503 body documented; openapi regenerated
- web: cleared numeric fields no longer save as 0; Run now asks for confirmation
- The display-name placeholder showed the monitor's own name, reading as "leave
this blank and we will use it". The server deliberately does the opposite: a
blank `display_name` publishes the raw monitor id, because publishing an
internal name has to be a decision. The placeholder now says "Public name
(required)" and Save is refused until every component has one, so nobody adds
five monitors and discovers five UUIDs on their public page. The server
fallback is unchanged.
- `deleteStatusPage` and `deleteStatusIncident` existed in the api client and
were wired to nothing, and the page address is immutable — delete was the
only correction for a typo and there was no way to reach it. The editor
header gains a typed-confirmation Delete page, and each incident row a
confirmed delete, both on the existing ConfirmDialog.
- The create modal's address hint had lost its em dash and read as a broken
sentence.
The SSR fetch set `Host` to the visitor's hostname. `Host` is a forbidden
header name and undici discards it silently, so the Go server saw
`server:8080`, `hostSlug` returned "", `InstanceFromHost` returned false and
every public status page 404'd on every deployment. The feature did not work.
- `web/` now forwards the visitor's host as `X-Forwarded-Host`, and their
address on `X-Forwarded-For` — without the latter gin sees a request from the
Next pod with no XFF and every visitor of every page shares one 120/min
bucket, tripped by exactly the traffic an outage produces.
- `publicStatusInstance` honours `X-Forwarded-Host` only when `c.RemoteIP()` is
in `TRUSTED_PROXIES`. It is a tenant selector, so an untrusted peer must not
be able to name one; `RemoteIP()` rather than `ClientIP()` because the latter
is reconstructed from the very headers being judged. `TrustedProxies()` moves
from main.go into the api package so the variable keeps one parser.
- A host naming no slug on a non-cloud deployment resolves the sole instance,
the way bootstrap does. A self-hosted install at vantage.acme.com or an IP
has no slug and could never serve a status page; more than one instance is a
404 rather than a guess, and an unknown-but-well-formed slug stays a 404.
- `InstanceFromHost` gains an explicit-host variant rather than a second copy
of the slug rules, and now caches negative lookups: an unknown host cost a
Mongo query per anonymous request, which is also a timing oracle separating
"no such instance" from "instance exists, page does not".
- The handler's `@Router` annotation is dropped. openapi.json declares one
server of `/api`, so it published `/api/public/status/{pageId}` — a path that
does not exist. The real address is described in prose instead.