From f1754c583e3b7aebd610fcc4f31d4d09b6d1d328 Mon Sep 17 00:00:00 2001 From: Niklas Ye Date: Sat, 10 Oct 2026 13:29:08 +0200 Subject: [PATCH] Flag incidents that carry notes in the queue A blue tag with the notebook icon and a count marks an incident with working notes; a green one marks a note on what fixed it. Both let you scan the queue for incidents that have more to say than their title, without opening each one. The incident list and detail JSON gain note_count and resolution_note_count, counted in the same query that selects the incident, so a list costs no extra request per row. The fields are additive: terdut-tui and terdut-operator ignore them and need no change. Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN --- internal/api/incident_store.go | 7 ++++++- internal/models/incident.go | 6 ++++++ internal/web/static/app.css | 2 ++ internal/web/static/js/queue.js | 3 ++- internal/web/static/js/ui.js | 13 +++++++++++++ 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/internal/api/incident_store.go b/internal/api/incident_store.go index cd7aafb..033a0ef 100644 --- a/internal/api/incident_store.go +++ b/internal/api/incident_store.go @@ -69,7 +69,11 @@ const incidentSelectFrom = ` i.acknowledged_by, i.acknowledged_at, ack.username, i.acknowledged_by_service_account_id, acksa.name, i.assigned_to, asg.username, i.snoozed_until, - i.resolved_at, i.resolution_source, i.archived_at + i.resolved_at, i.resolution_source, i.archived_at, + -- How many notes, so a list can flag the incidents that carry extra + -- information without fetching each timeline. + (SELECT count(*) FROM incident_events e WHERE e.incident_id = i.id AND e.type = 'note'), + (SELECT count(*) FROM incident_events e WHERE e.incident_id = i.id AND e.type = 'resolution_note') FROM incidents i JOIN teams t ON t.id = i.team_id LEFT JOIN users ack ON ack.id = i.acknowledged_by @@ -90,6 +94,7 @@ func scanIncident(s scanner) (models.Incident, error) { &i.AcknowledgedByServiceAccountID, &i.AcknowledgedByServiceAccountName, &i.AssignedToID, &i.AssignedToUser, &snoozedUntil, &resolvedAt, &i.ResolutionSource, &archivedAt, + &i.NoteCount, &i.ResolutionNoteCount, ); err != nil { return i, err } diff --git a/internal/models/incident.go b/internal/models/incident.go index c1847ca..cefc675 100644 --- a/internal/models/incident.go +++ b/internal/models/incident.go @@ -66,6 +66,12 @@ type Incident struct { ArchivedAt *time.Time `json:"archived_at,omitempty"` + // NoteCount and ResolutionNoteCount count the plain working notes and the + // "what fixed it" notes on the timeline, so a list can flag the incidents + // that carry extra information. + NoteCount int `json:"note_count"` + ResolutionNoteCount int `json:"resolution_note_count"` + // Alerts is populated by GET /api/incidents/{id} only. Alerts []Alert `json:"alerts,omitempty"` } diff --git a/internal/web/static/app.css b/internal/web/static/app.css index eca6fae..956695e 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -522,6 +522,8 @@ input:focus, textarea:focus { outline: none; border-color: var(--accent); box-sh .badge.st-unreachable, .badge.st-unpageable { background: var(--crit-soft); color: var(--crit); } .badge.sev-critical { background: var(--crit-soft); color: var(--crit); } .badge.sev-warning { background: var(--warn-soft); color: var(--warn); } +.badge.has-note { background: var(--info-soft); color: var(--info); } +.badge.has-fix { background: var(--ok-soft); color: var(--ok); } .badge.sev-info { background: var(--info-soft); color: var(--info); } /* ---------- incident detail ---------- */ diff --git a/internal/web/static/js/queue.js b/internal/web/static/js/queue.js index 43fb091..903000c 100644 --- a/internal/web/static/js/queue.js +++ b/internal/web/static/js/queue.js @@ -1,7 +1,7 @@ // The incident queue: filter chips and a list of incident rows. import * as api from './api.js'; -import { h, clear, icon, badge, severityBadge, originChip, emptyState, spinner, fadeOnOverflow } from './ui.js'; +import { h, clear, icon, badge, noteTags, severityBadge, originChip, emptyState, spinner, fadeOnOverflow } from './ui.js'; import { ago, until, isFuture, severityClass, labelSummary, originOf, titleWithoutOrigin } from './format.js'; import { state, myID, onTeamChange } from './state.js'; import * as onboarding from './onboarding.js'; @@ -309,6 +309,7 @@ function row(inc, index) { // The left-border colour alone doesn't say what it means; spell it out // too, same badge the incident detail page uses for severity. inc.severity && severityBadge(inc.severity), + ...(noteTags(inc) || []), assignee, team, labels && h('span', { class: 'labels', text: labels }), diff --git a/internal/web/static/js/ui.js b/internal/web/static/js/ui.js index 04fe645..19e1d8f 100644 --- a/internal/web/static/js/ui.js +++ b/internal/web/static/js/ui.js @@ -194,6 +194,19 @@ export function badge(text, cls = '') { return h('span', { class: `badge ${cls}`, text }); } +// A tag saying an incident has notes: blue for working notes, green for a note +// on what fixed it. Null when there are none, so it can sit in a row unguarded. +export function noteTags(inc) { + const tag = (n, cls, label) => n > 0 && h('span', { + class: `badge plain ${cls}`, title: `${n} ${label}${n === 1 ? '' : 's'}`, + }, icon('note', 'icon badge-icon'), String(n)); + const tags = [ + tag(inc.note_count, 'has-note', 'note'), + tag(inc.resolution_note_count, 'has-fix', 'resolution note'), + ].filter(Boolean); + return tags.length ? tags : null; +} + // Where it came from (the cluster): a chip in that origin's colour, with a // server icon so it reads as a place and not as a status. export function originChip(value) {