diff --git a/docs/superpowers/plans/2026-08-24-status-pages-mockup.html b/docs/superpowers/plans/2026-08-24-status-pages-mockup.html new file mode 100644 index 0000000..aeea739 --- /dev/null +++ b/docs/superpowers/plans/2026-08-24-status-pages-mockup.html @@ -0,0 +1,641 @@ +Vantage Status Pages + + +
+ +
+

Vantage · status pages · mockup for approval

+

Two screens: what the public sees, and what the operator edits

+

Drawn with the real web/ dark tokens and the existing sidebar idioms, so what gets approved here is what gets built. The public page is shown mid-incident rather than all-green, because that is the state it exists for.

+
+ + +
+
+

1 · Public status page

+

web/app/status/[pageId]/page.tsx · no auth, no sidebar

+
+ +
+
+ + https://acme.vantage.example.com/status/api + signed out +
+ +
+
+ +
+
AC
+
+

Acme Platform Status

+

Live availability for the Acme API and dashboard.

+
+
+ +
+ + Service disruption +
+ + + +
+

Active

+
+
+
+
+

Elevated error rates on database writes

+

Started 24 Aug 2026, 09:12 UTC

+
+ monitoring +
+

Affects Primary database, Public API

+
+
+
monitoring11:40 UTC
+

Failover completed. Write latency is back to normal and we are watching for recurrence before calling this resolved.

+
+
+
identified09:48 UTC
+

A failing disk on the primary database node is causing write timeouts. Failover to the standby node is in progress.

+
+
+
investigating09:15 UTC
+

We are investigating a rise in write errors affecting the API.

+
+
+
+
+
+ +
+

Scheduled maintenance

+
+
+
+
+

Object storage capacity upgrade

+

31 Aug 2026, 02:00 – 04:00 UTC

+
+ scheduled +
+

Affects Object storage

+
+
+
+ +
+

API

+
+
+
+ Public API + Down +
+
+
90 days ago99.81% uptimeToday
+
+
+
+ Webhook delivery + Operational +
+
+
90 days ago99.99% uptimeToday
+
+
+
+ +
+

Web

+
+
+
+ Dashboard + Operational +
+
+
90 days ago99.97% uptimeToday
+
+
+
+ +
+

Data

+
+
+
+ Primary database + Down +
+
+
90 days ago99.62% uptimeToday
+
+
+
+ Object storage + Maintenance +
+
+
90 days ago99.94% uptimeToday
+
+
+
+ Search index + Operational +
+
+
90 days ago100.00% uptimeToday
+
+
+
+ +
+

Past incidents

+
+
+
+
+

Public API unavailable

+

2 Aug 2026, 14:02 UTC — resolved 14:19 UTC

+
+ resolved +
+

Affects Public API

+
+
+
+
+

Slow dashboard loads in Europe

+

17 Jul 2026, 08:30 UTC — resolved 10:05 UTC

+
+ resolved +
+

Affects Dashboard

+
+
+
+ +

Updated 24 Aug 2026, 11:58 UTC · refreshes every 60 seconds

+
+
+
+ + +
+ + +
+
+

2 · Status page editor

+

web/app/(app)/status-pages/[pageId]/page.tsx · owner or admin

+
+ +
+
+ + +
+ + + All status pages + + +
+
+

Acme Platform Status

+
+ acme.vantage.example.com/status/api + +
+
+
+ + + View page + + +
+
+ +
+
+
+

Details

+

What visitors see at the top of the page.

+
+ published +
+
+
+
+ Published +

Anyone with the link can read this page. Unpublished pages return not found, so you can compose before announcing.

+
+ +
+
+
+ + +
+
+ + + Fixed once created — the link is already out there. +
+
+ + +
+
+ + +
+
+
+ + + Shown above everything else. Clear it to remove the notice. +
+
+
+ +
+
+
+

Components

+

Monitors grouped for the public page. Grouping here is separate from the groups on Monitors.

+
+ +
+
+ +
+
+ + +
+
+
+ prod-api-eu-health + http · every 30s +
+ + +
+
+
+ hooks-dispatch-probe + http · every 60s +
+ + +
+
+
+ +
+
+ + +
+
+
+ pg-primary-10-0-0-5 + tcp · every 30s +
+ + +
+
+
+ minio-gw + http · every 60s +
+ + +
+
+
+ +
+
+ +
+
+
+

Incidents

+

Written by you. Outages Vantage detects appear on the page automatically.

