Web: visual design pass (issue #26)
Addresses the screenshot-review feedback in #26. No framework or build step added — all of this stays within the existing plain HTML/CSS/ vanilla-JS + go:embed architecture. - Nav: re-enable the bottom tab bar that was already built and switched off (Queue/On-call/Alerts/Team + a "More" sheet for Stats/Admin/Account), replacing the hamburger on phone width. - Queue: chip counts, a scroll fade on the filter row, a "Triggered Xh ago" + severity label per row, a chevron on the team switcher so it reads as a dropdown. - On-call: collapse repeated same-person days into shift bars (week view and "your shifts" both), show the week as a date range with the ISO week number as secondary text, split "Current shift" out from "Next shifts" with "ends in Nd", a pill badge + row highlight for "you". - Incident detail: fix the actual bug behind the duplicate "acknowledged" timeline entries (acknowledgeIncident's UPDATE had no guard on the incident's current status, so acknowledging an already-acknowledged incident silently re-logged the event — now idempotent, with regression tests on both the authenticated route and the ntfy ack-button route). Relabel escalation re-pages so they don't look like the same page landing twice. Copy the primary action up near the top. Label the "···" button. Group the timeline by phase (triggered/acknowledged/resolved). Add an "at a glance" summary row (duration/severity/responsible) and collapse the group labels by default. - Team overview: reword the vague copy ("One owner." etc.) into plain labels. - Empty states: fill in missing icons/one-liners across queue, alerts, stats and the incident timeline. - CSS: fix card padding bugs, verify link contrast already passes AA, introduce a --fs-* type-scale token set and migrate the few genuinely isolated cases onto it (left sizes tied to a fixed shape, a deliberately prominent display, or a non-negotiable constraint like the iOS-zoom-prevention input size as documented exceptions rather than guess at a render this change can't see). Verified with the full fmt/lint/test/helm-lint gate, plus a live instance against the test DB with seeded incidents and schedule data to trace the on-call grouping and timeline phase-splitting logic against real API responses. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -338,6 +338,48 @@ func TestIncident_Acknowledge(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A second acknowledge — a retried request, or a stale push notification
|
||||
// tapped after the web UI already acked it — must be a no-op: same state,
|
||||
// no second "acknowledged" timeline entry. Regression test for the bug
|
||||
// described in issue #26 ("two acknowledged entries look like a bug").
|
||||
func TestIncident_AcknowledgeTwiceIsIdempotent(t *testing.T) {
|
||||
s := newTS(t)
|
||||
postWebhook(t, s, []map[string]any{
|
||||
amAlert("fp-ack2", "Y", "firing", "2026-05-20T10:00:00Z", zeroTime, nil),
|
||||
})
|
||||
|
||||
resp := s.req(t, http.MethodPost, "/api/incidents/1/acknowledge", nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("first acknowledge returned %d", resp.StatusCode)
|
||||
}
|
||||
var first map[string]any
|
||||
decode(t, resp, &first)
|
||||
|
||||
resp = s.req(t, http.MethodPost, "/api/incidents/1/acknowledge", nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("second acknowledge returned %d, want 200 (idempotent)", resp.StatusCode)
|
||||
}
|
||||
var second map[string]any
|
||||
decode(t, resp, &second)
|
||||
if second["status"] != "acknowledged" {
|
||||
t.Errorf("expected status still acknowledged, got %v", second["status"])
|
||||
}
|
||||
if second["acknowledged_by"] != first["acknowledged_by"] {
|
||||
t.Errorf("expected the same acknowledged_by, got %v then %v", first["acknowledged_by"], second["acknowledged_by"])
|
||||
}
|
||||
|
||||
types := eventTypes(timeline(t, s, 1))
|
||||
n := 0
|
||||
for _, ty := range types {
|
||||
if ty == "acknowledged" {
|
||||
n++
|
||||
}
|
||||
}
|
||||
if n != 1 {
|
||||
t.Errorf("expected exactly one acknowledged event, got %d in %v", n, types)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncident_ManualResolveIsTerminal(t *testing.T) {
|
||||
s := newTS(t)
|
||||
postWebhook(t, s, []map[string]any{
|
||||
|
||||
Reference in New Issue
Block a user