Files
Niklas Ye 79e77fadc6
CI / test (push) Successful in 4s
Release / test (push) Successful in 4s
Release / binaries (push) Successful in 12s
Let the release skill drive this repo, and let make drive the pipeline
The release skill only knew repos that deploy an image through a wrapper
chart. terdut-tui publishes binaries to a Gitea release and nothing else,
so its first two releases were cut by hand. It now has a .release.conf
saying KIND=binary, which the skill treats as gate, tag, wait for the
pipeline, then check what was published.

The gate had to exist as make targets for that: fmt, lint and test, the
same three the other repos have. ci.yaml and release.yaml now call them
instead of carrying their own copy of gofmt, vet and the tests, so a green
gate locally and a green pipeline are the same code and cannot drift. The
gofmt handling moved over as written, including the comment on why both of
its failure modes need catching; both fail the target, checked with a
misformatted file and an unparseable one.

The binaries job calls make dist too. DIST_TARGETS is now the one place
that says what a release contains, and dist-assets prints the names dist
builds so the skill can verify the published release against a list
instead of a count. The names are unchanged, and they are the self-updater's
contract with every installed binary: internal/updater matches
terdut-tui-<tag>-<goos>-<goarch> exactly.

CLAUDE.md gains a Release section, including that the annotated tag's
message is what appears on the release page.

Not run in the pipeline yet: make is in the golang image, as terdut-server's
CI relies on, but this repo's workflows only exercise it on the push that
carries this commit, and make dist only on the next tag. A failure in the
release workflow's test job stops the publish rather than shipping
something unchecked.
2026-09-24 08:38:02 +02:00

7.0 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.

Release

Say "Release" (or "Release X.Y.Z") and the release skill runs it. This repo is KIND=binary in .release.conf: it publishes binaries to a Gitea release and has no image, chart or wrapper-chart PR. The run is gate, commit, push, tag, wait for release.yaml, then verify-release. Preconditions and the plan, without side effects:

~/.claude/skills/release/scripts/release-preflight          # state + suggested version
~/.claude/skills/release/scripts/release-preflight vX.Y.Z   # validate that release
  • make fmt lint test is the gate, and it is what ci.yaml and release.yaml run.
  • make dist VERSION=vX.Y.Z builds the four binaries; make dist-assets lists their names. The pattern terdut-tui-<tag>-<goos>-<goarch> is the self-updater's contract with every installed binary, so changing it breaks self-update.
  • The annotated tag's message becomes the release notes (release.yaml copies it, minus its subject line). Write it for a reader of the release page. Never move a published tag.

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.