Assigning an on-call week panicked with "index out of range [-1]" on an ordinary schedule. The index came from scheduleTable.Cursor(). The cursor is not ours. bubbles' SetRows clamps it down when rows shrink (`if m.cursor > len(rows)-1`) but never back up, so setting zero rows drives it to -1 and filling the table afterwards leaves it there -- -1 is not greater than len-1, so nothing corrects it. Every table in this package is rebuilt from empty exactly once, when the first WindowSizeMsg arrives before any fetch has returned, so every cursor started at -1 and stayed there until the user pressed up or down. Pressing a direction key first is why this was survivable at all. setRows restores the invariant the rest of the package already assumes: a table with rows has a usable cursor. Every rebuild goes through it. Five call sites also bounds-checked only the top of the range, and are now consistent with their siblings, which already had `i < 0 ||`. They were the same latent panic: deleting a schedule entry, deleting a user, editing a topic, opening the API key menu, and the week assignment that actually fired. The regression test deliberately never calls SetCursor. That is what the existing schedule tests do, and SetCursor clamps, which is exactly how this got past them. It drives the real order instead: size, then data, then keys. Also carries a gofmt pass, which is why untouched files appear in the diff. The move to git.ryuvia.com rewrote import paths without re-sorting them, and the new path sorts before github.com/charmbracelet/..., where the old one sorted after. go vet does not look at import order, so CI had nothing to say.
terdut-tui
A terminal user interface for terdut-server. Communicates with the server over its REST API.
Written in Go using Bubbletea.
Features
- Incident queue — open incidents with severity, status, assignee and age, auto-refreshing
- Incident actions — acknowledge, assign, snooze, note, resolve and archive
- Timeline — the full history of an incident, system events, pages and notes together
- Alert feed — the raw read-only alerts underneath, each linked to its incident
- On-call schedule — visual calendar of who is on duty, assign and remove entries
- Statistics — MTTA and MTTR, plus alert frequency by name, hour and day
- User management — add and remove users, manage API keys, set each user's ntfy topic
Requires terdut-server v0.4.0 or later. Earlier servers have no incidents API; use terdut-tui v0.3.x with those.
Alerts and incidents
The server keeps two objects and this client follows that split:
- An alert is Alertmanager's record — firing or resolved, and read-only here.
- An incident is the work item. It is what you acknowledge, assign, snooze, discuss and resolve, and it is where all the actions live.
Incidents are correlated by the groupKey Alertmanager already computed from your
group_by configuration, so several alerts commonly share one incident.
Two behaviours worth knowing before you press a key:
- Resolving is final. The server treats a manual resolve as terminal: a later occurrence opens a new incident rather than reopening this one, and if the alert underneath never stops firing the incident stays closed. The TUI asks for confirmation before doing it.
- Snooze is the "not now" button. It hides an incident from the default queue without closing it, and expires on its own.
Push notifications
When the server is configured for ntfy, an incident that opens pages whoever is
on call. Each user has their own topic, shown as a column in the Users section
and edited with t. A user with no topic falls back to the server's shared
fallback topic, which carries no Acknowledge button — the topic is shared, so
a button on it would let any subscriber acknowledge as somebody else.
Every delivery lands on the incident's timeline: Notified <user> (triggered)
when ntfy accepted the page, and Notification to <user> failed when it ran out
of retries. That second one is the one to look for when nobody's phone rang.
Editing topics needs terdut-server v0.6.0 or later; the timeline entries need v0.7.0 or later. Against an older server the topic column stays empty and editing one reports the server's 404.
Installation
Download the latest release binary for your platform from the releases page, or build from source:
go install git.ryuvia.com/niklas/terdut-tui@latest
Configuration
Create ~/.config/terdut-tui/config.yaml:
server_url: https://terdut.example.com
api_key: <your-api-key>
refresh_interval: 30 # seconds, optional
The API key is generated in terdut-server. See the server documentation for how to bootstrap a user and issue an API key.
Usage
terdut-tui start the TUI
terdut-tui --version print version
terdut-tui --self-update update to the latest release
Keybindings
Global:
| Key | Action |
|---|---|
j / ↓ |
Move down |
k / ↑ |
Move up |
tab / shift+tab |
Next / previous section |
enter |
Open detail |
esc |
Go back |
r |
Refresh |
f |
Cycle filter |
q |
Quit |
The sections, in tab order: Incidents · Alerts · Stats · Archived · Schedule · Users.
Incidents section:
| Key | Action |
|---|---|
f |
Cycle: open → triggered → acknowledged → resolved → snoozed |
x |
Archive (resolved incidents only) |
Incident detail:
| Key | Action |
|---|---|
a / A |
Acknowledge / clear acknowledgement |
R |
Resolve — asks to confirm, and is final |
s |
Assign to a user |
z / Z |
Snooze for a duration / un-snooze |
c |
Add a note |
[ / ] |
Select a note |
d |
Delete the selected note (your own only) |
x |
Archive / un-archive |
Alerts section (read-only):
| Key | Action |
|---|---|
f |
Cycle: firing → resolved → all → archived |
i |
In detail: jump to the alert's incident |
Stats section:
| Key | Action |
|---|---|
j / k, pgup / pgdn |
Scroll |
Schedule section:
| Key | Action |
|---|---|
+ / W |
Assign a day / a whole week |
d |
Remove the assignment |
← / → |
Shift the week window |
One person holds a given day. Assigning over days somebody else already has
asks first — naming them and how many days are being taken — and moves the whole
selection at once when you accept, so reassigning a week is one confirmation
rather than seven deletions. Taking somebody's shift needs terdut-server
v0.8.0 or later; against an older server the assignment is refused with
date already assigned.
Users section:
| Key | Action |
|---|---|
n |
Create a user |
t |
Edit the user's ntfy topic — submit empty to clear it |
d |
Delete a user |
k |
API keys for the selected user |