44b2eb2cc3
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
91 lines
5.1 KiB
Markdown
91 lines
5.1 KiB
Markdown
# The web UI
|
|
|
|
_What the web UI offers, how sign-in and sessions work, and the Team and Admin tabs._ Back to the [README](../README.md) and the [documentation index](./README.md).
|
|
|
|
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:
|
|
|
|
```bash
|
|
# 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=Lax` session cookie. It lasts
|
|
30 days and slides forward while it is used, so an on-call phone stays signed
|
|
in.
|
|
- The cookie is marked `Secure` when `TERDUT_PUBLIC_URL` starts with `https://`,
|
|
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.
|