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.
/api-keys shares a raw string prefix with /api, and the proxies in front
of this app do not all match by path segment. Nginx Proxy Manager routes
/api straight to the Go server with a prefix location, so /api-keys never
reached Next at all — it reached a control plane with no such route and
came back as a JSON 404. Traefik's PathPrefix has the same shape of
matcher, which puts the Helm ingress at risk whenever ingress.api.enabled
is on.
The page is /tokens now, which cannot collide with anything, and which
matches the /api/tokens the REST API already publishes. The sidebar still
says API Keys — the label is for the reader, the path is for the router.
A permanent redirect covers anyone who bookmarked the old path today.
Fixing the proxy config instead would have left the trap set for the next
deployment, and for whatever sits in front of it.