Files
terdut-server/docs/web-ui.md
T
Niklas Ye 44b2eb2cc3 Rewrite the README as highlights with screenshots; move the detail into docs/
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
2026-10-09 14:56:13 +02:00

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