Epic: teams, escalation chains, self-service onboarding and an admin page #1

Open
opened 2026-09-19 20:03:28 +00:00 by niklas · 0 comments
Owner

Tracking issue. All sub-issues are done and deployed as of v0.15.0, 2026-09-21.

Why this was opened

terdut was single-tenant, and an unacknowledged incident re-paged the same person forever. There was no sign-up, and the alert webhook took no credential at all.

What shipped

#2 Postgres v0.11.1
#3 Roles — a system administrator, and the user and API-key endpoints locked down v0.12.0
#4 Teams — data scoped per team, per-team integration keys, schedules and dead-man switches v0.12.0
#5 Admin page, and behaviour settings moved from env into the database v0.13.0
#6 Escalation — levels with timeouts, targeting a person or whoever is on call v0.13.0
#17 Team settings UI — everything an owner configures, in the browser v0.14.0
#7 Sign-up, invite links, first-run checklist v0.15.0
— User management page — /admin/users/{id}, and the admin/team boundary corrected below v0.16.0

The unauthenticated POST /api/alertmanager/webhook was removed in v0.13.0, after Ryuvia/charts#233 moved this cluster's Alertmanager onto an integration key.

Decisions, for whoever reads this later

Topic Decision
Hosting model Self-hosted first, one org per install
Sign-up open / invite_only, default invite-only; invites are links, not email; no domain mode, because there is no email to verify against
Team membership Many teams per user, role per team
Team roles owner = team config + membership; member = incident work. An admin configures any team without being in it, but reads none — see the correction below
Multi-team UI One combined queue with team badges and a filter
Channels ntfy only
Escalation targets Users + on-call; repeat N times, then the team's fallback topic
Escalation vs reminders A team with a ladder gets escalation and no reminders — two pages for one silence is how a tool gets muted
Schedules Per-team day-based model; rotations are a separate issue
Settings Behaviour in the DB (seeded from env, never overwritten), infrastructure in env
Dead-man Per team; env vars seed a team that has none
Cross-team incidents Not supported; the team is fixed at creation
Disabling a user Not deleting: deleting nulls acknowledged_by and rewrites history

Correction: what "an admin is not implicitly in every team" actually meant

This row used to read "an admin is not implicitly in every team", full stop. That was never what the code did, and the gap went unnoticed from v0.12.0 to v0.16.0 because nothing tested it.

