diff --git a/clavitor.com/account/account.db b/clavitor.com/account/account.db index 4d0f4d7..2b20838 100644 Binary files a/clavitor.com/account/account.db and b/clavitor.com/account/account.db differ diff --git a/clavitor.com/account/account.db-wal b/clavitor.com/account/account.db-wal deleted file mode 100644 index d700392..0000000 Binary files a/clavitor.com/account/account.db-wal and /dev/null differ diff --git a/clavitor.com/clavitor-web b/clavitor.com/clavitor-web index 9cf1957..b322726 100755 Binary files a/clavitor.com/clavitor-web and b/clavitor.com/clavitor-web differ diff --git a/clavitor.com/clavitor.css b/clavitor.com/clavitor.css index c251a8a..c59c308 100644 --- a/clavitor.com/clavitor.css +++ b/clavitor.com/clavitor.css @@ -149,7 +149,7 @@ code { font-size: 0.875em; } /* === CHECKLISTS === */ .checklist { list-style: none; } .checklist li { display: flex; align-items: flex-start; gap: 0.75rem; font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 0.75rem; } -.checklist li::before { content: ''; width: 1rem; height: 1rem; flex-shrink: 0; background: var(--brand-black); border-radius: 50%; margin-top: 0.125rem; clip-path: polygon(20% 50%, 40% 70%, 80% 25%, 85% 30%, 40% 80%, 15% 55%); } +.checklist li::before { content: ''; width: 0.5rem; height: 0.5rem; flex-shrink: 0; background: var(--brand-black); margin-top: 0.375rem; } .checklist.red li::before { background: var(--brand-red); } /* === BADGES === */ @@ -263,4 +263,70 @@ code { font-size: 0.875em; } #dc-grid { flex-direction: column; } .nav-links { gap: 0.75rem; font-size: 0.75rem; } .section { padding-top: 48px; padding-bottom: 48px; } + .glass-grid { grid-template-columns: 1fr; } +} + +/* ---- Looking Glass ---- */ +.glass-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1px; + background: var(--border); + border: 1px solid var(--border); + border-radius: 8px; + overflow: hidden; +} +.glass-pop { + background: var(--bg); + padding: 20px; + transition: background 0.15s; +} +.glass-pop:hover { background: var(--surface); } +.glass-live { border-left: 3px solid var(--brand-red); } +.glass-planned { border-left: 3px solid var(--border); } +.glass-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} +.glass-city { + font-weight: 700; + font-size: 0.9rem; + color: var(--text); +} +.glass-status { + font-family: 'JetBrains Mono', monospace; + font-size: 0.65rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + padding: 2px 8px; + border-radius: 3px; +} +.glass-status-live { background: var(--brand-red); color: #fff; } +.glass-status-planned { background: var(--surface); color: var(--muted); } +.glass-details { display: flex; flex-direction: column; gap: 6px; } +.glass-row { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.78rem; +} +.glass-key { color: var(--muted); } +.glass-val { color: var(--text); } +.glass-val.mono { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; } +.glass-muted { color: var(--muted); font-style: italic; } +.glass-fast { color: #16a34a; font-weight: 600; } +.glass-ok { color: #ca8a04; font-weight: 600; } +.glass-slow { color: var(--brand-red); font-weight: 600; } + +@media (max-width: 1024px) { + .glass-grid { grid-template-columns: repeat(3, 1fr); } +} +@media (max-width: 768px) { + .glass-grid { grid-template-columns: repeat(2, 1fr); } +} +@media (max-width: 480px) { + .glass-grid { grid-template-columns: 1fr; } } diff --git a/clavitor.com/clavitor.db b/clavitor.com/clavitor.db new file mode 100644 index 0000000..ff3b822 Binary files /dev/null and b/clavitor.com/clavitor.db differ diff --git a/clavitor.com/account/account.db-shm b/clavitor.com/clavitor.db-shm similarity index 97% rename from clavitor.com/account/account.db-shm rename to clavitor.com/clavitor.db-shm index a309f0b..0af8095 100644 Binary files a/clavitor.com/account/account.db-shm and b/clavitor.com/clavitor.db-shm differ diff --git a/clavitor.com/clavitor.db-wal b/clavitor.com/clavitor.db-wal new file mode 100644 index 0000000..6ef4f45 Binary files /dev/null and b/clavitor.com/clavitor.db-wal differ diff --git a/clavitor.com/go.mod b/clavitor.com/go.mod index cab2024..b3d312e 100644 --- a/clavitor.com/go.mod +++ b/clavitor.com/go.mod @@ -1,3 +1,5 @@ module github.com/clavitor/clavitor-web go 1.23.6 + +require github.com/mattn/go-sqlite3 v1.14.37 // indirect diff --git a/clavitor.com/go.sum b/clavitor.com/go.sum new file mode 100644 index 0000000..9c79a75 --- /dev/null +++ b/clavitor.com/go.sum @@ -0,0 +1,2 @@ +github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg= +github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= diff --git a/clavitor.com/main.go b/clavitor.com/main.go index 90539e3..5649ebb 100644 --- a/clavitor.com/main.go +++ b/clavitor.com/main.go @@ -1,14 +1,20 @@ package main import ( + "database/sql" "embed" + "fmt" "html/template" "io" "io/fs" "log" + "net" "net/http" "os" "strings" + "time" + + _ "github.com/mattn/go-sqlite3" ) //go:embed templates/*.tmpl @@ -19,12 +25,27 @@ 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() { @@ -36,6 +57,25 @@ func loadTemplates() { } } +func loadPops() []Pop { + rows, err := db.Query("SELECT pop_id, city, country, lat, lon, region_name, ip, dns, status, provider FROM pops ORDER BY pop_id") + 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() @@ -74,6 +114,13 @@ func main() { } loadTemplates() + var err error + db, err = sql.Open("sqlite3", "clavitor.db?mode=ro") + if err != nil { + log.Fatalf("failed to open clavitor.db: %v", err) + } + defer db.Close() + port := os.Getenv("PORT") if port == "" { port = "8099" @@ -81,8 +128,29 @@ func main() { 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) { - render(w, PageData{Page: "hosted", Title: "clavitor — Hosted", ActiveNav: "hosted"}) + 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"}) @@ -102,6 +170,11 @@ func main() { http.HandleFunc("/styleguide", func(w http.ResponseWriter, r *http.Request) { render(w, PageData{Page: "styleguide", Title: "clavitor — Styleguide"}) }) + 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) { diff --git a/clavitor.com/templates/base.tmpl b/clavitor.com/templates/base.tmpl index 22d7f9e..d65f83f 100644 --- a/clavitor.com/templates/base.tmpl +++ b/clavitor.com/templates/base.tmpl @@ -35,10 +35,12 @@ {{else if eq .Page "terms"}}{{template "terms" .}} {{else if eq .Page "sources"}}{{template "sources" .}} {{else if eq .Page "styleguide"}}{{template "styleguide" .}} +{{else if eq .Page "glass"}}{{template "glass" .}} {{end}} {{if ne .Page "styleguide"}}{{template "footer"}}{{end}} {{if eq .Page "index"}}{{template "index-script"}} -{{else if eq .Page "hosted"}}{{template "hosted-script"}} +{{else if eq .Page "hosted"}}{{template "hosted-script" .}} +{{else if eq .Page "glass"}}{{template "glass-script"}} {{end}} diff --git a/clavitor.com/templates/glass.tmpl b/clavitor.com/templates/glass.tmpl new file mode 100644 index 0000000..8302256 --- /dev/null +++ b/clavitor.com/templates/glass.tmpl @@ -0,0 +1,121 @@ +{{define "glass"}} +
+

