package main import ( "crypto/sha256" "database/sql" "embed" "encoding/hex" "encoding/json" "fmt" "html/template" "io" "io/fs" "log" "net" "net/http" "net/smtp" "os" "path/filepath" "sort" "strings" "time" _ "github.com/mattn/go-sqlite3" ) //go:embed templates/*.tmpl var tmplFS embed.FS //go:embed *.svg *.css *.png var static embed.FS //go:embed fonts/*.woff2 var fontsFS embed.FS var templates *template.Template var devMode bool var db *sql.DB var processStartTime = time.Now().Unix() var countryNames = map[string]string{ "US": "United States", "CA": "Canada", "MX": "Mexico", "CO": "Colombia", "BR": "Brazil", "CL": "Chile", "GB": "United Kingdom", "CH": "Switzerland", "ES": "Spain", "SE": "Sweden", "TR": "Turkey", "AE": "UAE", "NG": "Nigeria", "KE": "Kenya", "ZA": "South Africa", "IN": "India", "SG": "Singapore", "AU": "Australia", "JP": "Japan", "KR": "South Korea", "HK": "Hong Kong", "NZ": "New Zealand", "KZ": "Kazakhstan", "BD": "Bangladesh", "PH": "Philippines", "TH": "Thailand", "TW": "Taiwan", "ID": "Indonesia", } func countryName(code string) string { if name, ok := countryNames[code]; ok { return name } return code } func fmtInt(n int) string { s := fmt.Sprintf("%d", n) if len(s) <= 3 { return s } var result []byte for i, c := range s { if i > 0 && (len(s)-i)%3 == 0 { result = append(result, ',') } result = append(result, byte(c)) } return string(result) } type Pop struct { PopID int City string Country string Lat float64 Lon float64 RegionName string IP string DNS string Status string Provider string CountryFull string BackupCity string BackupDistanceKM int BackupDistFmt string BackupDistMiFmt string } type PageData struct { Page string Title string Desc string ActiveNav string Pops []Pop PopsByCity []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, backup_city, backup_distance_km FROM pops ORDER BY CASE status WHEN 'live' THEN 0 ELSE 1 END, lon DESC") 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, &p.BackupCity, &p.BackupDistanceKM); err != nil { log.Printf("pops scan error: %v", err) continue } p.CountryFull = countryName(p.Country) p.BackupDistFmt = fmtInt(p.BackupDistanceKM) p.BackupDistMiFmt = fmtInt(int(float64(p.BackupDistanceKM) * 0.621371)) 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) } // classifyDomain asks an LLM to classify a domain into a scope category. // Called once per unknown domain. Result cached in SQLite forever. func classifyDomain(domain string) string { d := strings.ToLower(strings.TrimSpace(domain)) // Local IPs — no LLM needed if strings.HasPrefix(d, "192.168.") || strings.HasPrefix(d, "10.") || strings.HasPrefix(d, "172.16.") || strings.HasPrefix(d, "172.17.") || strings.HasPrefix(d, "172.18.") || strings.HasPrefix(d, "172.19.") || strings.HasPrefix(d, "172.2") || strings.HasPrefix(d, "172.3") || d == "localhost" || d == "127.0.0.1" { return "home" } // Ask LLM scope := classifyViaLLM(d) if scope != "" { return scope } return "misc" } // classifyBatch classifies all domains in one LLM call. // Returns map[domain]scope. Domains that fail get "misc". func classifyBatch(domains []string) map[string]string { result := make(map[string]string) // Handle local IPs without LLM var toLLM []string for _, d := range domains { if strings.HasPrefix(d, "192.168.") || strings.HasPrefix(d, "10.") || strings.HasPrefix(d, "172.") || d == "localhost" || d == "127.0.0.1" { result[d] = "home" } else { toLLM = append(toLLM, d) } } if len(toLLM) == 0 { return result } // Build domain list for prompt domainList := strings.Join(toLLM, "\n") prompt := fmt.Sprintf( `Classify each domain into exactly ONE category. Reply with ONLY lines in format: domain=category No explanations. One line per domain. Pick the single MOST specific category. Categories: finance, social, shopping, work, dev, email, media, health, travel, home, education, government Domains: %s`, domainList) reqBody, _ := json.Marshal(map[string]any{ "model": "anthropic/claude-haiku-4-5", "messages": []map[string]string{ {"role": "system", "content": "You classify internet domains. Reply ONLY with lines in format: domain=category. No explanations, no reasoning, no markdown."}, {"role": "user", "content": prompt}, }, "max_tokens": 4096, "temperature": 0, }) apiKey := os.Getenv("OPENROUTER_API_KEY") if apiKey == "" { log.Printf("classifyBatch: OPENROUTER_API_KEY not set") return result } req, err := http.NewRequest("POST", "https://openrouter.ai/api/v1/chat/completions", strings.NewReader(string(reqBody))) if err != nil { return result } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+apiKey) client := &http.Client{Timeout: 120 * time.Second} resp, err := client.Do(req) if err != nil { log.Printf("classifyBatch: LLM call error: %v", err) return result } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var llmResult struct { Choices []struct { Message struct { Content string `json:"content"` } `json:"message"` } `json:"choices"` } if err := json.Unmarshal(body, &llmResult); err != nil || len(llmResult.Choices) == 0 { snippet := string(body) if len(snippet) > 300 { snippet = snippet[:300] } log.Printf("classifyBatch: LLM error (status=%d): %s", resp.StatusCode, snippet) return result } // Parse response lines: "domain=scope1,scope2" content := llmResult.Choices[0].Message.Content log.Printf("classifyBatch: raw LLM response (%d chars): %s", len(content), content) // Strip thinking tags if idx := strings.Index(content, ""); idx >= 0 { content = content[idx+8:] } for _, line := range strings.Split(content, "\n") { line = strings.TrimSpace(line) // Try multiple separators: =, :, - var domain, scopes string for _, sep := range []string{"=", ": ", " - "} { parts := strings.SplitN(line, sep, 2) if len(parts) == 2 { domain = strings.TrimSpace(parts[0]) scopes = strings.TrimSpace(parts[1]) break } } if domain == "" || scopes == "" { continue } // Strip backticks, quotes, markdown domain = strings.Trim(domain, "`\"'*- ") scopes = strings.Trim(scopes, "`\"'*- ") // Validate each scope var valid []string for _, s := range strings.Split(scopes, ",") { s = strings.TrimSpace(s) if validScopes[s] { valid = append(valid, s) } } if len(valid) > 0 { result[domain] = strings.Join(valid, ",") } } log.Printf("classifyBatch: classified %d/%d domains", len(result), len(domains)) return result } var validScopes = map[string]bool{ "finance": true, "social": true, "shopping": true, "work": true, "dev": true, "email": true, "media": true, "health": true, "travel": true, "home": true, "education": true, "government": true, "misc": true, } func classifyViaLLM(domain string) string { prompt := fmt.Sprintf( `Classify this internet domain into one or more categories. Reply with ONLY the category names, comma-separated, nothing else. Pick the most specific ones. Use "misc" only if nothing else fits. Categories: finance, social, shopping, work, dev, email, media, health, travel, home, education, government Domain: %s`, domain) reqBody, _ := json.Marshal(map[string]any{ "model": "anthropic/claude-haiku-4-5", "messages": []map[string]string{ {"role": "user", "content": prompt}, }, "max_tokens": 200, "temperature": 0, }) req, err := http.NewRequest("POST", "https://openrouter.ai/api/v1/chat/completions", strings.NewReader(string(reqBody))) if err != nil { log.Printf("classify LLM req error: %v", err) return "" } req.Header.Set("Content-Type", "application/json") apiKey := os.Getenv("OPENROUTER_API_KEY") if apiKey == "" { log.Printf("classify: OPENROUTER_API_KEY not set") return "" } req.Header.Set("Authorization", "Bearer "+apiKey) client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { log.Printf("classify LLM call error: %v", err) return "" } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var result struct { Choices []struct { Message struct { Content string `json:"content"` } `json:"message"` } `json:"choices"` } if err := json.Unmarshal(body, &result); err != nil || len(result.Choices) == 0 { snippet := string(body) if len(snippet) > 500 { snippet = snippet[:500] } log.Printf("classify LLM error (status=%d): %s", resp.StatusCode, snippet) return "" } raw := strings.ToLower(strings.TrimSpace(result.Choices[0].Message.Content)) // Strip thinking tags if idx := strings.Index(raw, ""); idx >= 0 { raw = strings.TrimSpace(raw[idx+8:]) } // Validate each scope var valid []string for _, s := range strings.Split(raw, ",") { s = strings.TrimSpace(s) if validScopes[s] { valid = append(valid, s) } } if len(valid) == 0 { log.Printf("classify: %s → invalid response %q, defaulting to misc", domain, raw) return "misc" } scope := strings.Join(valid, ",") log.Printf("classify: %s → %s", domain, scope) return scope } 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() // Open corporate.db for onboarding (TLW). If it doesn't exist (e.g. dev // without admin running), the onboarding endpoints will return an error // but the rest of the site keeps working. if tlwErr := initTLW(); tlwErr != nil { log.Printf("TLW disabled: %v (onboarding endpoints will fail)", tlwErr) } else { defer corpDB.Close() } initOnboardingSecret() initOAuth() // Migrations db.Exec(`ALTER TABLE pops ADD COLUMN backup_city TEXT DEFAULT ''`) db.Exec(`ALTER TABLE pops ADD COLUMN backup_distance_km INTEGER DEFAULT 0`) db.Exec(`CREATE TABLE IF NOT EXISTS domain_scopes (domain TEXT PRIMARY KEY, scope TEXT NOT NULL, created_at INTEGER NOT NULL)`) port := os.Getenv("PORT") if port == "" { port = "8099" } // Domain classification — stateless, no logging, cached http.HandleFunc("/classify", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(204) return } if r.Method != "POST" { http.Error(w, `{"error":"POST required"}`, 405) return } var domains []string if err := json.NewDecoder(r.Body).Decode(&domains); err != nil || len(domains) == 0 { http.Error(w, `{"error":"expected JSON array of domains"}`, 400) return } if len(domains) > 5000 { domains = domains[:5000] } result := make(map[string]string) // Check cache first var uncached []string for _, d := range domains { d = strings.ToLower(strings.TrimSpace(d)) if d == "" { continue } // RFC1918 / local IPs → home if strings.HasPrefix(d, "192.168.") || strings.HasPrefix(d, "10.") || strings.HasPrefix(d, "172.") || d == "localhost" { result[d] = "home" continue } var scope string err := db.QueryRow("SELECT scope FROM domain_scopes WHERE domain = ?", d).Scan(&scope) if err == nil { result[d] = scope } else { uncached = append(uncached, d) } } // Classify uncached domains in chunks (max 200 per LLM call to stay within output token limits) if len(uncached) > 0 { chunkSize := 200 for i := 0; i < len(uncached); i += chunkSize { end := i + chunkSize if end > len(uncached) { end = len(uncached) } chunk := uncached[i:end] batch := classifyBatch(chunk) for _, d := range chunk { scope := batch[d] if scope == "" || scope == "misc" { result[d] = "misc" } else { result[d] = scope db.Exec("INSERT OR REPLACE INTO domain_scopes (domain, scope, created_at) VALUES (?, ?, ?)", d, scope, time.Now().Unix()) } } } log.Printf("classify: %d uncached domains in %d chunks, %d cached total", len(uncached), (len(uncached)+chunkSize-1)/chunkSize, len(result)) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(result) }) // API Key classification — stateless, masks keys for privacy http.HandleFunc("/classify-api-keys", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(204) return } if r.Method != "POST" { http.Error(w, `{"error":"POST required"}`, 405) return } var req struct { Items []struct { Masked string `json:"masked"` Note string `json:"note"` Title string `json:"title"` URLs []string `json:"urls"` Username string `json:"username"` } `json:"items"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil || len(req.Items) == 0 { http.Error(w, `{"error":"expected JSON with items array"}`, 400) return } // Build LLM prompt var itemLines []string for i, item := range req.Items { itemLines = append(itemLines, fmt.Sprintf("%d. masked=%s\n title=%s\n note=%s\n urls=%s\n username=%s", i+1, item.Masked, item.Title, item.Note, strings.Join(item.URLs, ","), item.Username)) } prompt := fmt.Sprintf(`You receive API key records from a password manager. Each record contains: - masked: The key with middle characters replaced by X - note: The full note field where the key was found - title: The password manager entry title - urls: Associated website URLs - username: Associated username or email Your task: Generate a concise, descriptive title for each API key (maximum 5 words). Use information from: 1. The entry title — primary service name 2. URL domain — secondary service confirmation 3. Note content — key type and purpose 4. Key prefix patterns — service indicators Guidelines: - Format: "{Service} {Type}" or "{Service} {Purpose} {Type}" - Distinguish multiple keys from the same entry by their context - Use specific type names: "API Key", "Access Token", "Secret Key", "Personal Token" - Default to "{title} API Key" if context is insufficient - NEVER include actual key values in output Items: %s Reply with ONLY a JSON array of titles in the same order: ["Title 1", "Title 2", ...]`, strings.Join(itemLines, "\n\n")) reqBody, _ := json.Marshal(map[string]any{ "model": "anthropic/claude-haiku-4-5", "messages": []map[string]string{ {"role": "system", "content": "You name API keys. Reply ONLY with a JSON array of short titles. No explanations, no markdown, no thinking tags."}, {"role": "user", "content": prompt}, }, "max_tokens": 2000, "temperature": 0, }) apiKey := os.Getenv("OPENROUTER_API_KEY") if apiKey == "" { log.Printf("classify-api-keys: OPENROUTER_API_KEY not set") http.Error(w, `{"error":"service unavailable"}`, 503) return } llmReq, err := http.NewRequest("POST", "https://openrouter.ai/api/v1/chat/completions", strings.NewReader(string(reqBody))) if err != nil { http.Error(w, `{"error":"internal error"}`, 500) return } llmReq.Header.Set("Content-Type", "application/json") llmReq.Header.Set("Authorization", "Bearer "+apiKey) client := &http.Client{Timeout: 120 * time.Second} resp, err := client.Do(llmReq) if err != nil { log.Printf("classify-api-keys: LLM call error: %v", err) http.Error(w, `{"error":"classification failed"}`, 502) return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var llmResult struct { Choices []struct { Message struct { Content string `json:"content"` } `json:"message"` } `json:"choices"` } if err := json.Unmarshal(body, &llmResult); err != nil || len(llmResult.Choices) == 0 { snippet := string(body) if len(snippet) > 300 { snippet = snippet[:300] } log.Printf("classify-api-keys: LLM error (status=%d): %s", resp.StatusCode, snippet) http.Error(w, `{"error":"classification failed"}`, 502) return } content := llmResult.Choices[0].Message.Content // Strip thinking tags if idx := strings.Index(content, ""); idx >= 0 { content = strings.TrimSpace(content[idx+8:]) } // Try to extract JSON array content = strings.TrimSpace(content) if !strings.HasPrefix(content, "[") { // Look for array in the response if start := strings.Index(content, "["); start >= 0 { if end := strings.LastIndex(content[start:], "]"); end >= 0 { content = content[start : start+end+1] } } } // Validate it's valid JSON var titles []string if err := json.Unmarshal([]byte(content), &titles); err != nil { log.Printf("classify-api-keys: failed to parse LLM response: %s", content) // Return fallback titles for _, item := range req.Items { titles = append(titles, item.Title+" API Key") } } // Ensure we have the right number of titles if len(titles) != len(req.Items) { log.Printf("classify-api-keys: mismatch - got %d titles for %d items", len(titles), len(req.Items)) // Pad with fallback titles for len(titles) < len(req.Items) { titles = append(titles, req.Items[len(titles)].Title+" API Key") } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(titles) }) // Entry context classification — pure proxy with prompt http.HandleFunc("/classify-entries", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(204) return } if r.Method != "POST" { http.Error(w, `{"error":"POST required"}`, 405) return } var req struct { Items []struct { Idx int `json:"idx"` Title string `json:"title"` Note string `json:"note"` URLs []string `json:"urls"` } `json:"items"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil || len(req.Items) == 0 { http.Error(w, `{"error":"expected items"}`, 400) return } // Build numbered list using provided idx values var lines []string for _, item := range req.Items { lines = append(lines, fmt.Sprintf("%d. title=%s | urls=%s | note=%s", item.Idx, item.Title, strings.Join(item.URLs, ","), item.Note)) } prompt := `Given password manager entries, infer for each: 1. Likely domain/URL 2. Scope category (finance, social, dev, shopping, work, email, media, health, travel, home, education, government, misc) 3. Descriptive names for any API keys mentioned (masked with #) Use the entry numbers provided as JSON keys. Return ONLY JSON: {"1012": {"urls": ["x.com"], "scope": "social", "apiTitles": {"sk-abc########": "X API Token"}}, ...} Entries: ` + strings.Join(lines, "\n") body, _ := json.Marshal(map[string]any{ "model": "anthropic/claude-haiku-4-5", "messages": []map[string]string{ {"role": "system", "content": "You classify entries and name API keys. Reply only with JSON."}, {"role": "user", "content": prompt}, }, "max_tokens": 2000, "temperature": 0, }) apiKey := os.Getenv("OPENROUTER_API_KEY") if apiKey == "" { http.Error(w, `{"error":"service unavailable"}`, 503) return } req2, _ := http.NewRequest("POST", "https://openrouter.ai/api/v1/chat/completions", strings.NewReader(string(body))) req2.Header.Set("Content-Type", "application/json") req2.Header.Set("Authorization", "Bearer "+apiKey) client := &http.Client{Timeout: 120 * time.Second} resp, err := client.Do(req2) if err != nil { http.Error(w, `{"error":"classification failed"}`, 502) return } defer resp.Body.Close() // Parse OpenRouter response and extract the JSON content var openRouterResp struct { Choices []struct { Message struct { Content string `json:"content"` } `json:"message"` } `json:"choices"` } if err := json.NewDecoder(resp.Body).Decode(&openRouterResp); err != nil || len(openRouterResp.Choices) == 0 { http.Error(w, `{"error":"classification failed"}`, 502) return } // Return the parsed JSON content directly w.Header().Set("Content-Type", "application/json") w.Write([]byte(openRouterResp.Choices[0].Message.Content)) }) 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() sorted := make([]Pop, len(data.Pops)) copy(sorted, data.Pops) sort.Slice(sorted, func(i, j int) bool { return sorted[i].City < sorted[j].City }) data.PopsByCity = sorted 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) { data := PageData{Page: "pricing-new", Title: "Pricing — clavitor", Desc: "From $12/year. Personal, Family, Team, Enterprise. Same vault, same encryption, every tier.", ActiveNav: "pricing"} data.Pops = loadPops() render(w, data) }) http.HandleFunc("/install-new", func(w http.ResponseWriter, r *http.Request) { data := PageData{Page: "install-new", Title: "Self-host Clavitor — clavitor", Desc: "One binary, no Docker, no dependencies. Self-host guide.", ActiveNav: "install"} data.Pops = loadPops() render(w, data) }) http.HandleFunc("/for/consumer", func(w http.ResponseWriter, r *http.Request) { data := PageData{Page: "for-consumer", Title: "Clavitor for Individuals — AI-native credential issuance", Desc: "Your AI agent needs your API keys — not your passport number. Scoped credential issuance for personal AI agents.", ActiveNav: "for-consumer"} data.Pops = loadPops() render(w, data) }) http.HandleFunc("/for/smb", func(w http.ResponseWriter, r *http.Request) { data := PageData{Page: "for-smb", Title: "Clavitor for Teams — credential issuance for AI agents", Desc: "Every employee will have an agent. Each agent needs credentials. Each credential needs boundaries. Scoped credential issuance for teams.", ActiveNav: "for-smb"} data.Pops = loadPops() render(w, data) }) http.HandleFunc("/for/mme", func(w http.ResponseWriter, r *http.Request) { data := PageData{Page: "for-mme", Title: "Clavitor for Mid-Market Enterprise — credential issuance at scale", Desc: "500+ employees, thousands of AI agents. SCIM provisioning, centralized audit, per-user pricing. Credential issuance that scales.", ActiveNav: "for-mme"} data.Pops = loadPops() render(w, data) }) http.HandleFunc("/for/enterprise", func(w http.ResponseWriter, r *http.Request) { data := PageData{Page: "for-enterprise", Title: "Clavitor Enterprise — credential issuance with compliance", Desc: "Your agents are smarter than your policies. Hardware-enforced credential boundaries. SOC 2, ISO 27001, five nines SLA.", ActiveNav: "for-enterprise"} data.Pops = loadPops() render(w, data) }) http.HandleFunc("/for/msp", func(w http.ResponseWriter, r *http.Request) { data := PageData{Page: "for-msp", Title: "Clavitor for MSPs — managed credential issuance", Desc: "Deploy credential issuance across your client base. Client-owned vaults, technician scoping, cross-client audit. Reseller compensation included.", ActiveNav: "for-msp"} data.Pops = loadPops() render(w, data) }) http.HandleFunc("/privacy", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "privacy", Title: "Privacy Policy — clavitor"}) }) http.HandleFunc("/cookies", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "cookies", Title: "Cookie 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"}) }) http.HandleFunc("/upgrade", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "upgrade", Title: "Upgrade to Clavitor — clavitor", ActiveNav: "upgrade"}) }) http.HandleFunc("/developers", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "developers", Title: "Developers — clavitor", ActiveNav: "developers"}) }) http.HandleFunc("/integrations/claude-code", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "claude-code", Title: "Clavitor + Claude Code — Secure credential access", ActiveNav: "integrations"}) }) http.HandleFunc("/integrations/codex", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "codex", Title: "Clavitor + OpenAI Codex — CLI integration", ActiveNav: "integrations"}) }) http.HandleFunc("/integrations/openclaw", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "openclaw", Title: "Clavitor + OpenClaw — Multi-agent credentials", ActiveNav: "integrations"}) }) http.HandleFunc("/integrations/openclaw/cn", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "openclaw-cn", Title: "Clavitor + OpenClaw — AI 智能体凭据管理", ActiveNav: "integrations"}) }) // 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("/onboarding/profile", func(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" { handleOnboardingProfile(w, r) return } st := getOnboardingState(r) if st == nil { http.Redirect(w, r, "/signup", http.StatusSeeOther) return } render(w, PageData{Page: "onboarding-profile", Title: "Tell us about yourself — clavitor"}) }) http.HandleFunc("/onboarding/plan", func(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" { handleOnboardingPlan(w, r) return } st := getOnboardingState(r) if st == nil || st.AddressID == "" { http.Redirect(w, r, "/signup", http.StatusSeeOther) return } render(w, PageData{Page: "onboarding-plan", Title: "Pick your plan — clavitor"}) }) http.HandleFunc("/onboarding/done", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "onboarding-done", Title: "Welcome — clavitor"}) }) http.HandleFunc("/onboarding/email", handleSignupEmail) // New designer onboarding flow (preview pages — backend not yet wired). http.HandleFunc("/onboarding/login", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "onboarding-login", Title: "Sign in — clavitor"}) }) http.HandleFunc("/onboarding/details", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "onboarding-details", Title: "Your details — clavitor"}) }) http.HandleFunc("/onboarding/product", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "onboarding-product", Title: "Pick your plan — clavitor"}) }) http.HandleFunc("/onboarding/terms", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "onboarding-terms", Title: "Review terms — clavitor"}) }) http.HandleFunc("/onboarding/checkout", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "onboarding-checkout", Title: "Checkout — clavitor"}) }) // OAuth start/callback for each provider for _, name := range []string{"google", "apple", "meta"} { http.HandleFunc("/auth/"+name+"/start", handleOAuthStart) http.HandleFunc("/auth/"+name+"/callback", handleOAuthCallback) } // Self-hosted fonts. Strong cache — woff2 files are content-addressable // in spirit (any change ships with a new binary build). http.HandleFunc("/fonts/", func(w http.ResponseWriter, r *http.Request) { name := strings.TrimPrefix(r.URL.Path, "/fonts/") if name == "" || strings.Contains(name, "/") || strings.Contains(name, "..") { http.NotFound(w, r) return } data, err := fontsFS.ReadFile("fonts/" + name) if err != nil { http.NotFound(w, r) return } w.Header().Set("Content-Type", "font/woff2") w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") w.Header().Set("Access-Control-Allow-Origin", "*") w.Write(data) }) 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 now := time.Now().Unix() serverAge := now - processStartTime 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) { // Normal: extend existing span db.Exec(`UPDATE uptime_spans SET end_at = ? WHERE id = ?`, now, spanID) } else if err == nil && serverAge < 60 { // Server just started — can't judge gaps yet, extend existing span log.Printf("SPAN EXTEND node=%s gap=%ds (server up %ds, too early to judge)", t.NodeID, now-spanEnd, serverAge) db.Exec(`UPDATE uptime_spans SET end_at = ? WHERE id = ?`, now, spanID) } else if !inMaint { // Genuine outage or first-ever span gapSeconds := now - spanEnd if err == nil { log.Printf("OUTAGE SPAN node=%s gap=%ds last_seen=%s resumed=%s prev_span_id=%d hostname=%s version=%s cpu=%.1f%% mem=%d/%dMB disk=%d/%dMB load=%.2f uptime=%ds", t.NodeID, gapSeconds, time.Unix(spanEnd, 0).UTC().Format(time.RFC3339), time.Unix(now, 0).UTC().Format(time.RFC3339), spanID, t.Hostname, t.Version, t.CPUPercent, t.MemUsedMB, t.MemTotalMB, t.DiskUsedMB, t.DiskTotalMB, t.Load1m, t.UptimeSeconds) } else { log.Printf("OUTAGE SPAN node=%s first_span=true hostname=%s version=%s", t.NodeID, t.Hostname, t.Version) } db.Exec(`INSERT INTO uptime_spans (node_id, start_at, end_at) VALUES (?, ?, ?)`, t.NodeID, now, now) // Alert via ntfy go func(nodeID, hostname string, gap int64, firstSpan bool) { title := fmt.Sprintf("Outage recovery: %s", nodeID) body := fmt.Sprintf("Node **%s** (%s) created new span after **%ds** gap", nodeID, hostname, gap) if firstSpan { title = fmt.Sprintf("New node online: %s", nodeID) body = fmt.Sprintf("Node **%s** (%s) first heartbeat — new span created", nodeID, hostname) } req, err := http.NewRequest("POST", "http://127.0.0.1:2586/clavitor-alerts", strings.NewReader(body)) if err != nil { log.Printf("OUTAGE SPAN ntfy error creating request: %v", err) return } req.Header.Set("Authorization", "Bearer tk_k120jegay3lugeqbr9fmpuxdqmzx5") req.Header.Set("Title", title) req.Header.Set("Markdown", "yes") req.Header.Set("Priority", "high") req.Header.Set("Tags", "warning") client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { log.Printf("OUTAGE SPAN ntfy error sending alert: %v", err) return } resp.Body.Close() log.Printf("OUTAGE SPAN ntfy alert sent for node=%s", nodeID) }(t.NodeID, t.Hostname, gapSeconds, err != nil) } // 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"` Country string `json:"Country"` 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, Country: countryName(p.Country), 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 } // Find when this node first came online (first span ever) var firstEver int64 db.QueryRow(`SELECT MIN(start_at) FROM uptime_spans WHERE node_id = ?`, nodeID).Scan(&firstEver) // If the node didn't exist yet on this day, no data if firstEver == 0 || firstEver >= deUnix { return -1 } // If the node came online partway through this day, start counting from then if firstEver > dsUnix { dsUnix = firstEver 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", ActiveNav: "status"} 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 = "down" if ts, ok := lastSeen[id]; ok { age := now.Unix() - ts if age < 150 { health = "operational" } 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: countryName(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) } } // Outages type Outage struct { ID int `json:"id"` StartAt string `json:"start_at"` EndAt string `json:"end_at"` NodeID string `json:"node_id"` Status string `json:"status"` Description string `json:"description"` } var outages []Outage oRows, _ := db.Query(`SELECT id, start_at, COALESCE(end_at,''), node_id, status, description FROM outages ORDER BY id DESC`) if oRows != nil { defer oRows.Close() for oRows.Next() { var o Outage oRows.Scan(&o.ID, &o.StartAt, &o.EndAt, &o.NodeID, &o.Status, &o.Description) outages = append(outages, o) } } overall := "All Systems Operational" if inMaintenance { overall = "Scheduled Maintenance" } else 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, "outages": outages, "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() } // Don't count time before the node first came online var firstEver int64 db.QueryRow(`SELECT MIN(start_at) FROM uptime_spans WHERE node_id = ?`, node).Scan(&firstEver) if firstEver > 0 && firstEver > dsUnix { dsUnix = firstEver } 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", ActiveNav: "glass"} data.Pops = loadPops() render(w, data) }) // Downloads API — list files with SHA-256 fingerprints http.HandleFunc("/downloads/api", func(w http.ResponseWriter, r *http.Request) { entries, err := os.ReadDir("downloads") if err != nil { http.Error(w, "[]", 500) return } type dlInfo struct { Name string `json:"name"` Size int64 `json:"size"` SHA256 string `json:"sha256"` } var files []dlInfo for _, e := range entries { if e.IsDir() || strings.HasPrefix(e.Name(), ".") { continue } info, err := e.Info() if err != nil { continue } data, err := os.ReadFile(filepath.Join("downloads", e.Name())) if err != nil { continue } h := sha256.Sum256(data) files = append(files, dlInfo{Name: e.Name(), Size: info.Size(), SHA256: hex.EncodeToString(h[:])}) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(files) }) // Downloads — direct file serving, no traversal http.HandleFunc("/download/", func(w http.ResponseWriter, r *http.Request) { name := strings.TrimPrefix(r.URL.Path, "/download/") if name == "" || strings.Contains(name, "/") || strings.Contains(name, "\\") || strings.Contains(name, "..") || strings.HasPrefix(name, ".") { http.NotFound(w, r) return } path := filepath.Join("downloads", name) info, err := os.Stat(path) if err != nil || info.IsDir() { http.NotFound(w, r) return } w.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"") http.ServeFile(w, r, path) }) // SEO http.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.Write([]byte("User-agent: *\nAllow: /\nDisallow: /noc\nDisallow: /telemetry\nDisallow: /glass\n\nSitemap: https://clavitor.ai/sitemap.xml\n")) }) http.HandleFunc("/sitemap.xml", func(w http.ResponseWriter, r *http.Request) { type PageInfo struct { Path string Priority string Change string } pages := []PageInfo{ {"/", "1.0", "weekly"}, {"/hosted", "0.9", "weekly"}, {"/pricing", "0.9", "weekly"}, {"/install", "0.8", "monthly"}, {"/integrations/claude-code", "0.7", "monthly"}, {"/integrations/codex", "0.7", "monthly"}, {"/integrations/openclaw", "0.7", "monthly"}, {"/integrations/openclaw/cn", "0.6", "monthly"}, {"/privacy", "0.3", "yearly"}, {"/terms", "0.3", "yearly"}, {"/sources", "0.3", "yearly"}, } lastmod := time.Now().Format("2006-01-02") w.Header().Set("Content-Type", "application/xml") xml := ` ` + "\n" for _, p := range pages { xml += fmt.Sprintf("https://clavitor.ai%s%s%s%s\n", p.Path, lastmod, p.Change, p.Priority) } xml += `` w.Write([]byte(xml)) }) http.HandleFunc("/security.txt", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.Write([]byte("Contact: mailto:security@clavitor.ai\nPreferred-Languages: en\n")) }) http.HandleFunc("/.well-known/security.txt", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.Write([]byte("Contact: mailto:security@clavitor.ai\nPreferred-Languages: en\n")) }) // Catch-all: index page at "/" or static files or .html redirects http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { data := 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."} data.Pops = loadPops() render(w, data) 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) } }