Files
terdut-tui/CLAUDE.md
T
Niklas Ye f4ca0059dc
CI / test (push) Successful in 12s
Release / test (push) Successful in 5s
Release / binaries (push) Successful in 12s
Sign in as a user instead of with an API key
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.
2026-09-23 22:15:14 +02:00

5.9 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

  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

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

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)
8 Sign in as a user instead of an API key (server v0.10+ session cookie)

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.