package main import ( "database/sql" "embed" "encoding/json" "fmt" "net/smtp" "html/template" "io" "io/fs" "log" "net" "net/http" "os" "strings" "time" _ "github.com/mattn/go-sqlite3" ) //go:embed templates/*.tmpl var tmplFS embed.FS //go:embed *.svg *.css *.png var static embed.FS var templates *template.Template var devMode bool var db *sql.DB type Pop struct { PopID int City string Country string Lat float64 Lon float64 RegionName string IP string DNS string Status string Provider string } type PageData struct { Page string Title string Desc string ActiveNav string Pops []Pop } func loadTemplates() { if devMode { templates = template.Must(template.ParseGlob("templates/*.tmpl")) } else { sub, _ := fs.Sub(tmplFS, "templates") templates = template.Must(template.ParseFS(sub, "*.tmpl")) } } func loadPops() []Pop { rows, err := db.Query("SELECT pop_id, city, country, lat, lon, region_name, ip, dns, status, provider FROM pops ORDER BY CASE status WHEN 'live' THEN 0 ELSE 1 END, lon") if err != nil { log.Printf("pops query error: %v", err) return nil } defer rows.Close() var pops []Pop for rows.Next() { var p Pop if err := rows.Scan(&p.PopID, &p.City, &p.Country, &p.Lat, &p.Lon, &p.RegionName, &p.IP, &p.DNS, &p.Status, &p.Provider); err != nil { log.Printf("pops scan error: %v", err) continue } pops = append(pops, p) } return pops } func render(w http.ResponseWriter, data PageData) { if devMode { loadTemplates() } w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := templates.ExecuteTemplate(w, "base.tmpl", data); err != nil { log.Printf("template error: %v", err) http.Error(w, "Internal error", 500) } } func geoHandler(w http.ResponseWriter, r *http.Request) { ip := r.Header.Get("X-Forwarded-For") if ip == "" { ip = r.RemoteAddr } if i := strings.LastIndex(ip, ":"); i >= 0 { ip = ip[:i] } ip = strings.Trim(ip, "[]") resp, err := http.Get("https://ipapi.co/" + ip + "/json/") if err != nil { http.Error(w, `{"error":"geo failed"}`, 502) return } defer resp.Body.Close() w.Header().Set("Content-Type", "application/json") io.Copy(w, resp.Body) } func main() { if _, err := os.Stat("templates"); err == nil { devMode = true log.Println("dev mode: templates loaded from disk") } loadTemplates() var err error db, err = sql.Open("sqlite3", "clavitor.db") if err != nil { log.Fatalf("failed to open clavitor.db: %v", err) } defer db.Close() port := os.Getenv("PORT") if port == "" { port = "8099" } http.HandleFunc("/geo", geoHandler) http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) { host := r.URL.Query().Get("host") if host == "" { http.Error(w, `{"error":"missing host"}`, 400) return } start := time.Now() conn, err := net.DialTimeout("tcp", host+":1984", 5*time.Second) if err != nil { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"error":"unreachable"}`)) return } conn.Close() ms := time.Since(start).Milliseconds() w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"ms":%d}`, ms) }) http.HandleFunc("/hosted", func(w http.ResponseWriter, r *http.Request) { data := PageData{Page: "hosted", Title: "clavitor — Hosted", ActiveNav: "hosted"} data.Pops = loadPops() render(w, data) }) http.HandleFunc("/install", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "install", Title: "Self-host — clavitor", Desc: "Self-host clavitor in 30 seconds. One binary, no dependencies.", ActiveNav: "install"}) }) http.HandleFunc("/pricing", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "pricing", Title: "Pricing — clavitor", Desc: "Free self-hosted or $12/year hosted (launch price). No tiers, no per-seat, no contact sales.", ActiveNav: "pricing"}) }) http.HandleFunc("/privacy", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "privacy", Title: "Privacy Policy — clavitor"}) }) http.HandleFunc("/terms", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "terms", Title: "Terms of Service — clavitor"}) }) http.HandleFunc("/sources", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "sources", Title: "Sources — clavitor"}) }) // Notify — sends signup interest email http.HandleFunc("/notify", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { w.WriteHeader(405) return } var req struct { Email string `json:"email"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Email == "" { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"error":"invalid email"}`)) return } smtpUser := os.Getenv("SMTP_USER") smtpPass := os.Getenv("SMTP_PASS") if smtpUser == "" || smtpPass == "" { log.Printf("notify: SMTP not configured, email from %s", req.Email) w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"ok":true}`)) return } go func() { msg := fmt.Sprintf("From: %s\r\nTo: johan@clavitor.ai\r\nSubject: Clavitor signup interest: %s\r\n\r\n%s wants to be notified when signups open.\r\n", smtpUser, req.Email, req.Email) auth := smtp.PlainAuth("", smtpUser, smtpPass, "smtp.protonmail.ch") if err := smtp.SendMail("smtp.protonmail.ch:587", auth, smtpUser, []string{"johan@clavitor.ai"}, []byte(msg)); err != nil { log.Printf("notify: smtp error: %v", err) } else { log.Printf("notify: sent for %s", req.Email) } }() w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"ok":true}`)) }) http.HandleFunc("/signup", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "signup", Title: "Sign up — clavitor"}) }) http.HandleFunc("/styleguide", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "styleguide", Title: "clavitor — Styleguide"}) }) // NOC telemetry ingest — agents POST here // Accepts both flat format (node_id, cpu_percent, ...) and nested vault format // (hostname, system.cpu_percent, vaults.count, ...) http.HandleFunc("/telemetry", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { w.WriteHeader(405) return } var t struct { // Flat fields (legacy/direct) NodeID string `json:"node_id"` Version string `json:"version"` Hostname string `json:"hostname"` UptimeSeconds int64 `json:"uptime_seconds"` CPUPercent float64 `json:"cpu_percent"` MemTotalMB int64 `json:"memory_total_mb"` MemUsedMB int64 `json:"memory_used_mb"` DiskTotalMB int64 `json:"disk_total_mb"` DiskUsedMB int64 `json:"disk_used_mb"` Load1m float64 `json:"load_1m"` VaultCount int `json:"vault_count"` VaultSizeMB float64 `json:"vault_size_mb"` VaultEntries int `json:"vault_entries"` Mode string `json:"mode"` // Nested fields (clovis-vault TelemetryPayload) System struct { OS string `json:"os"` Arch string `json:"arch"` CPUs int `json:"cpus"` CPUPercent float64 `json:"cpu_percent"` MemTotalMB int64 `json:"memory_total_mb"` MemUsedMB int64 `json:"memory_used_mb"` DiskTotalMB int64 `json:"disk_total_mb"` DiskUsedMB int64 `json:"disk_used_mb"` Load1m float64 `json:"load_1m"` } `json:"system"` Vaults struct { Count int `json:"count"` TotalSizeMB int64 `json:"total_size_mb"` TotalEntries int64 `json:"total_entries"` } `json:"vaults"` } if err := json.NewDecoder(r.Body).Decode(&t); err != nil { http.Error(w, `{"error":"bad payload"}`, 400) return } // Use hostname as node_id if node_id not provided if t.NodeID == "" { t.NodeID = t.Hostname } if t.NodeID == "" { http.Error(w, `{"error":"missing node_id or hostname"}`, 400) return } // Merge nested fields into flat fields if flat is zero if t.CPUPercent == 0 && t.System.CPUPercent != 0 { t.CPUPercent = t.System.CPUPercent } if t.MemTotalMB == 0 { t.MemTotalMB = t.System.MemTotalMB } if t.MemUsedMB == 0 { t.MemUsedMB = t.System.MemUsedMB } if t.DiskTotalMB == 0 { t.DiskTotalMB = t.System.DiskTotalMB } if t.DiskUsedMB == 0 { t.DiskUsedMB = t.System.DiskUsedMB } if t.Load1m == 0 { t.Load1m = t.System.Load1m } if t.VaultCount == 0 { t.VaultCount = int(t.Vaults.Count) } if t.VaultSizeMB == 0 { t.VaultSizeMB = float64(t.Vaults.TotalSizeMB) } if t.VaultEntries == 0 { t.VaultEntries = int(t.Vaults.TotalEntries) } db.Exec(`INSERT INTO telemetry (node_id, version, hostname, uptime_seconds, cpu_percent, memory_total_mb, memory_used_mb, disk_total_mb, disk_used_mb, load_1m, vault_count, vault_size_mb, vault_entries, mode) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, t.NodeID, t.Version, t.Hostname, t.UptimeSeconds, t.CPUPercent, t.MemTotalMB, t.MemUsedMB, t.DiskTotalMB, t.DiskUsedMB, t.Load1m, t.VaultCount, t.VaultSizeMB, t.VaultEntries, t.Mode) // Uptime span tracking: extend existing span or create new one // During maintenance: always extend, never create new (no false gaps) // Normal: gap threshold 60s (2x the 30s interval, allows one missed beat) now := time.Now().Unix() var inMaint bool db.QueryRow(`SELECT COUNT(*) > 0 FROM maintenance WHERE end_at IS NULL`).Scan(&inMaint) var spanID int64 var spanEnd int64 err = db.QueryRow(`SELECT id, end_at FROM uptime_spans WHERE node_id = ? ORDER BY end_at DESC LIMIT 1`, t.NodeID).Scan(&spanID, &spanEnd) if err == nil && (inMaint || (now-spanEnd) <= 60) { db.Exec(`UPDATE uptime_spans SET end_at = ? WHERE id = ?`, now, spanID) } else if !inMaint { db.Exec(`INSERT INTO uptime_spans (node_id, start_at, end_at) VALUES (?, ?, ?)`, t.NodeID, now, now) } // Legacy daily uptime (kept for backwards compat) today := time.Now().Format("2006-01-02") db.Exec(`INSERT OR REPLACE INTO uptime (node_id, date, status) VALUES (?, ?, 'operational')`, t.NodeID, today) w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"ok":true}`)) }) // NOC API — latest telemetry per node nocPin := func(r *http.Request) bool { return r.URL.Query().Get("pin") == "250365" } http.HandleFunc("/noc/api/telemetry", func(w http.ResponseWriter, r *http.Request) { if !nocPin(r) { http.NotFound(w, r); return } rows, err := db.Query(`SELECT t.node_id, t.received_at, t.version, t.hostname, t.uptime_seconds, t.cpu_percent, t.memory_total_mb, t.memory_used_mb, t.disk_total_mb, t.disk_used_mb, t.load_1m, t.vault_count, t.vault_size_mb, t.vault_entries, t.mode FROM telemetry t INNER JOIN (SELECT node_id, MAX(id) as max_id FROM telemetry GROUP BY node_id) latest ON t.id = latest.max_id`) if err != nil { http.Error(w, `{"error":"query failed"}`, 500) return } defer rows.Close() type Tel struct { NodeID string `json:"node_id"` ReceivedAt int64 `json:"received_at"` Version string `json:"version"` Hostname string `json:"hostname"` UptimeSec int64 `json:"uptime_seconds"` CPU float64 `json:"cpu_percent"` MemTotal int64 `json:"memory_total_mb"` MemUsed int64 `json:"memory_used_mb"` DiskTotal int64 `json:"disk_total_mb"` DiskUsed int64 `json:"disk_used_mb"` Load1m float64 `json:"load_1m"` VaultCount int `json:"vault_count"` VaultSizeMB float64 `json:"vault_size_mb"` VaultEntries int `json:"vault_entries"` Mode string `json:"mode"` } var list []Tel for rows.Next() { var t Tel rows.Scan(&t.NodeID, &t.ReceivedAt, &t.Version, &t.Hostname, &t.UptimeSec, &t.CPU, &t.MemTotal, &t.MemUsed, &t.DiskTotal, &t.DiskUsed, &t.Load1m, &t.VaultCount, &t.VaultSizeMB, &t.VaultEntries, &t.Mode) list = append(list, t) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{"telemetry": list}) }) http.HandleFunc("/noc/api/nodes", func(w http.ResponseWriter, r *http.Request) { if !nocPin(r) { http.NotFound(w, r); return } pops := loadPops() type N struct { ID string `json:"ID"` City string `json:"City"` Status string `json:"Status"` } var nodes []N for _, p := range pops { id := p.DNS if idx := strings.Index(id, "."); idx > 0 { id = id[:idx] // "use1.clavitor.ai" -> "use1" } if id == "" { id = p.City } nodes = append(nodes, N{ID: id, City: p.City, Status: p.Status}) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{"nodes": nodes}) }) http.HandleFunc("/noc/api/telemetry/history", func(w http.ResponseWriter, r *http.Request) { if !nocPin(r) { http.NotFound(w, r); return } node := r.URL.Query().Get("node") limit := r.URL.Query().Get("limit") if limit == "" { limit = "60" } rows, err := db.Query(`SELECT received_at, cpu_percent, memory_used_mb, memory_total_mb FROM telemetry WHERE node_id = ? ORDER BY id DESC LIMIT ?`, node, limit) if err != nil { http.Error(w, `{"error":"query failed"}`, 500) return } defer rows.Close() type H struct { TS int64 `json:"ts"` CPU float64 `json:"cpu"` MemUsed int64 `json:"mem_used_mb"` MemTotal int64 `json:"mem_total_mb"` } var hist []H for rows.Next() { var h H rows.Scan(&h.TS, &h.CPU, &h.MemUsed, &h.MemTotal) hist = append(hist, h) } // Reverse so oldest first for i, j := 0, len(hist)-1; i < j; i, j = i+1, j-1 { hist[i], hist[j] = hist[j], hist[i] } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{"history": hist}) }) // NOC dashboard — hardcoded PIN, read-only, not a security boundary http.HandleFunc("/noc", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("pin") != "250365" { http.NotFound(w, r) return } data := PageData{Page: "noc", Title: "NOC — clavitor"} data.Pops = loadPops() render(w, data) }) // --- Uptime rollup helper --- // Calculates uptime % for a node on a given date from spans + maintenance windows. // Caches result in uptime_daily. Only recalculates if date is today (ongoing) or not cached. rollupDay := func(nodeID, date string) float64 { // Check cache (skip today — always recalculate) today := time.Now().Format("2006-01-02") if date != today { var cached float64 if db.QueryRow(`SELECT uptime_pct FROM uptime_daily WHERE node_id = ? AND date = ?`, nodeID, date).Scan(&cached) == nil { return cached } } // Parse day boundaries dayStart, _ := time.Parse("2006-01-02", date) dayEnd := dayStart.Add(24 * time.Hour) dsUnix := dayStart.Unix() deUnix := dayEnd.Unix() // If day is in the future, return -1 (no data) if dsUnix > time.Now().Unix() { return -1 } // Cap end to now if today if deUnix > time.Now().Unix() { deUnix = time.Now().Unix() } totalSeconds := deUnix - dsUnix if totalSeconds <= 0 { return -1 } // Sum span overlap with this day var upSeconds int64 var hasSpans bool var lastSpanEnd int64 if rows, err := db.Query(`SELECT start_at, end_at FROM uptime_spans WHERE node_id = ? AND end_at > ? AND start_at < ? ORDER BY start_at`, nodeID, dsUnix, deUnix); err == nil { for rows.Next() { hasSpans = true var s, e int64 rows.Scan(&s, &e) if s < dsUnix { s = dsUnix } if e > deUnix { e = deUnix } if e > s { upSeconds += e - s } lastSpanEnd = e } rows.Close() } // If the trailing gap to now is within heartbeat interval, count it as up if lastSpanEnd > 0 && (deUnix-lastSpanEnd) <= 60 { upSeconds += deUnix - lastSpanEnd } // No spans at all for this day = no data (node didn't exist yet) if !hasSpans { return -1 } // Subtract maintenance windows from denominator var maintSeconds int64 if mRows, err := db.Query(`SELECT start_at, COALESCE(end_at, ?) FROM maintenance WHERE end_at IS NULL OR (end_at >= ? AND start_at <= ?)`, deUnix, dsUnix, deUnix); err == nil { for mRows.Next() { var s, e int64 mRows.Scan(&s, &e) if s < dsUnix { s = dsUnix } if e > deUnix { e = deUnix } if e > s { maintSeconds += e - s } } mRows.Close() } effectiveTotal := totalSeconds - maintSeconds if effectiveTotal <= 0 { effectiveTotal = 1 upSeconds = 1 } pct := float64(upSeconds) / float64(effectiveTotal) * 100 if pct > 100 { pct = 100 } // Cache (don't cache today since it changes) if date != today { db.Exec(`INSERT OR REPLACE INTO uptime_daily (node_id, date, up_seconds, total_seconds, uptime_pct) VALUES (?,?,?,?,?)`, nodeID, date, upSeconds, effectiveTotal, pct) } return pct } // --- Maintenance API --- http.HandleFunc("/noc/api/maintenance", func(w http.ResponseWriter, r *http.Request) { if !nocPin(r) { http.NotFound(w, r); return } w.Header().Set("Content-Type", "application/json") switch r.Method { case "POST": var req struct { Action string `json:"action"` // "start" or "stop" Reason string `json:"reason"` By string `json:"by"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"bad request"}`, 400) return } if req.Action == "start" { db.Exec(`INSERT INTO maintenance (reason, started_by) VALUES (?, ?)`, req.Reason, req.By) w.Write([]byte(`{"ok":true,"status":"maintenance started"}`)) } else if req.Action == "stop" { now := time.Now().Unix() db.Exec(`UPDATE maintenance SET end_at = ?, ended_by = ? WHERE end_at IS NULL`, now, req.By) w.Write([]byte(`{"ok":true,"status":"maintenance ended"}`)) } else { http.Error(w, `{"error":"action must be start or stop"}`, 400) } case "GET": rows, _ := db.Query(`SELECT id, start_at, end_at, reason, started_by, ended_by FROM maintenance ORDER BY id DESC LIMIT 20`) type M struct { ID int `json:"id"` StartAt int64 `json:"start_at"` EndAt *int64 `json:"end_at"` Reason string `json:"reason"` StartBy string `json:"started_by"` EndBy string `json:"ended_by"` } var list []M if rows != nil { defer rows.Close() for rows.Next() { var m M rows.Scan(&m.ID, &m.StartAt, &m.EndAt, &m.Reason, &m.StartBy, &m.EndBy) list = append(list, m) } } // Check if currently in maintenance var active bool db.QueryRow(`SELECT COUNT(*) > 0 FROM maintenance WHERE end_at IS NULL`).Scan(&active) json.NewEncoder(w).Encode(map[string]any{"active": active, "windows": list}) } }) // Public status page http.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { data := PageData{Page: "status", Title: "Status — clavitor"} data.Pops = loadPops() render(w, data) }) // Status API — public, no PIN needed http.HandleFunc("/status/api", func(w http.ResponseWriter, r *http.Request) { pops := loadPops() type DayUptime struct { Date string `json:"date"` Pct float64 `json:"pct"` // 0-100, -1 = no data } type NodeStatus struct { ID string `json:"id"` City string `json:"city"` Country string `json:"country"` Region string `json:"region"` Status string `json:"status"` Health string `json:"health"` Uptime []DayUptime `json:"uptime_90"` } // Get latest telemetry per node tRows, _ := db.Query(`SELECT t.node_id, t.received_at FROM telemetry t INNER JOIN (SELECT node_id, MAX(id) as max_id FROM telemetry GROUP BY node_id) latest ON t.id = latest.max_id`) lastSeen := map[string]int64{} if tRows != nil { defer tRows.Close() for tRows.Next() { var nid string var ts int64 tRows.Scan(&nid, &ts) lastSeen[nid] = ts } } // Build 90-day date list now := time.Now() var dates []string for i := 89; i >= 0; i-- { dates = append(dates, now.AddDate(0, 0, -i).Format("2006-01-02")) } // Check maintenance status var inMaintenance bool db.QueryRow(`SELECT COUNT(*) > 0 FROM maintenance WHERE end_at IS NULL`).Scan(&inMaintenance) var nodes []NodeStatus allOperational := true for _, p := range pops { id := p.DNS if idx := strings.Index(id, "."); idx > 0 { id = id[:idx] } if id == "" { id = p.City } health := "planned" if p.Status == "live" { health = "unknown" if ts, ok := lastSeen[id]; ok { age := now.Unix() - ts if age < 150 { health = "operational" } else if age < 600 { if inMaintenance { health = "maintenance" } else { health = "degraded" } } else { if inMaintenance { health = "maintenance" } else { health = "down" } } } if health != "operational" && health != "maintenance" { allOperational = false } } // Build 90-day uptime from spans uptime90 := make([]DayUptime, 90) for i, d := range dates { pct := rollupDay(id, d) uptime90[i] = DayUptime{Date: d, Pct: pct} } nodes = append(nodes, NodeStatus{ ID: id, City: p.City, Country: p.Country, Region: p.RegionName, Status: p.Status, Health: health, Uptime: uptime90, }) } // Get recent incidents type Incident struct { ID int `json:"id"` Title string `json:"title"` Status string `json:"status"` Date string `json:"date"` } var incidents []Incident iRows, _ := db.Query(`SELECT id, title, status, date FROM incidents ORDER BY id DESC LIMIT 10`) if iRows != nil { defer iRows.Close() for iRows.Next() { var inc Incident iRows.Scan(&inc.ID, &inc.Title, &inc.Status, &inc.Date) incidents = append(incidents, inc) } } overall := "All Systems Operational" if !allOperational { overall = "Some Systems Degraded" } w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") var lastBeat int64 db.QueryRow(`SELECT MAX(received_at) FROM telemetry`).Scan(&lastBeat) json.NewEncoder(w).Encode(map[string]any{ "overall": overall, "nodes": nodes, "incidents": incidents, "dates": dates, "last_heartbeat": lastBeat, }) }) // Status API — day spans for tooltip http.HandleFunc("/status/api/spans", func(w http.ResponseWriter, r *http.Request) { node := r.URL.Query().Get("node") date := r.URL.Query().Get("date") if node == "" || date == "" { http.Error(w, `{"error":"missing node or date"}`, 400) return } dayStart, _ := time.Parse("2006-01-02", date) dayEnd := dayStart.Add(24 * time.Hour) dsUnix := dayStart.Unix() deUnix := dayEnd.Unix() if deUnix > time.Now().Unix() { deUnix = time.Now().Unix() } type Span struct { Start int64 `json:"start"` End int64 `json:"end"` Type string `json:"type"` // "up" or "down" } var spans []Span rows, err := db.Query(`SELECT start_at, end_at FROM uptime_spans WHERE node_id = ? AND end_at > ? AND start_at < ? ORDER BY start_at`, node, dsUnix, deUnix) if err == nil { prev := dsUnix for rows.Next() { var s, e int64 rows.Scan(&s, &e) if s < dsUnix { s = dsUnix } if e > deUnix { e = deUnix } if s > prev { spans = append(spans, Span{Start: prev, End: s, Type: "down"}) } spans = append(spans, Span{Start: s, End: e, Type: "up"}) prev = e } rows.Close() // Only mark trailing gap as "down" if it's significant (>60s) // Gaps within heartbeat interval are just "not yet reported" if prev < deUnix && (deUnix-prev) > 60 { spans = append(spans, Span{Start: prev, End: deUnix, Type: "down"}) } else if prev < deUnix { // Extend last up span to now (within heartbeat window) if len(spans) > 0 && spans[len(spans)-1].Type == "up" { spans[len(spans)-1].End = deUnix } } } w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") json.NewEncoder(w).Encode(map[string]any{"spans": spans, "day_start": dsUnix, "day_end": deUnix}) }) http.HandleFunc("/glass", func(w http.ResponseWriter, r *http.Request) { data := PageData{Page: "glass", Title: "Looking Glass — clavitor"} data.Pops = loadPops() render(w, data) }) // Catch-all: index page at "/" or static files or .html redirects http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { render(w, PageData{Page: "index", Title: "clavitor — AI-native password manager", Desc: "Field-level encryption for password managers that live alongside AI assistants. Your AI gets what it needs. Your secrets stay yours."}) return } // Redirect old .html URLs to clean paths if strings.HasSuffix(r.URL.Path, ".html") { clean := strings.TrimSuffix(r.URL.Path, ".html") if clean == "/index" { clean = "/" } http.Redirect(w, r, clean, http.StatusMovedPermanently) return } http.FileServer(http.FS(static)).ServeHTTP(w, r) }) log.Printf("clavitor-web starting on :%s", port) if err := http.ListenAndServe(":"+port, nil); err != nil { log.Fatal(err) } }