f4ca0059dc
The web UI signs in with a username and password and holds a session cookie; the TUI was the only client still needing an API key pasted into a config file. It now asks for the same credentials on a form at start. What is kept between runs is the session token, not the password, in session.json under the config directory, mode 0600 and keyed by server URL so one server's token is never offered to another. It resumes on the next start; the server's sessions last 30 days and slide with use. L signs out, which ends the session on the server and deletes the saved one even if the server cannot be reached. The client attaches the cookie by hand instead of using a cookie jar: the server marks it Secure behind https, and a jar drops a Secure cookie it is given over plain http, which would break a local server for no reason. It sends no Authorization header at all, since the server judges a request carrying one on that alone and never falls back to the cookie. Writes go through the server's cross-origin guard, which lets a client that sends neither Origin nor Sec-Fetch-Site through; checked against a real v0.20.1 server for both reads and writes. A 401 from anything means the session is gone (expired, ended from the web UI, or the account disabled), so the TUI returns to the form with the reason, forgets the saved token, and drops what the last session loaded rather than showing it to whoever signs in next. A 403 is a permission and leaves the session alone. The refresh timer is started once, so signing out and in does not leave two running. An account with no password cannot sign in, and the server answers it exactly like a wrong password, so the form's message says a password must be set first. Users created only for API access hit this. Breaking: api_key in config.yaml is no longer used. It is not an error to leave it there; the form says it is ignored. API keys still exist on the server and k in Users still manages them.
129 lines
5.9 KiB
Markdown
129 lines
5.9 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
|
|
username: niklas # optional, prefills the sign-in form
|
|
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.
|
|
|
|
There is no API key in the config. The TUI signs in as a user (`POST /api/login`, the
|
|
same session cookie as the web UI) and `internal/session` keeps the token in
|
|
`~/.config/terdut-tui/session.json`, mode 0600, keyed by server URL. The client
|
|
attaches `terdut_session` itself rather than using a cookie jar, because a jar drops the
|
|
server's Secure cookie over plain http. It must never send `Authorization` as well: the
|
|
server judges a request with that header on it alone. A 401 from anything (`msgError`
|
|
in `update.go`) returns to the sign-in form and clears `Model`. A user with no password
|
|
cannot sign in, and the server answers it like a wrong one, so the form says so.
|
|
|
|
## 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) |
|
|
| 8 | Sign in as a user instead of an API key (server v0.10+ session cookie) |
|
|
|
|
<!-- 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 -->
|