+
+
+ + +
+
+
+
+
+ Elevated error rates on database writes + Opened 09:12 UTC · 3 updates · affects Primary database, Public API +
+
+ monitoring + +
+
+
+
+ Object storage capacity upgrade + 31 Aug, 02:00–04:00 UTC · affects Object storage +
+
+ scheduled + +
+
+
+
+ Slow dashboard loads in Europe + 17 Jul · resolved after 1h 35m · affects Dashboard +
+
+ resolved + +
+
+
+
+ +
+
+
+ + +
+ +
+ + diff --git a/docs/superpowers/plans/2026-08-24-status-pages.md b/docs/superpowers/plans/2026-08-24-status-pages.md index ef0e20a..b009640 100644 --- a/docs/superpowers/plans/2026-08-24-status-pages.md +++ b/docs/superpowers/plans/2026-08-24-status-pages.md @@ -10,11 +10,18 @@ **Spec:** `docs/superpowers/specs/2026-08-24-status-pages-design.md` +**Design:** Approved 2026-08-24. Mockup of both screens: +`docs/superpowers/plans/2026-08-24-status-pages-mockup.html`, also published at +https://claude.ai/code/artifact/13cfe71a-dda7-4780-a8f0-57ea8ae0d57d — open the +local file in a browser if the link is unavailable. Tasks 9 and 10 implement +what it shows; where this plan's code and the mockup disagree, the mockup is +the approved artefact and the code is the error. + ## Global Constraints - **Every new collection carries `instance_id` and MUST be listed in `services.ScopedCollections`** (`server/internal/services/migrate_instance.go:24`), or its rows outlive a deleted instance. - **`models.Monitor` is never marshalled to an anonymous caller.** Only `services.assembleSnapshot` produces public monitor data, and only the fields named in Task 3. -- **No component in `web/` may carry a hex colour.** Use the existing CSS tokens. `web/` is dark-only; the status page inherits that. +- **No component in `web/` may carry a hex colour.** Use the existing Tailwind token names, which are **not** the CSS variable names: `bg-background`, `bg-surface`, `bg-surface-2`, `bg-well`, `border-border`, `border-border-soft`, `text-text-primary`, `text-text-secondary`, `text-text-tertiary`, `accent`, `accent-hover`, `accent-ink`, `success`, `warning`, `danger`. Confirmed against `web/tailwind.config.ts:24-54`. `web/` is dark-only; the status page inherits that. - **Every `/api` route must have an entry in `routeScopes`** (`server/internal/api/scopes.go:27`) or `AssertScopeMapComplete` fails boot (`server/cmd/main.go:267`). - **Every mutating `/api` handler writes an audit event** via `services.LogEvent(instanceID, eventType, actor, serverID, keyID, details)`. - **`server/internal/api/docs/openapi.json` is generated and committed.** `server-deploy.yml` runs `git diff --exit-code` against it, so handler annotations and the committed file must agree. @@ -2373,6 +2380,12 @@ No test runner exists in `web/`. Verification is `npm run build`, `npm run lint` - Consumes: `GET /public/status/:pageId` from Task 7. - Produces: TypeScript types `StatusSnapshot`, `PublicSection`, `PublicComponent`, `PublicDay`, `PublicIncident` exported from `web/lib/api.ts` (added in Task 10; declare them locally in `StatusPageView.tsx` for this task and move them in Task 10 — or do Task 10's type block first if executing in order). +- [ ] **Step 0: Open the approved mockup** + +Open `docs/superpowers/plans/2026-08-24-status-pages-mockup.html` (artboard 1) in a browser and keep it beside you. It is the approved design for this task: header, overall banner, notice, active incidents, scheduled maintenance, component sections, past incidents, refresh footer, in that order. + +Three details in it are decisions rather than styling, and each has a test or a note behind it elsewhere in this plan: the no-data tail renders grey rather than counting as uptime; a component under maintenance keeps its unmodified uptime figure; and every state carries a word and a shape, never a hue alone. + - [ ] **Step 1: Add the rewrite** In `web/next.config.ts`, add to the `rewrites()` array: @@ -2489,8 +2502,8 @@ export default function StatusPageView({ if (!snap.available) { return (
-

{snap.title || "Status"}

-

+

{snap.title || "Status"}

+

{snap.reason === "licence_inactive" ? "This status page is temporarily unavailable." : "Status pages are not enabled on this instance."} @@ -2507,15 +2520,15 @@ export default function StatusPageView({ ) : null}

-

{snap.title}

+

{snap.title}