requireTeamOwner has taken the administrator flag since a4fbd60 (#4), with the reason in its own comment: "A system administrator passes without being a member, because somebody has to be able to repair a team whose owner has left." It guards nine call sites — membership, invites, schedule, integrations, escalation, dead-man, rename, delete — so an administrator has always been able to configure any team on the server.

The decision holds in the direction that matters. An administrator configures any team; an administrator reads no team. callerTeamIDs is built from real memberships only, so the queue, the alerts and the incidents stay invisible until they actually join a team — which is a membership change, and shows as one.

Both halves are now pinned by TestAdmin_ConfiguresATeamTheyAreNotIn: the admin renames, invites, adds and removes on a team they are not in, and then sees zero of its incidents. The README's Teams table marks owner as owner-or-admin, and its Authentication section states the two directions.

Still outstanding, and not covered by any issue yet

  • terdut-tui is broken against this server and has been since v0.12.0. The schedule endpoints moved under the team, GET /api/schedule/current returns an array, non-admins get 403 on the user endpoints, and ingestion needs an integration key. Nothing in the cluster runs it, which is why it went unnoticed — it wants its own issue in that repo.
  • PV pvc-c5ef5aa7-017d-4550-ab93-c17f640e0c1f still holds the pre-Postgres SQLite database, Released and retained.
  • On-call rotations — the day-based model stays until somebody wants layers and overrides.
  • Any channel other than ntfy — no email, SMS or outbound webhooks.
  • The user management page has not been looked at in a browser. Its wiring is checked (imports, API calls, CSS classes, deep-link routing) and the server side is tested, but no one has clicked through /admin/users/{id}, particularly at phone width.
Tracking issue. **All sub-issues are done and deployed as of v0.15.0, 2026-09-21.** ### Why this was opened terdut was single-tenant, and an unacknowledged incident re-paged the same person forever. There was no sign-up, and the alert webhook took no credential at all. ### What shipped | | | | |---|---|---| | #2 | **Postgres** | v0.11.1 | | #3 | **Roles** — a system administrator, and the user and API-key endpoints locked down | v0.12.0 | | #4 | **Teams** — data scoped per team, per-team integration keys, schedules and dead-man switches | v0.12.0 | | #5 | **Admin page**, and behaviour settings moved from env into the database | v0.13.0 | | #6 | **Escalation** — levels with timeouts, targeting a person or whoever is on call | v0.13.0 | | #17 | **Team settings UI** — everything an owner configures, in the browser | v0.14.0 | | #7 | **Sign-up, invite links, first-run checklist** | v0.15.0 | | — | **User management page** — `/admin/users/{id}`, and the admin/team boundary corrected below | v0.16.0 | The unauthenticated `POST /api/alertmanager/webhook` was removed in v0.13.0, after `Ryuvia/charts#233` moved this cluster's Alertmanager onto an integration key. ### Decisions, for whoever reads this later | Topic | Decision | |---|---| | Hosting model | Self-hosted first, one org per install | | Sign-up | `open` / `invite_only`, default invite-only; invites are links, not email; no domain mode, because there is no email to verify against | | Team membership | Many teams per user, role per team | | Team roles | `owner` = team config + membership; `member` = incident work. An admin **configures any team without being in it**, but **reads none** — see the correction below | | Multi-team UI | One combined queue with team badges and a filter | | Channels | ntfy only | | Escalation targets | Users + on-call; repeat N times, then the team's fallback topic | | Escalation vs reminders | A team with a ladder gets escalation and **no** reminders — two pages for one silence is how a tool gets muted | | Schedules | Per-team day-based model; rotations are a separate issue | | Settings | Behaviour in the DB (seeded from env, **never** overwritten), infrastructure in env | | Dead-man | Per team; env vars seed a team that has none | | Cross-team incidents | Not supported; the team is fixed at creation | | Disabling a user | Not deleting: deleting nulls `acknowledged_by` and rewrites history | ### Correction: what "an admin is not implicitly in every team" actually meant This row used to read *"an admin is **not** implicitly in every team"*, full stop. That was never what the code did, and the gap went unnoticed from v0.12.0 to v0.16.0 because nothing tested it. `requireTeamOwner` has taken the administrator flag since a4fbd60 (#4), with the reason in its own comment: *"A system administrator passes without being a member, because somebody has to be able to repair a team whose owner has left."* It guards nine call sites — membership, invites, schedule, integrations, escalation, dead-man, rename, delete — so an administrator has always been able to configure any team on the server. The decision holds in the direction that matters. An administrator **configures** any team; an administrator **reads** no team. `callerTeamIDs` is built from real memberships only, so the queue, the alerts and the incidents stay invisible until they actually join a team — which is a membership change, and shows as one. Both halves are now pinned by `TestAdmin_ConfiguresATeamTheyAreNotIn`: the admin renames, invites, adds and removes on a team they are not in, and then sees zero of its incidents. The README's Teams table marks **owner** as owner-or-admin, and its Authentication section states the two directions. ### Still outstanding, and not covered by any issue yet - **terdut-tui is broken against this server** and has been since v0.12.0. The schedule endpoints moved under the team, `GET /api/schedule/current` returns an array, non-admins get `403` on the user endpoints, and ingestion needs an integration key. Nothing in the cluster runs it, which is why it went unnoticed — it wants its own issue in that repo. - **PV `pvc-c5ef5aa7-017d-4550-ab93-c17f640e0c1f`** still holds the pre-Postgres SQLite database, `Released` and retained. - **On-call rotations** — the day-based model stays until somebody wants layers and overrides. - **Any channel other than ntfy** — no email, SMS or outbound webhooks. - **The user management page has not been looked at in a browser.** Its wiring is checked (imports, API calls, CSS classes, deep-link routing) and the server side is tested, but no one has clicked through `/admin/users/{id}`, particularly at phone width.
niklas changed title from Teams, escalation chains, self-service onboarding and an admin page to Epic: teams, escalation chains, self-service onboarding and an admin page 2026-09-20 05:59:55 +00:00
niklas added the epic label 2026-09-20 06:12:57 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: niklas/terdut-server#1