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) {