6235a2f737
The stats page could only say how many alerts fired. It now answers when
one alert fires, how often, and how fast it is picked up and fixed: each
row of the top-alerts list opens /stats/alert/{name}, with a timeline,
by-hour and by-weekday charts, time-to-resolve per occurrence and a table
of every occurrence linking to its incident. The overview gains a
weekday-by-hour heatmap and a few derived observations.
An occurrence is an incident, not an alert row. Alert rows are reused per
fingerprint and mutated in place, so they carry no history; incidents are
the only record of each time something fired and was answered. That means
/api/stats/alerts/top now counts incidents opened (filtered on
triggered_at) so the list and the drilldown agree. The response keeps name
and count and only adds fields, so existing clients keep working, but the
counts can differ from before.
No migration: the lookups use incidents_signature_idx. Label breakdown,
team and severity filters and period-over-period deltas from the design
draft are left for later.
Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
29 lines
602 B
Go
29 lines
602 B
Go
package web
|
|
|
|
import (
|
|
"io/fs"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestStatsPageIsEmbedded(t *testing.T) {
|
|
sub, err := fs.Sub(files, "static")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
index, err := fs.ReadFile(sub, "index.html")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{`id="view-stats"`, `data-section="stats"`} {
|
|
if !strings.Contains(string(index), want) {
|
|
t.Errorf("index.html lacks %s", want)
|
|
}
|
|
}
|
|
for _, f := range []string{"js/stats.js", "js/statsalert.js", "js/charts.js"} {
|
|
if _, err := fs.Stat(sub, f); err != nil {
|
|
t.Errorf("%s not embedded: %v", f, err)
|
|
}
|
|
}
|
|
}
|