The README was 1,240 lines of reference material and still described a SQLite quick start. It is now a short tour (highlights, screenshots of the web UI, an accurate quick start against Postgres), and each topic has its own page under docs/ with an index: deployment, configuration, Alertmanager, incidents, notifications, escalation, dead man's switches, single sign-on, web UI, API and development. SERVICE-ACCOUNTS.md is rewritten from a proposal into a reference, and TEAM-LOOKUP.md is gone with the endpoint it described. The "Upgrading to ..." sections for an unreleased product are dropped. Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
5.1 KiB
The web UI
What the web UI offers, how sign-in and sessions work, and the Team and Admin tabs. Back to the README and the documentation index.
The server serves a web UI at /: the incident queue, each incident's alerts
and timeline with every action (acknowledge, assign, snooze, note, resolve,
archive), who is on call, the alert feed, and an Account tab for your own
password and the ntfy topic your pages go to. It is built for a phone first. On a phone
it navigates through a hamburger menu and has a sticky action bar, it follows the
system's dark mode, and it can be added to the home screen. From 900px wide it switches
to a sidebar with the queue and the incident side by side. The Stats page shows
incident counts, MTTA and MTTR, and alert frequency by name, hour and day over a
chosen range.
You sign in with a username and password. Users have no password until one is set, and a user without one can only use API keys:
# an admin sets someone's first password with their API key
curl -X PUT http://localhost:8080/api/users/2/password \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"password": "<at least 10 characters>"}'
After that, users change it themselves under Account. Changing your own password requires the current one.
How a browser stays signed in:
- A successful login sets an
HttpOnly,SameSite=Laxsession cookie. It lasts 30 days and slides forward while it is used, so an on-call phone stays signed in. - The cookie is marked
SecurewhenTERDUT_PUBLIC_URLstarts withhttps://, so set it to the HTTPS address. TLS terminates at the gateway and the server itself only ever sees plain HTTP. - Requests authenticated by the cookie are checked for cross-origin use (Go's
http.CrossOriginProtection). That is the CSRF guard. Bearer-key clients are not affected. - Setting a password signs that user out everywhere else.
- Ten failed logins for one username within 15 minutes lock that username for the rest of the window.
With TERDUT_PUBLIC_URL set, tapping a push notification opens the incident in
the web UI (/incidents/{id}).
A Team tab holds everything a team owns, in five sub-sections with a URL
each and a strip across the top to move between them: the on-call rota
(/team/rota), the membership (/team/members), the escalation ladder
(/team/escalation), the alert sources with their keys (/team/sources) and
the dead man's switches (/team/deadman). /team itself is an overview — who
is on call today, how many members and owners, how many ladder levels, how many
keys and how many switches — so a page fetches only what it shows. An owner
edits it; a member sees the same pages read-only, because the server refuses
their writes anyway. Somebody in more than one team picks between them above
the strip, since the choice changes the subject of all five.
The rota is a month at a time, one coloured initial per day with a legend underneath, and it says how many days are left uncovered — the question a rota is read for is who holds which stretch, and a run of one colour answers it where a list of dates does not. An owner taps a day to hand it to somebody or empty it, and fills a whole shift from the range form folded in below.
The Admin tab appears only for a system administrator, and holds what
belongs to the whole server rather than to one team. It has three sub-sections,
each with a URL of its own and a strip across the top to move between them:
every team (/admin/teams), every user (/admin/users), and the settings that
used to be environment variables (/admin/settings). /admin itself is an
overview — how many of each, and what each section is for. Adding somebody is
minting them an invite link into a team, rather than creating a bare account:
the person who accepts it picks their own password, so one never passes through
an administrator, and the link carries the team, so they land somewhere with a
queue in it. That happens on the team's own page, since an invite is a fact
about a team; the user list points there rather than asking which team beside a
form.
A name in the team list opens that team's page, at /admin/teams/{id}: when it
was created, how many are in it and how much is open, a field to rename it, the
members with their roles, the invites into it, and deletion. The member list is the
one thing there that needed a new endpoint — GET /api/teams/{id}/members is
member-only and answers 404 to an administrator who is not in the team, which is
the rule and not an oversight, so the page reads GET /api/admin/teams/{id} instead.
An administrator still sees none of that team's incidents, alerts or rota.
A name in the user list opens that person's page, at /admin/users/{id}: their
email and when they joined, where their notifications go, whether they are an
administrator, whether the account is disabled, the teams they are in with their
role in each, a password field for a first or forgotten one, and deletion. It is
the one place membership is edited from the person's side — the Team tab answers
"who is in this team", and answering "which teams is this person in" there means
visiting each team in turn.