Skip to main content

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.

FieldNotes
TitleShown at the top of the public page.
SlugThe page is served at /status/{slug} (lowercase letters, digits, hyphens).
DescriptionRich text (WYSIWYG), shown under the title.
AccessPublic, shared Password, or Account (see Access control).
Custom domaine.g. status.example.com — see Custom domains.
PublishedUnpublished pages 404 publicly (draft mode).
Brand colourA small accent used on the page.
ComponentsTick 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.

  1. 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).
  2. 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.
  3. 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:

  1. DNS — create a CNAME from your status subdomain to the Snitcher host (use a status. subdomain; a zone apex can't be a CNAME).

  2. 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 ask endpoint with ?domain=<host> before issuing a certificate; GET /api/public/domain-check returns 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 the ask at 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.

note

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/.