Show 90-day uptime bars on the status page

Every check is also counted in a per-day table that outlives the raw checks, so the public page
can draw one bar per UTC day with a tooltip, as most hosted status pages do. Existing databases
are backfilled once from the checks they still have.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
VeillaxandClaude Sonnet 5.5 committed 2026-10-27 04:22:54 -06:00
1 parent 73f45fb3ae
commit 4ea4fe5289
7 files changed
+221 -14

No files matched your search

+34
View File
@@ -233,6 +233,37 @@ func (s *Server) monitorState(id string) MonitorState {
return st
}
// uptimeDays is how many days of bars the status page shows.
const uptimeDays = 90
// dailyUptime returns [checks, failed] for each of the last n UTC days, oldest first, so the
// last entry is today. A day with no checks is [0, 0].
func (s *Server) dailyUptime(monitorID string, n int, now time.Time) [][2]int {
out := make([][2]int, n)
today := now.UTC().Truncate(24 * time.Hour)
rows, err := s.st.db.Query(`SELECT day,checks,fails FROM daily WHERE monitor_id=? AND day>=?`,
monitorID, today.AddDate(0, 0, -(n-1)).Format("2006-01-02"))
if err != nil {
return out
}
defer rows.Close()
for rows.Next() {
var day string
var c, f int
if rows.Scan(&day, &c, &f) != nil {
continue
}
d, err := time.Parse("2006-01-02", day)
if err != nil {
continue
}
if i := n - 1 - int(today.Sub(d).Hours()/24); i >= 0 && i < n {
out[i] = [2]int{c, f}
}
}
return out
}
func (s *Server) saveState(st MonitorState) {
s.st.db.Exec(`INSERT INTO mstate(monitor_id,status,since,last_check,last_ms,fails,last_ping,detail) VALUES(?,?,?,?,?,?,?,?)
ON CONFLICT(monitor_id) DO UPDATE SET status=excluded.status, since=excluded.since, last_check=excluded.last_check,
@@ -309,6 +340,8 @@ func (s *Server) recordResult(m *Monitor, st MonitorState, now time.Time, dur ti
okInt = 1
}
s.st.db.Exec(`INSERT INTO checks(monitor_id,time,ok,ms,detail) VALUES(?,?,?,?,?)`, m.ID, now.Unix(), okInt, dur.Milliseconds(), errText(err))
s.st.db.Exec(`INSERT INTO daily(monitor_id,day,checks,fails) VALUES(?,?,1,?)
ON CONFLICT(monitor_id,day) DO UPDATE SET checks=checks+1, fails=fails+excluded.fails`, m.ID, now.UTC().Format("2006-01-02"), 1-okInt)
st.LastCheck, st.LastMS, st.Detail = now.Unix(), int(dur.Milliseconds()), errText(err)
key := "monitor:" + m.ID
if ok {
@@ -369,6 +402,7 @@ func (s *Server) Heartbeat(token string) bool {
func (s *Server) deleteMonitorData(id string) {
s.st.db.Exec(`DELETE FROM mstate WHERE monitor_id=?`, id)
s.st.db.Exec(`DELETE FROM checks WHERE monitor_id=?`, id)
s.st.db.Exec(`DELETE FROM daily WHERE monitor_id=?`, id)
s.ResolveAlertKey("monitor:"+id, "monitor deleted")
}