Conormis status

The manual

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:

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.