package main import ( "database/sql" "fmt" "math/rand/v2" "os" "time" _ "github.com/mattn/go-sqlite3" ) func main() { if len(os.Args) < 2 { fmt.Fprintln(os.Stderr, "usage: backfill ") os.Exit(1) } db, err := sql.Open("sqlite3", os.Args[1]) if err != nil { fmt.Fprintf(os.Stderr, "open: %v\n", err) os.Exit(1) } defer db.Close() ids, err := loadSiteIDs(db) if err != nil { fmt.Fprintf(os.Stderr, "load site IDs: %v\n", err) os.Exit(1) } rng := rand.New(rand.NewPCG(42, 0)) now := time.Now().UTC() if err := backfillHistory(db, rng, now, ids); err != nil { fmt.Fprintf(os.Stderr, "history: %v\n", err) os.Exit(1) } if err := backfillStateChanges(db, now, ids); err != nil { fmt.Fprintf(os.Stderr, "state changes: %v\n", err) os.Exit(1) } if err := backfillLogs(db, now); err != nil { fmt.Fprintf(os.Stderr, "logs: %v\n", err) os.Exit(1) } if err := backfillNodes(db, now); err != nil { fmt.Fprintf(os.Stderr, "nodes: %v\n", err) os.Exit(1) } if err := backfillMaintenance(db, now, ids); err != nil { fmt.Fprintf(os.Stderr, "maintenance: %v\n", err) os.Exit(1) } var count int _ = db.QueryRow("SELECT COUNT(*) FROM check_history").Scan(&count) fmt.Printf("Backfill complete: %d check records\n", count) var token string if err := db.QueryRow("SELECT token FROM sites WHERE name='Nightly Backup'").Scan(&token); err == nil { fmt.Printf("PUSH_TOKEN=%s\n", token) } } func loadSiteIDs(db *sql.DB) (map[string]int, error) { rows, err := db.Query("SELECT id, name FROM sites") if err != nil { return nil, err } defer rows.Close() ids := make(map[string]int) for rows.Next() { var id int var name string if err := rows.Scan(&id, &name); err != nil { return nil, err } ids[name] = id } return ids, rows.Err() } type monitorProfile struct { name string minMs int maxMs int downFrom int // check index where DOWN starts (-1 = never) } func backfillHistory(db *sql.DB, rng *rand.Rand, now time.Time, ids map[string]int) error { profiles := []monitorProfile{ {"Nextcloud", 40, 80, -1}, {"Jellyfin", 80, 200, -1}, {"Home Assistant", 15, 45, -1}, {"Gitea", 40, 90, -1}, {"Traefik Dashboard", 5, 25, -1}, {"Vaultwarden", 50, 130, -1}, {"Personal Blog", 25, 65, -1}, {"Immich", 100, 280, -1}, // spikes handled below {"Auth Portal", 30, 70, 40}, // DOWN after check 40 {"Edge Router", 5, 15, -1}, // ping {"Postgres", 1, 5, -1}, // port {"DNS Primary", 10, 30, -1}, } tx, err := db.Begin() if err != nil { return err } defer func() { _ = tx.Rollback() }() stmt, err := tx.Prepare("INSERT INTO check_history (site_id, latency_ns, is_up, checked_at) VALUES (?, ?, ?, ?)") if err != nil { return err } defer stmt.Close() const total = 60 for _, p := range profiles { siteID, ok := ids[p.name] if !ok { continue } for i := 0; i < total; i++ { minutesAgo := (total - i) * 24 checkedAt := now.Add(-time.Duration(minutesAgo) * time.Minute) var latencyNs int64 isUp := true if p.downFrom >= 0 && i >= p.downFrom { latencyNs = 0 isUp = false } else { ms := p.minMs + rng.IntN(p.maxMs-p.minMs) if p.name == "Immich" && i%17 == 0 { ms = 250 + rng.IntN(100) } latencyNs = int64(ms) * 1_000_000 } if _, err := stmt.Exec(siteID, latencyNs, isUp, checkedAt.Format("2006-01-02 15:04:05")); err != nil { return err } } } return tx.Commit() } func backfillStateChanges(db *sql.DB, now time.Time, ids map[string]int) error { type sc struct { name string from string to string reason string at time.Time } changes := []sc{ {"Nextcloud", "UP", "DOWN", "read timeout", now.Add(-3 * 24 * time.Hour).Add(-5 * time.Minute)}, {"Nextcloud", "DOWN", "UP", "", now.Add(-3 * 24 * time.Hour)}, {"Jellyfin", "UP", "DOWN", "connection reset", now.Add(-18 * time.Hour).Add(-3 * time.Minute)}, {"Jellyfin", "DOWN", "UP", "", now.Add(-18 * time.Hour)}, {"Auth Portal", "UP", "DOWN", "connection refused", now.Add(-8 * time.Hour)}, {"Immich", "UP", "DOWN", "502 Bad Gateway", now.Add(-12 * time.Hour).Add(-8 * time.Minute)}, {"Immich", "DOWN", "UP", "", now.Add(-12 * time.Hour)}, } tx, err := db.Begin() if err != nil { return err } defer func() { _ = tx.Rollback() }() stmt, err := tx.Prepare("INSERT INTO state_changes (site_id, from_status, to_status, error_reason, changed_at) VALUES (?, ?, ?, ?, ?)") if err != nil { return err } defer stmt.Close() for _, c := range changes { siteID, ok := ids[c.name] if !ok { continue } if _, err := stmt.Exec(siteID, c.from, c.to, c.reason, c.at.Format("2006-01-02 15:04:05")); err != nil { return err } } return tx.Commit() } func backfillLogs(db *sql.DB, now time.Time) error { type logEntry struct { msg string at time.Time } logs := []logEntry{ {"[06:12] Monitor 'Auth Portal' confirmed DOWN: connection refused", now.Add(-8 * time.Hour)}, {"[06:12] Monitor 'Auth Portal' failed check 2/2", now.Add(-8*time.Hour - 30*time.Second)}, {"[06:11] Monitor 'Auth Portal' failed check 1/2", now.Add(-8*time.Hour - 60*time.Second)}, {"[12:33] Monitor 'Immich' recovered (was down 8m)", now.Add(-12 * time.Hour)}, {"[12:25] Monitor 'Immich' confirmed DOWN: 502 Bad Gateway", now.Add(-12*time.Hour - 8*time.Minute)}, {"[12:25] Monitor 'Immich' failed check 3/3", now.Add(-12*time.Hour - 8*time.Minute - 30*time.Second)}, {"[12:25] Monitor 'Immich' failed check 2/3", now.Add(-12*time.Hour - 8*time.Minute - 60*time.Second)}, {"[12:24] Monitor 'Immich' failed check 1/3", now.Add(-12*time.Hour - 9*time.Minute)}, {"[06:14] Monitor 'Jellyfin' recovered (was down 3m)", now.Add(-18 * time.Hour)}, {"[06:11] Monitor 'Jellyfin' confirmed DOWN: connection reset", now.Add(-18*time.Hour - 3*time.Minute)}, {"[06:11] Monitor 'Jellyfin' failed check 2/2", now.Add(-18*time.Hour - 3*time.Minute - 30*time.Second)}, {"[06:10] Monitor 'Jellyfin' failed check 1/2", now.Add(-18*time.Hour - 4*time.Minute)}, {"[23:45] SSL certificate for 'Personal Blog' expires in 42 days", now.Add(-28 * time.Hour)}, {"[08:00] Loaded check history from database", now.Add(-32*time.Hour - 30*time.Minute)}, {"[08:00] Engine RESUMED (Active)", now.Add(-32*time.Hour - 30*time.Minute - 5*time.Second)}, } tx, err := db.Begin() if err != nil { return err } defer func() { _ = tx.Rollback() }() stmt, err := tx.Prepare("INSERT INTO logs (message, created_at) VALUES (?, ?)") if err != nil { return err } defer stmt.Close() for _, l := range logs { if _, err := stmt.Exec(l.msg, l.at.Format("2006-01-02 15:04:05")); err != nil { return err } } return tx.Commit() } func backfillNodes(db *sql.DB, now time.Time) error { _, err := db.Exec( "INSERT OR REPLACE INTO nodes (id, name, region, last_seen, version) VALUES (?, ?, ?, ?, ?)", "node-1", "leader", "us-east", now.Format("2006-01-02 15:04:05"), "2026.05.1", ) return err } func backfillMaintenance(db *sql.DB, now time.Time, ids map[string]int) error { tx, err := db.Begin() if err != nil { return err } defer func() { _ = tx.Rollback() }() stmt, err := tx.Prepare("INSERT INTO maintenance_windows (monitor_id, title, description, type, start_time, end_time, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)") if err != nil { return err } defer stmt.Close() jellyfinID := ids["Jellyfin"] past := now.Add(-3 * 24 * time.Hour) if _, err := stmt.Exec(jellyfinID, "Jellyfin upgrade", "Upgrade to v10.10 + plugin updates", "maintenance", past.Format("2006-01-02 15:04:05"), past.Add(2*time.Hour).Format("2006-01-02 15:04:05"), "admin"); err != nil { return err } future := now.Add(2 * 24 * time.Hour) if _, err := stmt.Exec(0, "Network switch replacement", "Replacing core switch in rack 2", "maintenance", future.Format("2006-01-02 15:04:05"), future.Add(4*time.Hour).Format("2006-01-02 15:04:05"), "admin"); err != nil { return err } return tx.Commit() }