Status pages & incident management
A status page is a public, curated view over the targets you already run in Snitcher. You choose which resources appear on which page; Snitcher computes an overall status banner, surfaces active maintenance, and lets you communicate incidents with a rich-text timeline.
Because components are your targets (not a parallel list you maintain by hand), a single incident automatically shows up on every page that displays the affected resource.
Creating a status page
Status Pages → New status page.
| Field | Notes |
|---|---|
| Title | Shown at the top of the public page. |
| Slug | The page is served at /status/{slug} (lowercase letters, digits, hyphens). |
| Description | Rich text (WYSIWYG), shown under the title. |
| Access | Public, shared Password, or Account (see Access control). |
| Custom domain | e.g. status.example.com — see Custom domains. |
| Published | Unpublished pages 404 publicly (draft mode). |
| Brand colour | A small accent used on the page. |
| Components | Tick the targets to show; optionally give each a public name that hides the internal target name. |
The status banner
Snitcher rolls every component's public status up into one headline using a fixed precedence ladder:
- A component shows an incident-declared status first, then maintenance (
Under Maintenance), then its automatic target status (Up → Operational,Degraded → Degraded Performance,Down → Major Outage). - The page banner is the worst of those, combined with open-incident impact. Note the deliberate asymmetry: one component in major outage reads as Partial System Outage; the banner only says Major System Outage when all components are down or an incident is marked Critical.
- Maintenance never turns the page red — it shows a blue System Under Maintenance.
Incidents
Open a page's Incidents to communicate an outage.
- New incident — give it a title, an incident type, an initial lifecycle
status (
Investigating → Identified → Monitoring → Resolved), a rich-text message, and tick the affected components with the public status to show for each (e.g. Partial Outage). - Post updates — append to the incident's public timeline as you learn more; each update carries a lifecycle status. The newest update's status is the incident's current status.
- Resolve — post an update with status Resolved; the incident stops affecting the banner and moves to Past incidents.
All incident/description/maintenance HTML is sanitised server-side with a strict allowlist (no scripts, event handlers, or unsafe URLs) and rendered through Angular's sanitizer — safe by construction.
Incident response (internal → public)
The Dashboard surfaces the same claimable events feed up front — a red "unclaimed issues" panel lists each open event with Claim/Dismiss actions, an in-progress incident count, and a compact incident history, so responders can see what's wrong and claim it without leaving the home screen. When nothing is open it shows a plain "All systems operational" banner instead. The Incidents page in the main nav is the fuller internal incident-response workspace:
- Events feed & claiming — when a target goes Down or Degraded, a claimable event appears. A responder claims it (or dismisses it as noise). Claiming auto-opens an internal incident scoped to that target and assigns the claimer. Internal incidents never appear on status pages.
- Assignees — an incident can have several assignees; an assignee can act on it without a broader grant.
- Timeline — one message box, and the only way to change an incident's status. Post it as Internal or Public (Internal by default; the org-wide default is configurable in Settings → Incidents). A status selector defaults to Status unchanged — leave it there to post a plain comment, or pick a real status to change it: on a Public message that posts a public status-page update; on an Internal message it changes the status quietly, with no public announcement. Public comments appear on the status page without showing who wrote them; you can edit your own comments, editing other people's needs a separate permission.
- History — every status change, assignment, publish, and comment/update is logged with who did it, visible on the internal page (never public).
- Publish — promote an internal incident to public (so its public timeline entries become visible) once you're ready to communicate externally; unpublish to pull it back.
Settings → Incidents configures global policy: auto-resolve an incident after its targets have recovered for N minutes, mute other alerts for a target while it has an active incident, and whether new messages default to Public (Internal by default — public is an explicit choice).
Permissions. Incidents use their own scoped permissions — you can, for example, let a team claim
and publish incidents only for targets labelled dev, without letting them create incidents from
scratch or touch prod.
Incident types
Types are configurable, colour-coded categories (e.g. Investigating, Degraded Performance, Partial Outage, Security) assignable to an incident. Each carries a Resolved flag. That flag — not the incident's internal lifecycle status — is what the public status page uses to decide whether an incident is pinned under Active incidents or folded into Past incidents: an incident whose assigned type has Resolved off is always active, regardless of its own status; flip the type to Resolved (or assign a Resolved type) and it moves to history. An incident with no type assigned behaves as active.
Five built-in types are seeded (Investigating, Degraded Performance, Partial Outage, Major Outage, Maintenance), all Resolved off by default — every field, including name, colour, and the Resolved flag, is fully editable, and built-in types are just as deletable as custom ones. Manage them, and the policy settings above, together on Settings → Incidents.
Incident notifications
Status pages don't carry their own alerter list. To notify on incident activity, use the Incident Event alert type: create an alert rule that fires when an incident is created, reaches a chosen lifecycle status, or is resolved, and route it to any Alerter (Email / Webhook / Slack / Discord). Delivery reuses your existing alerters — no separate subscriber list is required.
Maintenance on status pages
Any maintenance window marked public appears on the status pages whose components it covers — in-progress windows drive the blue Under Maintenance state and an "in progress" panel; future windows show under Scheduled maintenance. This reuses the same windows that already suppress alerts, so there is nothing extra to model.
Access control
Each page has exactly one gate:
- Public — anyone can view.
- Password — visitors enter a shared password once; Snitcher issues a signed, 12-hour access cookie. Attempts are rate-limited (10/min per IP).
- Account — the visitor must be signed in to Snitcher. (Serve account-gated pages on the primary Snitcher host, not a custom domain — the session cookie can't be shared cross-domain.)
Embedding in another site
A public status page can be embedded in an <iframe> on your own website. Framing is denied by
default (clickjacking protection), so allow your site's origin with the status-page-scoped setting —
this keeps the Snitcher admin UI un-embeddable while opening just the /status/* pages:
# whole app (compose / .env)
Snitcher__Security__StatusPageFrameAncestors: "https://www.example.com"
# multiple origins are space-separated; wildcards work too:
# "https://www.example.com https://*.example.com"
Then embed the page by its slug or custom domain:
<iframe src="https://status.example.com/status/my-page" width="100%" height="600"></iframe>
See Security → Framing for the full header behaviour and
the whole-origin Snitcher__Security__FrameAncestors variant.
Custom domains
Point several domains at Snitcher and each resolves to its own page (status.acme.com,
status.example.com, …). Set the Custom domain on a page, then:
-
DNS — create a
CNAMEfrom your status subdomain to the Snitcher host (use astatus.subdomain; a zone apex can't be a CNAME). -
TLS — terminate HTTPS for the custom domain at your reverse proxy. With Caddy, on-demand TLS makes this automatic:
{on_demand_tls { ask http://snitcher:8080/api/public/domain-check }}:443 {tls { on_demand }reverse_proxy snitcher:8080}Caddy calls the
askendpoint with?domain=<host>before issuing a certificate;GET /api/public/domain-checkreturns 200 only when the host matches a published page's custom domain and 404 otherwise, so Caddy never issues an ACME certificate for an arbitrary hostname pointed at the server (which would otherwise exhaust the Let's Encrypt rate limits). Point theaskat this endpoint rather than a generic health check that answers 200 for every host.(Or add each domain to a Traefik/nginx cert config.) Snitcher serves the SPA for any host; when the host matches a page's custom domain the app boots straight into that public page.
Automatic ACME issuance driven from inside Snitcher, DNS ownership verification, subscriber email lists,
per-component uptime-history bars, an RSS/JSON feed, and a live SignalR push channel are on the roadmap
— see the design report in ai-docs/research/.