Turn the status page into a full status page generator

Components can be put in groups and given public names. Public incidents carry
posts written for customers (the internal timeline stays internal), the
components they affect, and a page for each incident plus paged history.
Scheduled maintenance has its own window, shows affected components as under
maintenance, and is announced when scheduled, started, ended or canceled.

Branding (title, logo, accent checked for contrast, contact link, footer) and a
custom domain that serves only the status page. Visitors follow by RSS, by
confirmed email, or by webhook that must answer a test request. Admin UI for
all of it, tests, README and design notes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
VeillaxandClaude Sonnet 5.5 committed 2026-10-27 17:05:38 -06:00
1 parent b4fd31a182
commit cf4d5b3ce2
12 files changed
+2403 -147

No files matched your search

+60 -2
View File
@@ -8,7 +8,7 @@ Sandbar covers the ground of five tools:
| Area | Like | What it does |
|---|---|---|
| **Topics** | ntfy | Publish a message to a topic with one HTTP request. Subscribe in the browser, with `curl`, or with the ntfy phone app |
| **Monitors** | Tianji, Uptime Kuma | Website, port, DNS and certificate checks, heartbeats for cron jobs, and a public status page |
| **Monitors** | Tianji, Uptime Kuma | Website, port, DNS and certificate checks, heartbeats for cron jobs, and a status page with groups, incident posts, maintenance and subscribers |
| **Websites** | Tianji, Plausible, Umami | Page views, visitors, referrers, browsers, countries and custom events for your own sites, with no cookies and no personal data |
| **On-call** | Opsgenie | Rotating schedules, escalation policies, and alerts that are acknowledged, snoozed or resolved |
| **Incidents** | incident.io | Declare, assign, update and close an incident, with a timeline and a post-mortem |
@@ -190,7 +190,7 @@ app to be paged on your phone when an escalation policy reaches you.
| Heartbeat | Your job requests `/api/v1/hb/<token>` within its interval plus the allowed delay | A request on a down heartbeat recovers it at once |
A monitor goes down after the set number of failures in a row (default 2), raises an alert with the monitor's
escalation policy, and recovers on the first success. Monitors marked public appear on `/status`, together with
escalation policy, and recovers on the first success. Monitors marked public appear on the status page (see below), together with
incidents marked public. The status page never shows staff names or private monitors.
The status page shows each public monitor as 90 daily uptime bars, like most hosted status pages. A day is green with
@@ -202,6 +202,64 @@ in a small per-day table, so the bars go back 90 days even though raw check resu
Monitors cannot probe private or internal addresses unless you tick **Allow targets on private networks** on that
monitor, so a user who can add a monitor cannot use Sandbar to scan your network.
## Status page
The public page is at `/status` (and at the root of its own domain, see below). Everything is set up under
**Status page** in the sidebar.
- **Components and groups.** Every monitor with **Show on the public status page** ticked is a component, shown as
90 daily uptime bars (see Monitors). Create groups on the Groups tab, then pick a group, and optionally a different
public name, in the monitor's form. Groups are ordered by their order number; components without a group come first.
- **Incidents.** An incident marked public is posted with its status, the components it affects, and the updates you
write. Declare it as public, or switch it on later. **Post an update** (or changing the status) adds a post with
your text, or standard wording if you leave it empty. Customers see only the posts. The internal timeline,
comments, names and post-mortem never leave Sandbar. The page shows the last 14 days; everything older is on
`/status/history`, and each incident has its own page with the full list of updates.
- **What the page says.** A component is *Major outage* when its monitor is down or a sev1 incident names it,
*Partial outage* for sev2, *Degraded performance* for sev3, and *Under maintenance* during a maintenance window for
it. The banner shows the worst of them.
- **Scheduled maintenance.** Give a window (your local time in the form, stored as UTC) and the affected
components. It is announced when scheduled, listed on the page until it ends, and subscribers are told when it
starts and when it ends. During the window its components show as under maintenance instead of as an outage.
Windows can be moved or canceled, and subscribers are told. Alerts for failing checks still fire as usual.
- **Branding.** Title, description, logo (an https address), accent color, a contact link and footer text. The
accent is checked so that white text on it stays readable (WCAG 4.5 to 1).
- **Custom domain.** Enter a host name such as `status.example.com`, point its DNS at your server and have the
reverse proxy forward that host to Sandbar with the `Host` header unchanged (Caddy does this by default; with nginx
add `proxy_set_header Host $host;`). Sandbar then serves the status page at `/` on that host, and nothing else:
the app, API and topics are not reachable through it. Links in emails and feeds use `https://<domain>`. The proxy
provides the certificate. With `SANDBAR_TRUST_PROXY=1` the host is read from `X-Forwarded-Host` when present.
### Following the page
Visitors choose **Subscribe to updates** on the page:
- **Email.** Needs `SANDBAR_SMTP_HOST`; the form is hidden without it. A visitor gets a confirmation link, and no
mail is sent until it is opened. Every message has an unsubscribe link, which asks for one click on the page, so
mail scanners cannot unsubscribe people. Unconfirmed addresses are forgotten after 7 days.
- **Webhook.** The address must answer a test request with a 2xx status before it is subscribed. Requests to private
networks are refused. Each update is a JSON `POST`:
`{"page","event","title","status","message","url","time","unsubscribe"}`. The events are `incident.created`,
`incident.updated`, `incident.resolved`, `maintenance.scheduled`, `maintenance.rescheduled`, `maintenance.started`,
`maintenance.completed` and `maintenance.canceled`. The `unsubscribe` link works with a `POST` of
`{"token": ...}` to `/status/unsubscribe`, or can be opened in a browser.
- **RSS.** `/status/feed.rss` has every incident update and maintenance announcement, newest first.
Subscribers can be seen and removed on the Subscribers tab (webhook addresses show only the host). Addresses are
stored encrypted. Subscribing is limited to 6 requests per hour per address.
### Status page API
| Request | |
|---|---|
| `GET /status.json` | Settings, groups, components with their 90 days, current incidents, upcoming and running maintenance |
| `GET /status/api/history?before=<time>&limit=<n>` | Past incidents and maintenance, newest first, with the time to pass as `before` for the next page |
| `GET /status/api/incidents/<id>` | One public incident with all its updates |
| `GET/PUT /api/v1/statuspage` | Settings (PUT is for administrators) |
| `/api/v1/status-groups` | Groups, as the other configuration lists |
| `/api/v1/maintenance`, `POST .../<id>/cancel` | Scheduled maintenance (writes are for administrators) |
| `PATCH /api/v1/incidents/<id>` | Also takes `components` (monitor ids) and `note` (the text of the public post) |
## Websites (analytics)
Add a website on the Websites page, then paste its one-line tracking code into the head of your pages: