diff --git a/clavitor.com/CLAUDE.md b/clavitor.com/CLAUDE.md new file mode 100644 index 0000000..96bd6bb --- /dev/null +++ b/clavitor.com/CLAUDE.md @@ -0,0 +1,59 @@ +# Clavitor Website — clavitor.com + +## Architecture +- Go web server (`main.go`) with `go:embed` for templates, CSS, SVGs, PNGs +- Templates in `templates/*.tmpl`, single CSS in `clavitor.css` +- SQLite DB: `clavitor.db` (pops, telemetry, uptime, incidents, accounts, vaults, sessions) +- Dev mode: auto-detected when `templates/` dir exists on disk — reloads templates per request, but CSS/SVGs require rebuild (`go:embed`) +- Port 8099 + +## Build & Run +``` +CGO_ENABLED=1 go build -o clavitor-web . +./clavitor-web +``` +CSS and SVG changes require rebuild (embedded at compile time). Template changes reload in dev mode. + +## Brand & Design +- Light mode only. Single source of truth: `clavitor.css` +- Logo: the black square (`#0A0A0A`). favicon.svg = black square +- Colors: black `#0A0A0A` (brand), red `#DC2626` (accent), light red `#F5B7B7` (planned/secondary), grayscale +- No purple. No green (except inherited SVG diagrams). Red is the only accent. +- Square shapes for permanent UI elements. Circles only for transient animations (pulses, "You" dot) +- Fonts: Figtree (body), JetBrains Mono (code/monospace) +- No inline styles, no CSS in templates. Everything in clavitor.css. + +## Encryption Terminology +- **Vault Encryption** — whole vault at rest +- **Credential Encryption** — per-field, server-side (AI agents can read via MCP) +- **Identity Encryption** — per-field, client-side via WebAuthn PRF (Touch ID only, server cannot decrypt) +- Never use "sealed fields", "agent fields", "L1", "L2", "L3" + +## POPs (Points of Presence) +- Stored in `pops` table in clavitor.db — the single source of truth +- Map on /hosted is generated dynamically from DB via JavaScript +- Zürich = HQ, black dot, larger (11×11). Live POPs = red. Planned = light red. +- "You" visitor dot = circle (not square — "you" is not clavitor) + +## Key URLs +- `/hosted` — hosted product page with dynamic world map +- `/glass` — looking glass (latency from user's browser) +- `/noc?pin=250365` — NOC dashboard (telemetry, read-only, hardcoded PIN) +- `/telemetry` — POST endpoint for POP agent heartbeats (no auth) +- `/ping` — server-side TCP ping (for diagnostics) + +## Vault Binary +- Source: `~/dev/clavitor/clovis/clovis-vault/` +- Build for ARM64: `cd ~/dev/clavitor/clovis/clovis-vault && GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o clavitor-linux-arm64 ./cmd/clavitor` +- All POPs are ARM64 (AWS t4g.micro) +- Vault runs on port 1984 with TLS +- Has `/ping` endpoint (11 bytes, no DB, CORS via middleware) for looking glass +- Has `/health` endpoint (heavier, queries DB) + +## Providers +- AWS: most POPs (free tier t4g.micro) +- LightNode: Santiago, Bogotá, Manila, Dhaka +- ishosting: Istanbul, Almaty +- HostAfrica: Lagos, Nairobi +- Voyager NZ → switched to AWS for Auckland +- Rackmill: Perth diff --git a/clavitor.com/apple-touch-icon.png b/clavitor.com/apple-touch-icon.png new file mode 100644 index 0000000..e8860ec Binary files /dev/null and b/clavitor.com/apple-touch-icon.png differ diff --git a/clavitor.com/clavitor-logo.svg b/clavitor.com/clavitor-logo.svg new file mode 100644 index 0000000..ecb96fe --- /dev/null +++ b/clavitor.com/clavitor-logo.svg @@ -0,0 +1,4 @@ + + Clavitor + + diff --git a/clavitor.com/clavitor.db b/clavitor.com/clavitor.db index ff3b822..0b4d3e5 100644 Binary files a/clavitor.com/clavitor.db and b/clavitor.com/clavitor.db differ diff --git a/clavitor.com/favicon.svg b/clavitor.com/favicon.svg index 92d2303..ecb96fe 100644 --- a/clavitor.com/favicon.svg +++ b/clavitor.com/favicon.svg @@ -1,5 +1,4 @@ - - - v - 84 + + Clavitor + diff --git a/clavitor.com/main.go b/clavitor.com/main.go index 5649ebb..c2b1b2f 100644 --- a/clavitor.com/main.go +++ b/clavitor.com/main.go @@ -3,6 +3,7 @@ package main import ( "database/sql" "embed" + "encoding/json" "fmt" "html/template" "io" @@ -20,7 +21,7 @@ import ( //go:embed templates/*.tmpl var tmplFS embed.FS -//go:embed *.svg *.css +//go:embed *.svg *.css *.png var static embed.FS var templates *template.Template @@ -115,7 +116,7 @@ func main() { loadTemplates() var err error - db, err = sql.Open("sqlite3", "clavitor.db?mode=ro") + db, err = sql.Open("sqlite3", "clavitor.db") if err != nil { log.Fatalf("failed to open clavitor.db: %v", err) } @@ -170,6 +171,136 @@ func main() { http.HandleFunc("/styleguide", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "styleguide", Title: "clavitor — Styleguide"}) }) + // NOC telemetry ingest — agents POST here + http.HandleFunc("/telemetry", func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + w.WriteHeader(405) + return + } + var t struct { + 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"` + } + if err := json.NewDecoder(r.Body).Decode(&t); err != nil || t.NodeID == "" { + http.Error(w, `{"error":"bad payload"}`, 400) + return + } + 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) + // Update daily uptime + 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"` + Status string `json:"Status"` + } + var nodes []N + for _, p := range pops { + nodes = append(nodes, N{ID: 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) + }) + http.HandleFunc("/glass", func(w http.ResponseWriter, r *http.Request) { data := PageData{Page: "glass", Title: "Looking Glass — clavitor"} data.Pops = loadPops() diff --git a/clavitor.com/templates/base.tmpl b/clavitor.com/templates/base.tmpl index d65f83f..085ce95 100644 --- a/clavitor.com/templates/base.tmpl +++ b/clavitor.com/templates/base.tmpl @@ -6,6 +6,7 @@ {{.Title}} {{if .Desc}}{{end}} + @@ -18,7 +19,7 @@ diff --git a/clavitor.com/templates/index.tmpl b/clavitor.com/templates/index.tmpl index 1247065..2b7cac0 100644 --- a/clavitor.com/templates/index.tmpl +++ b/clavitor.com/templates/index.tmpl @@ -135,7 +135,7 @@
Credential Encryption

AI-readable

-

Encrypted at rest, decryptable by the vault server. Your AI agent reads these via MCP.

+

Encrypted at rest, decryptable by the vault server. Your AI agent accesses these via the CLI.

What this policy covers

-

This privacy policy applies to the hosted clavitor service at clavitor.com. If you self-host clavitor, your data never touches our servers and this policy doesn't apply to you — your privacy is entirely in your own hands.

+

This privacy policy applies to the hosted Clavitor service at clavitor.com. If you self-host Clavitor, your data never touches our servers and this policy doesn't apply to you — your privacy is entirely in your own hands.

Data we store

-

When you use hosted clavitor, we store:

+

When you use hosted Clavitor, we store: