3 — The admin guide
The admin (/admin) is the console — the runtime operate path. The
board light at the top reads the estate's worst-of state and the
queue's depth at a glance; the work queue (drafts, composer, live
incidents) carries the doing; the registers rail holds the
administration. The console wears the deployment's theme tokens and
fonts (the whitelabel seam — the flagship instance ships the Ribose
identity).
Its gate is the deployment's OPS_TOKEN secret (unset, the whole
surface answers 404 as if it did not exist); admin: false in the
instance YAML closes it explicitly; admin.session_ttl_hours sets
the session's lifetime (default 8).
Signing in
Two postures, one session machinery either way: a valid login issues an HMAC-signed, HttpOnly, Secure, SameSite=Strict session cookie (backed by a revocable server-side row — signing out deletes it).
The audit register (TODO 74): every operator act lands an
append-only row in the console's Audit card — publishes, updates,
write-ups, deletes, the GDPR subscriber delete, key create/revoke,
member and invite changes, page register/retire, restores, the
registry write surface, and both login postures (a FAILED login is
recorded as anonymous; a success names the identity: the OP's
subject for OIDC, token or ops-token/api-key for the machine
Bearers). Details carry ids and codes, never payloads — the GDPR
delete deliberately records no address. The register is bounded (the
oldest 500 age out) and rides in the backup with a row-shape check.
The operator token (no admin.oidc in the instance YAML):
enter the OPS_TOKEN; the write API's Bearer is the same secret.
The identity service (admin.oidc configured): the login page
offers "Sign in with the identity service". The flow is
authorization-code + PKCE (S256, public client — no secret here),
and the ID token is verified against the provider's JWKS: iss,
aud, exp, and the ES256 signature. Access is decided by your
identity — the token's sub is in the configured subjects list,
or its admins_claim claim is truthy. The provider is reached by
protocol: only the issuer lives in the config; discovery supplies
every endpoint at run time.
Setting the posture up is one operator act at the provider: register
a public client whose redirect URI is exactly
<base_url>/admin/oidc/callback, then fill admin.oidc
(issuer, client_id, and the access rule) in the instance YAML.
On a plane-provisioned page the registration act itself is a
CONTROL_TOKEN call (POST /v1/pages/<slug>/op-client,
docs/manual/06): the YAML arrives with admin.oidc already real,
and the remaining act is naming who administers (the subjects list,
an admins claim, or the org block).
The Bearer stays OPS_TOKEN for machines either way — the write API
never speaks OIDC.
The GET /ops/drafts JSON surface (the token as a Bearer) remains
for scripting.
The Design register
The console customizes the page's identity at run time (TODO 78): the
logo pair uploads to the Design register — SVG, PNG, or JPEG, at most
100 KB — and serves from /brand/ page-scoped, so a mounted page's
URLs stay right. Theme tokens layer OVER the config's declaration;
values are HEX COLORS only (a token is --name: #rrggbb — the
raw-CSS refusal is a security contract, and an uploaded SVG serves
under a sandbox CSP so it can never execute in the site's origin).
Both postures are owner acts, audited like every other (the register
lands design.logo, design.logo-remove, design.theme rows), and
both ride in the backup with row-shape checks. Fonts stay a config
act (self-hosted subsets are a repo change by design).
The probe log
The Probe log register shows what the prober SAW, per leg: the
vantage, the verdict, the latency, and the honest reason — the rings
answer "was it up", this register answers "what exactly happened".
The summary lists every service's newest legs; ?log_service=<id>
focuses one service's last 120. The window is site.probe_log_hours
(default 24; 0 turns the log off), the store hard-caps each service's
row, and the rows ride the backup with a shape check. Missing minutes
stay the coverage column's truth — a gap is named there, never hidden
here.
Drafts → publish
When the automation's verdict turns a service DOWN it opens a DRAFT — evidence folded in, never rendered publicly, never mailed. The dashboard lists the open drafts with the evidence prefilled into a publish form: edit the copy, set the window and scope, and publish. Publishing does NOT stop the automation (a still-failing service re-drafts per its own rules). A draft slot closes on the genuine recovery (every vantage that went down passes again) — or by the human override: Dismiss clears the slot, and a still-failing service simply re-opens it (a review action, never a silencing).
Composing incidents
Title, date, kind (incident or maintenance), the window, the impact line, the affected services (checkboxes from your registry; none = estate-wide), optionally an opening update and the write-up (markdown, rendered at runtime). Runtime incidents live in KV under the same model as the build-time rows — the board, the history, the feeds, and the notification leg all render the MERGED set.
Driving the update trail
Each runtime incident card carries the update form: status
(investigating / identified / monitoring / resolved) + the one-line
body. The card's public phase follows the last update. Delete removes
the runtime row (build-time incidents/*.md rows are the repo's to
remove — the strongest review posture, always available).
Subscribers
The card shows the verified/pending split, each address with its scope (the first 200 listed; the backup document carries the full set), and the delete form. Deleting removes the address's rows entirely; the notification ledger stays (it is the audit trail and carries no address).
Backups and restores
See the backups chapter. The card downloads the backup document and restores an uploaded one; the restore validates every key and merges (same-key rows overwrite, nothing deleted).
The postmortem flow
An incident composed live (updates only) gains its write-up
afterwards: the runtime card's Add or replace the write-up form
(the same act as PUT /api/incidents/<slug>/body). The full arc:
compose live → drive the trail → resolve → add the postmortem.
Templates
If the instance declares incident_templates, the composer offers
one prefill link per template — the declared fields land in the form,
everything still editable before publishing.
The org (when org is declared): members, roles, pages
With an org block in the instance YAML, this deployment serves one
tenant, and the admin becomes role-gated:
- Owners administer: the org card (the members table, the invite form, the pending invites with their revokes), the page registry, backups/restores, and subscriber deletes.
- Operators publish: the open drafts with their publish forms, the composer, the runtime update trail. They see the members list (read-only) — knowing the team is operator-relevant; the forms hide, and the owner-only POSTs answer 403.
The bootstrap floor is config: org.owners lists the identity
service subjects that are ALWAYS owners — KV can never lock them
out, and an owner listed there cannot be removed by hand (the form
refuses; removal is the config edit).
Invites are pending rows, not sent mail: an owner enters an
address and a role; when that person signs in through the identity
service, the ID token's email claim redeems the invite into a
member row and consumes it — but only while the OP asserts
email_verified: true for it (a signed token with an unverified
address never redeems). Tell them to sign in — no mail is sent.
(The OP includes email + email_verified at scope openid email, which the client always requests.)
Removal takes effect on the next request: authority resolves per request from the member rows, never cached in the session — a removed member's still-valid cookie loses its role immediately.
The page registry is the org's directory of status pages: this deployment's own entry (marked "this deployment", maintained by the config) plus any external pages an owner registers (slug, title, URL). It is the data the multi-page control plane will consume.
API keys (machine access with names and revocation)
With an org declared, owners create named API keys on the org card:
the key shows ONCE at creation (only its hash is kept — a lost key
is revoked and re-created, the same posture as the identity
service's client secrets). A key carries a role: an operator key
works as the write API's Bearer (publish level); an owner key also
downloads backups. Revoking the row takes effect on the machine's
NEXT call. Without an org, the keys do not exist — the shared
OPS_TOKEN remains the machine posture (and always the superuser).
Provisioning a new page
The org card's "Provision a new page" drafts a complete NEW
instance from a form: the starter YAML (content-asserted services
from id | name | url | needle lines, the honest one-vantage
starter), the worker manifest (name, cron, the KV binding with its
TODO), and the operator checklist (the KV creation, the secrets,
the OP client registration, the domain attach, the first deploy).
Nothing is written and nothing is deployed — emission is drafting;
registering the page in the registry and deploying it are separate
deliberate acts.
Webhook deliveries
When the instance declares webhook receivers, the dashboard shows each receiver's latest delivery from the once-on-2xx ledger: delivered (when), or abandoned after the retry budget (the failure named in the worker's log).
The rest of the surface
The admin is one room in the house. The public page's cards, uptime
windows, and the per-service latency drill-down are covered in
chapter 2 (docs/manual/02-the-public-page.md, with the embeddable
/badge.svg); the write API — the same composer and update trail as
JSON, for automation — is docs/api.md; the page's chrome words
come from site.locale + site.strings
(docs/config-schema.md); backups and restores are chapter 5.