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.
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 testis the gate, and it is whatci.yamlandrelease.yamlrun.make dist VERSION=vX.Y.Zbuilds the four binaries;make dist-assetslists their names. The patternterdut-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.yamlcopies 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
- 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
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. ⚠ 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.