terdut-server v0.12 made everything team-scoped and v0.20 is what this
client now targets. Against it the old client was wrong in three ways:
the schedule moved to /api/teams/{id}/schedule, GET /api/schedule/current
became a list with one entry per team, and users, incidents, alerts and
schedule entries all grew fields the client ignored.
T steps through all teams and then each of yours. The header names what
is showing, and incident and alert rows gain a Team column when more than
one team can appear. team: in config.yaml picks the team to start on, by
name or id; an unknown one is reported and falls back to all teams.
The schedule is one team's rota, so it shows the active team, or with
all teams showing the first one you own. Writes need an owner or an
administrator, and the picker offers only the team's members, since the
server answers 404 for anybody else. Both are checked up front and the
reason goes in the status bar, rather than surfacing as a 403 after the
user has picked somebody. Stats are not team-scoped by the server and
stay that way here.
Users shows an admin/disabled Flags column. Creating and deleting users
is administrators only, and topic, keys and password work on your own
row or on anyone's for an administrator; the server enforces the same
rule, this only explains it before the round trip.
The server has no version endpoint, so an older one is recognised by
GET /api/teams answering 404, and the TUI says it needs v0.20 or later.
Connecting now also loads /api/teams and /api/me with the key, which
means a wrong key fails on start instead of on the first list; /healthz
does not check it. There is no fallback to the pre-team paths.
Rebuilding a table whose column count changes under loaded rows panicked
inside bubbles, because it re-renders the old rows on SetColumns. The
rows are now cleared first and the cursor put back, so a refresh still
does not jump to the top.
Escalation ladders, invites, integrations and the admin settings are
left to the server's web UI. Checked against a real v0.20.1 server with
two teams, an administrator and a plain member.
Breaking: requires terdut-server v0.20.0 or later. Use terdut-tui v0.9.x
with servers before v0.12.
5.3 KiB
terdut-tui
TUI client for terdut-server, a Prometheus Alertmanager receiver and incident manager. Requires server v0.20.0+ (team-scoped API).
Domain model
The server splits alerts from incidents, and this client mirrors it:
- Alert — Alertmanager's record. Firing or resolved, read-only, no workflow state.
- Incident — the work item: triggered → acknowledged → resolved, with an
assignee, snooze, notes and an append-only timeline. Many alerts to one incident,
correlated by Alertmanager's
groupKey.
The server is multi-team: incidents, alerts and the schedule belong to a team, and
the caller only sees their own teams. Model.activeTeamID (0 = all) narrows the
incident and alert lists via team_id; the schedule is per team and uses
Model.scheduleTeam(). Users have is_admin, and the TUI mirrors the server's
permission rules up front (canEditSchedule, canManageUser, isAdmin) so a 403 is
explained before the round trip, not after. The server has no version endpoint;
an old one is recognised by GET /api/teams answering 404 (errServerTooOld).
All user actions target incidents. Two server behaviours the UI has to respect: manual resolve is terminal (hence the confirmation prompt), and snooze is the non-destructive "not now" alternative.
Tech stack
- Go 1.25+
- Bubbletea — TUI framework (strict Elm architecture)
- Lipgloss — styles (all in
internal/tui/styles.go, never inline) - Bubbles — table, textinput, help components
Project layout
main.go CLI entry point: flags, config load, start TUI
internal/api/client.go REST API client — one method per endpoint
internal/config/config.go Config loader (~/.config/terdut-tui/config.yaml)
internal/theme/ Colour themes: semantic tokens, built-ins, user file loader
internal/tui/ Bubbletea UI
model.go Model struct, mode/section constants, Init(), tea.Cmd constructors
update.go Update() — dispatch only, no API calls inline
view.go View() — pure rendering
keys.go keyMap (bubbles/key pattern)
styles.go Styles struct — every lipgloss style, built from a theme
internal/updater/updater.go Self-update via Gitea releases
Architecture rules
- Never call API inside
Update()— returntea.Cmdinstead; the runtime runs it async. View()is pure — no side effects, no state mutations.- All state in
Model— no globals. - All styles in
styles.go— never use lipgloss inline inview.go. Styles live onModel.styles, built once bynewStyles(theme.Theme); the handful of free functions inview.gotake aStylesas their first argument. No colour literal appears outsideinternal/theme.
Config
Location: ~/.config/terdut-tui/config.yaml
server_url: https://terdut.example.com
api_key: <64-char hex key>
refresh_interval: 30 # seconds, optional, default 30
theme: gruvbox-dark # optional, default gruvbox-dark
team: Ops # optional, team name or id to start on, default all
Built-in themes are gruvbox-dark and gruvbox-light; user themes are YAML
files in ~/.config/terdut-tui/themes/, optionally extends:-ing a built-in.
See the README for the token list.
The API key is a one-time secret generated by terdut-server (POST /api/users/{id}/api-keys).
Running
go run .
go run . --version
go run . --self-update
Building
go build -ldflags="-X main.version=v0.1.0" -o terdut-tui .
Sections
Incidents (the queue, and the default) · Alerts (raw read-only feed) ·
Stats (MTTA/MTTR and alert frequency charts) ·
Archived (archived incidents) · Schedule · Users
Development stages
| Stage | Feature |
|---|---|
| 1 | Scaffold, config, health check, placeholder TUI |
| 2 | Alert dashboard with auto-refresh and stats |
| 3 | Alert detail: acknowledge, comment, statistics charts |
| 4 | On-call schedule calendar view |
| 5 | User management and API key lifecycle |
| 6 | Incidents: queue, timeline, ack/assign/snooze/resolve, MTTA/MTTR |
| 7 | Teams: T switcher, per-team schedule, admin/disabled markers (server v0.20) |
Memory (GrayMatter)
This project has persistent agent memory via the graymatter MCP tools:
memory_search(agent_id,query) — call at the start of a task when prior context might matter.memory_add(agent_id,text) — call whenever you learn something durable: user preferences, decisions, conventions, gotchas.memory_reflect(action,agent,text/target) — update or forget stale facts. ⚠ takesagent, notagent_id.checkpoint_save/checkpoint_resume(agent_id) — snapshot/restore session state before major refactors or across restarts.
Use a stable agent_id of the form <project>-<role> (e.g. myapp-backend). Store conclusions, not conversation logs. Err on the side of remembering.