- 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.
- A derived monitor outage with no `resolved_at` went to `History`, so an
ongoing disruption was listed under "Past incidents" while the component pill
beside it read Down. Unresolved now goes to `ActiveIncidents`.
- `overallState` returned `up` when nothing was counted: "all systems
operational" claimed from no evidence at all. A page with no components now
reports `no_data`, which the view already renders as "Status unknown".
- `EnsureStatusPageIndexes` returned on the first failure, so a transient
failure on the `status_pages` index left `status_incidents` with no unique
`(instance_id, incident_id)` index — a correctness property, not a scan
optimisation. All three are attempted and the failures joined.
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.
Adds owner|admin routes under /api/status-pages for authoring status pages
and their incidents/maintenance windows, gated by the status_pages licence
feature. Adds the "status" token scope resource and the ten route-scope
entries, and regenerates the committed OpenAPI document.
Also types ErrPageInvalid as a sentinel for status page/incident validation
failures (previously bare errors), so statusPageError maps them to 400
instead of 500, and createStatusIncident/updateStatusIncident route through
the shared error mapper rather than hand-rolling a 400 for any service error.
/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.
The page was hand-rolling its loading spinner, error line and empty
paragraph while the rest of the console routes these through
AsyncBoundary with a TableSkeleton and an EmptyState — the same drift
Async.tsx was written to end. It also passed className="p-0" where Card
takes padding={false}.
The admin-only scope switch becomes the vulnerabilities page's pill
filter rather than a loose checkbox: seeing everyone's keys is a filter
over the list, not a preference, and someone who has learned one control
has now learned both.
Scopes collapse to one chip per resource with an r/rw qualifier. Sixteen
badges made the row taller than everything around it and still had to be
read one at a time.
In the create dialog Copy is now the primary action and Done the quiet
one, because the value is unrecoverable once the dialog closes, and the
result panel repeats the role, scopes and expiry that were just granted.