Network

+

Looking Glass

+

{{len .Pops}} points of presence. Real-time status.

+
+ +
+ +
+
+ {{range .Pops}} +
+
+ {{.City}} + {{.Status}} +
+
+
+ Region + {{.RegionName}} +
+
+ Provider + {{.Provider}} +
+ {{if .IP}}
+ IPv4 + {{.IP}} +
{{end}} + {{if .DNS}}
+ DNS + {{.DNS}} +
{{end}} +
+ Latency + {{if eq .Status "live"}}—{{else}}Q2 2026{{end}} +
+
+
+ {{end}} +
+
+{{end}} + +{{define "glass-script"}} + +{{end}} diff --git a/clavitor.com/templates/hosted.tmpl b/clavitor.com/templates/hosted.tmpl index 241fc94..f67a203 100644 --- a/clavitor.com/templates/hosted.tmpl +++ b/clavitor.com/templates/hosted.tmpl @@ -2,107 +2,19 @@

clavitor hosted

-

Zero cache. Every request hits the vault. So the vault has to be close.

-

Clavitor never caches credentials — not in memory, not on disk, not anywhere. Every request is a fresh decrypt from the vault. That's the security model. To make it fast, we run across 22 regions on every continent. Your data lives where you choose. $20 $12/yr.

+

Zero cache. Every request hits the vault.

+

Clavitor never caches credentials — not in memory, not on disk, not anywhere. Every request is a fresh decrypt from the vault. That's the security model. To make it fast, we run {{len .Pops}} regions across every continent. Your data lives where you choose. $20 $12/yr.

-
+
- - - - Washington D.C. - - - - San Francisco - - - - Montréal - - - - Mexico City - - - - Bogotá - - - - São Paulo - - - - Santiago - - - - London - - - - - Zürich - - - - Madrid - - - - Stockholm - - - - Istanbul - - - - Dubai - - - - Lagos - - - - Nairobi - - - - Cape Town - - - - Mumbai - - - - Singapore - - - - Sydney - - - - Tokyo - - - - Seoul - - - - Hong Kong + CLAVITOR GLOBAL PRESENCE
-
+
@@ -136,8 +48,8 @@

Why Zürich

-

Sealed fields: jurisdiction irrelevant.
Agent fields: it isn't.

-

Sealed fields are protected by math — where the server sits doesn't matter. But agent fields live on a server in a jurisdiction. A US server is subject to the CLOUD Act. Zürich, Switzerland is subject to Swiss law — which does not cooperate with foreign government data requests. No backdoors. Both layers protected.

+

Identity Encryption: jurisdiction irrelevant.
Credential Encryption: it isn't.

+

Identity fields are protected by math — where the server sits doesn't matter. But Credential fields live on a server in a jurisdiction. A US server is subject to the CLOUD Act. Zürich, Switzerland is subject to Swiss law — which does not cooperate with foreign government data requests. No backdoors. Both layers protected.

Self-hosted · US

@@ -205,6 +117,8 @@