The scratch runtime stage copies an explicit /tmp: restore extracts an
archive there before verifying it, and a scratch image has none.
shared/ now fans out to four Go images rather than three.
--force requires a typed database name on a terminal and --confirm-db
without one, so a copy-pasted restore command carries its intended target
and cannot destroy a different database.
Also silences cobra's own error print (root.go) so a failure is reported
once by main.go instead of twice, and pins the Changed()-based env
fallback in resolveGlobals with a test for an explicitly empty --db.
Progress output goes to stderr so --out - stays a clean pipe into restic,
age or aws s3 cp. Archive names carry no colon, because these get copied
onto Windows shares.
Its own module rather than a package under shared, so cobra and pflag stay
out of the module graphs of server, admin and sitesvc, which never use
them.
A fingerprint comparison proves two archives agree about a key. Only
opening real ciphertext from the target proves the key in hand reads the
data, which is the question an operator actually has.
Every refusal happens before the first write: format, checksums, key
policy, then target inspection. A unique index that will not build aborts,
because the unique indexes here are tenant-isolation properties rather
than optimisations.
Collections are enumerated live rather than from a list, so a collection
added later is backed up with no code change. Documents are written as the
raw BSON the driver returned, so Decimal128, ObjectId, DateTime and binary
subtypes survive byte for byte.
Open extracts and verifies every member against the manifest before the
reader is usable, so a corrupt archive is refused before a restore writes
its first document rather than halfway through.
services/crypto.go keeps its function names and its KEY_ENCRYPTION_KEY
lookup and delegates the cipher, so vantagectl's verify probe can decrypt
with the same implementation rather than a second copy.
Thirteen tasks, each ending in a testable deliverable and a commit.
Also drops --log-level from the spec: the tool's whole output is what it is
telling the operator, and a level that could hide a key warning is worth not
having.
vantagectl/Dockerfile follows the repo's per-module convention rather than
living under deploy/docker.
verify's live check needs AES-GCM open, so the cipher primitives move to
shared/cryptobox and services/crypto.go delegates, rather than a second
copy of the cipher existing in another module.
Standalone vantagectl CLI (cobra, own module) that dumps and restores a
whole Vantage MongoDB database, stamping a sha256 fingerprint of
KEY_ENCRYPTION_KEY into the manifest so a restore cannot silently produce
a database whose secrets are unreadable.
The key itself never enters the archive.
- The status page URL was given only as `<instance>.vantage.<tld>`, which a
self-hosted install does not serve. Both deployments are now described.
- The banner is documented as one notice: the editor exposes no level picker
and the view renders every level identically.
- `pending` added to the component states, which a monitor with no result yet
renders.
- Delete page documented alongside un-publish.
- `TRUSTED_PROXIES` names the LAN case: with the RFC1918 default, a client on a
private range reaching the server directly is itself trusted and can spoof
`X-Forwarded-For` — and now `X-Forwarded-Host`. Narrow it to the proxy.
- CLAUDE.md: scopes are nine resources, not eight; `status-pages` added to the
REST route table; the host-resolution rules recorded under Status pages.
- 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.