27008086b0
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.
121 lines
5.3 KiB
Markdown
121 lines
5.3 KiB
Markdown
# terdut-tui
|
|
|
|
TUI client for [terdut-server](https://git.ryuvia.com/niklas/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](https://github.com/charmbracelet/bubbletea) — TUI framework (strict Elm architecture)
|
|
- [Lipgloss](https://github.com/charmbracelet/lipgloss) — styles (all in `internal/tui/styles.go`, never inline)
|
|
- [Bubbles](https://github.com/charmbracelet/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
|
|
|
|
1. **Never call API inside `Update()`** — return `tea.Cmd` instead; the runtime runs it async.
|
|
2. **`View()` is pure** — no side effects, no state mutations.
|
|
3. **All state in `Model`** — no globals.
|
|
4. **All styles in `styles.go`** — never use lipgloss inline in `view.go`.
|
|
Styles live on `Model.styles`, built once by `newStyles(theme.Theme)`; the
|
|
handful of free functions in `view.go` take a `Styles` as their first
|
|
argument. No colour literal appears outside `internal/theme`.
|
|
|
|
## Config
|
|
|
|
Location: `~/.config/terdut-tui/config.yaml`
|
|
|
|
```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
|
|
|
|
```bash
|
|
go run .
|
|
go run . --version
|
|
go run . --self-update
|
|
```
|
|
|
|
## Building
|
|
|
|
```bash
|
|
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) |
|
|
|
|
<!-- graymatter:instructions:begin — managed by `graymatter init`; edits inside this block are overwritten -->
|
|
## 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. ⚠ takes `agent`, not `agent_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.
|
|
<!-- graymatter:instructions:end -->
|