# 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": ""}' ``` 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.