79e77fadc6
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.
148 lines
7.0 KiB
Markdown
148 lines
7.0 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.
|
|
|
|
## 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:
|
|
|
|
```sh
|
|
~/.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](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 -->
|