{snap.description ? ( -

{snap.description}

+

{snap.description}

) : null}
{snap.banner ? ( -
+
{snap.banner.text}
) : null} @@ -2530,7 +2543,7 @@ export default function StatusPageView({ {snap.active_incidents.length > 0 ? (
-

+

Active

@@ -2543,7 +2556,7 @@ export default function StatusPageView({ {snap.upcoming_maintenance.length > 0 ? (
-

+

Scheduled maintenance

@@ -2556,10 +2569,10 @@ export default function StatusPageView({ {snap.sections.map((section) => (
-

+

{section.name}

-
+
{section.components.map((c) => ( ))} @@ -2569,7 +2582,7 @@ export default function StatusPageView({ {snap.history.length > 0 ? (
-

+

Past incidents

@@ -2580,7 +2593,7 @@ export default function StatusPageView({
) : null} -
@@ -2588,7 +2601,9 @@ export default function StatusPageView({ } ``` -**Before writing this, confirm the token class names.** Run `grep -n "^\s*--" web/app/globals.css | head -40` and use the names that are actually defined (`--fg`, `--fg-muted`, `--panel`, `--border`, `--success`, `--warning`, `--danger`, `--accent` above are the expected ones). Do not introduce a hex value if a name differs — fix the class name. +The class names above are the real ones from `web/tailwind.config.ts:24-54`. If a name is ever missing, add the token to the config — never reach for a hex. + +Match the approved mockup for layout and copy: overall banner above the notice, incidents before components, sections in page order, the refresh line in the footer. - [ ] **Step 4: Write the history bar** @@ -2653,8 +2668,8 @@ export default function ComponentRow({ component }: { component: PublicComponent return (
- {component.name} - + {component.name} + @@ -2664,7 +2679,7 @@ export default function ComponentRow({ component }: { component: PublicComponent
-
+
90 days ago {component.uptime_90d.toFixed(2)}% uptime Today @@ -2683,19 +2698,19 @@ import type { PublicIncident } from "@/lib/api"; export default function IncidentCard({ incident }: { incident: PublicIncident }) { return ( -
+
-

{incident.title}

- +

{incident.title}

+ {incident.status.replace("_", " ")}
{incident.affected && incident.affected.length > 0 ? ( -

+

Affects {incident.affected.join(", ")}

) : null} -

+

{new Date(incident.started_at).toLocaleString()} {incident.resolved_at ? ` — resolved ${new Date(incident.resolved_at).toLocaleString()}` @@ -2708,13 +2723,13 @@ export default function IncidentCard({ incident }: { incident: PublicIncident }) .reverse() .map((u, i) => (

  • - + {u.status.replace("_", " ")} - + {new Date(u.at).toLocaleString()} -

    {u.body}

    +

    {u.body}

  • ))} @@ -2754,6 +2769,12 @@ git commit -m "feat: public status page" - Consumes: the ten routes from Task 8; the snapshot types used by Task 9. - Produces: `api.listStatusPages`, `api.getStatusPage`, `api.createStatusPage`, `api.updateStatusPage`, `api.deleteStatusPage`, `api.listStatusIncidents`, `api.createStatusIncident`, `api.updateStatusIncident`, `api.deleteStatusIncident`, `api.postStatusIncidentUpdate`, and the exported types. +- [ ] **Step 0: Open the approved mockup** + +Open `docs/superpowers/plans/2026-08-24-status-pages-mockup.html` (artboard 2). It is the approved design for the editor: back link, title with the full public URL as a click-to-copy record line, View page and Save changes, then the Details, Components and Incidents panels in that order. + +Two details are decisions: the monitor's own identifier stays visible beside the **public name** field, and the field's placeholder is that identifier — so publishing an internal name is a visible choice rather than a default. And the page address is locked after creation, because the link has already been handed out. + - [ ] **Step 1: Add the types** In `web/lib/api.ts`, near the monitor types: @@ -2935,7 +2956,7 @@ In the `api` object in `web/lib/api.ts`: Create `web/app/(app)/status-pages/page.tsx`: a TanStack Query list of `api.listStatusPages()` following the structure of `web/app/(app)/monitors/page.tsx`. Each row shows title, `page_id`, a Published or Draft pill, the component count, and a copy-to-clipboard link to `https:///status/`. A "New status page" button opens a modal with fields for `page_id` (validated client-side against `/^[a-z0-9][a-z0-9-]{1,38}[a-z0-9]$/`, the same rule as `ValidatePageID`), title, description and logo URL. -Read `web/app/(app)/monitors/page.tsx` first and follow its query keys, panel classes and modal pattern rather than inventing new ones. +Read `web/app/(app)/monitors/page.tsx` first and follow its query keys, panel classes and modal pattern rather than inventing new ones. The row and pill treatment is the one in the mockup's Incidents panel. - [ ] **Step 4: Build the editor page** @@ -2945,7 +2966,7 @@ Create `web/app/(app)/status-pages/[pageId]/page.tsx` with three panels: 2. **Sections** — add or remove a named section; within each, add monitors from a picker fed by `api.listMonitors()`, with an optional display-name field per entry. Reorder is out of scope for v1; adding to the end is enough. 3. **Incidents** — list from `api.listStatusIncidents(pageId)`, a form to open an incident or schedule maintenance, and a "post update" control on each open one calling `api.postStatusIncidentUpdate`. -The monitor picker must show the monitor's real name (this is the authenticated side) while making clear the display name is what gets published — label the field "Public name" with the monitor name as its placeholder. +The monitor picker must show the monitor's real name (this is the authenticated side) while making clear the display name is what gets published — label the field "Public name" with the monitor name as its placeholder. Build all three panels to match artboard 2 of the mockup, including its copy: buttons are named for their outcome ("Open incident", "Post update", "Schedule maintenance"), the published toggle spells out that unpublished pages return not found, and the notice field says clearing it removes the notice. - [ ] **Step 5: Add the sidebar entry**