From 12824ddbef69059d5bf0439bed9f3211f774b0ec Mon Sep 17 00:00:00 2001 From: James Date: Thu, 9 Apr 2026 03:33:54 -0400 Subject: [PATCH 1/3] api: add /api/currencies endpoint for currency dropdown Implements currency API per issue #11 requirements: - Returns top 10 currencies (USD, EUR, GBP, JPY, CAD, AUD, CHF, SEK, NOK, NZD) - Returns all remaining active currencies alphabetically - Queries corporate.db currencies table - Proper error handling with unique error codes (ERR-ADMIN-001, ERR-ADMIN-002) - CORS enabled for frontend access fixes #11 Author: Emma --- clavitor.ai/main.go | 69 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/clavitor.ai/main.go b/clavitor.ai/main.go index e87ac95..7bbf6fc 100644 --- a/clavitor.ai/main.go +++ b/clavitor.ai/main.go @@ -1104,6 +1104,75 @@ Entries: w.Write([]byte(`{"ok":true}`)) }) + // Currencies API — returns top 10 + all currencies for pricing display + http.HandleFunc("/api/currencies", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + + if corpDB == nil { + w.WriteHeader(http.StatusServiceUnavailable) + json.NewEncoder(w).Encode(map[string]any{ + "error": "ERR-ADMIN-001: Currency data unavailable — corporate DB not connected", + }) + return + } + + // Top currencies to display first + topCodes := []string{"USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF", "SEK", "NOK", "NZD"} + topMap := make(map[string]bool) + for _, code := range topCodes { + topMap[code] = true + } + + rows, err := corpDB.Query(`SELECT code, name, symbol, symbol_position FROM currencies WHERE is_active = 1 ORDER BY code`) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]any{ + "error": "ERR-ADMIN-002: Failed to query currencies — " + err.Error(), + }) + return + } + defer rows.Close() + + type Currency struct { + Code string `json:"code"` + Name string `json:"name"` + Symbol string `json:"symbol"` + SymbolPosition string `json:"symbol_position"` + } + + var top []Currency + var all []Currency + + for rows.Next() { + var c Currency + if err := rows.Scan(&c.Code, &c.Name, &c.Symbol, &c.SymbolPosition); err != nil { + continue // Skip malformed rows + } + if topMap[c.Code] { + top = append(top, c) + } else { + all = append(all, c) + } + } + + // Ensure top currencies are in the defined order + orderedTop := make([]Currency, 0, len(topCodes)) + for _, code := range topCodes { + for _, c := range top { + if c.Code == code { + orderedTop = append(orderedTop, c) + break + } + } + } + + json.NewEncoder(w).Encode(map[string]any{ + "top": orderedTop, + "all": all, + }) + }) + // NOC API — latest telemetry per node nocPin := func(r *http.Request) bool { return r.URL.Query().Get("pin") == "250365" } From c082f8410988cee58b294e51ebb0eef925a106f7 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 9 Apr 2026 03:35:27 -0400 Subject: [PATCH 2/3] test: Add comprehensive error handling tests for clavis-telemetry Adds test coverage for issues #002, #003, #004: - TestUpdateSpan_MaintenanceModeError: Verifies ERR-TELEMETRY-010 logging - TestUpdateSpan_UptimeSpanQueryError: Verifies ERR-TELEMETRY-011 logging - TestSendKumaPush_DatabaseError: Verifies ERR-TELEMETRY-033 logging - TestSendKumaPush_NetworkError: Verifies ERR-TELEMETRY-030 logging - TestSendKumaPush_NonOKStatus: Verifies ERR-TELEMETRY-031 logging - TestTarpit_EarlyDisconnect: Verifies tarpit handles client disconnect - TestErrorCodes_Unique: Verifies all error codes are unique (caught duplicates!) - TestErrorCodes_Format: Verifies error message format compliance - TestHandleTelemetry_DatabaseInsertError: Verifies ERR-TELEMETRY-004 - TestConcurrentErrorHandling: Verifies thread-safe error logging Also fixes duplicate error codes found by tests: - ERR-TELEMETRY-010 -> ERR-TELEMETRY-015 for maintenance table creation - Added ERR-TELEMETRY-034 for Kuma response body close after non-OK status fixes #002 fixes #003 fixes #004 Author: Shakib --- .../clavis-telemetry/error_handling_test.go | 581 ++++++++++++++++++ clavis/clavis-telemetry/kuma.go | 2 +- clavis/clavis-telemetry/main.go | 2 +- 3 files changed, 583 insertions(+), 2 deletions(-) create mode 100644 clavis/clavis-telemetry/error_handling_test.go diff --git a/clavis/clavis-telemetry/error_handling_test.go b/clavis/clavis-telemetry/error_handling_test.go new file mode 100644 index 0000000..43640a8 --- /dev/null +++ b/clavis/clavis-telemetry/error_handling_test.go @@ -0,0 +1,581 @@ +//go:build commercial + +package main + +import ( + "bytes" + "database/sql" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/http/httptest" + "os" + "strings" + "sync" + "testing" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +// logCapture captures log output for verification +type logCapture struct { + original *os.File + reader *os.File + writer *os.File + buffer chan string + done chan bool +} + +func captureLogs() *logCapture { + // Save original stderr + original := os.Stderr + + // Create pipe to capture log output + r, w, _ := os.Pipe() + + // Redirect log output + os.Stderr = w + log.SetOutput(w) + + lc := &logCapture{ + original: original, + reader: r, + writer: w, + buffer: make(chan string, 100), + done: make(chan bool), + } + + // Start goroutine to read from pipe + go func() { + var buf bytes.Buffer + io.Copy(&buf, r) + lc.buffer <- buf.String() + close(lc.buffer) + lc.done <- true + }() + + return lc +} + +func (lc *logCapture) restore() string { + // Restore original stderr + os.Stderr = lc.original + log.SetOutput(lc.original) + + // Close writer to signal EOF to reader + lc.writer.Close() + + // Wait for reader goroutine to finish + <-lc.done + + // Return captured output + return <-lc.buffer +} + +// TestUpdateSpan_DatabaseErrors verifies ERR-TELEMETRY-010 through ERR-TELEMETRY-014 +func TestUpdateSpan_DatabaseErrors(t *testing.T) { + tests := []struct { + name string + setupDB func(*sql.DB) + expectedErrors []string + description string + }{ + { + name: "ERR-TELEMETRY-010: Maintenance check fails", + setupDB: func(testDB *sql.DB) { + // Drop maintenance table to cause query failure + testDB.Exec(`DROP TABLE IF EXISTS maintenance`) + }, + expectedErrors: []string{"ERR-TELEMETRY-010"}, + description: "Should log ERR-TELEMETRY-010 when maintenance check fails", + }, + { + name: "ERR-TELEMETRY-011: Uptime span query fails", + setupDB: func(testDB *sql.DB) { + // Drop uptime_spans table to cause query failure + testDB.Exec(`DROP TABLE IF EXISTS uptime_spans`) + }, + expectedErrors: []string{"ERR-TELEMETRY-011"}, + description: "Should log ERR-TELEMETRY-011 when uptime span query fails", + }, + { + name: "ERR-TELEMETRY-012: Span extend fails", + setupDB: func(testDB *sql.DB) { + // Insert a span but make the table read-only by corrupting it + testDB.Exec(`INSERT INTO uptime_spans (node_id, start_at, end_at) VALUES (?, ?, ?)`, + "test-node-err", time.Now().Unix()-30, time.Now().Unix()-30) + // Now drop and recreate to cause update to reference non-existent row + testDB.Exec(`DELETE FROM uptime_spans WHERE node_id = ?`, "test-node-err") + }, + expectedErrors: []string{}, // May or may not trigger depending on timing + description: "Database error handling for span extend", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup test database + setupTestDB(t) + defer cleanupTestDB() + + // Apply database corruption/setup + tt.setupDB(db) + + // Capture logs + lc := captureLogs() + + // Call updateSpan which should trigger error logging + updateSpan("test-node-err", "test-host", "1.0.0", 50.0, 4096, 8192, 50000, 100000, 0.5, 3600) + + // Restore and get logs + logs := lc.restore() + + // Verify expected errors appear in logs + for _, errCode := range tt.expectedErrors { + if !strings.Contains(logs, errCode) { + t.Errorf("%s: expected log to contain %s, but got:\n%s", tt.description, errCode, logs) + } + } + }) + } +} + +// TestUpdateSpan_MaintenanceModeError specifically tests ERR-TELEMETRY-010 +func TestUpdateSpan_MaintenanceModeError(t *testing.T) { + setupTestDB(t) + defer cleanupTestDB() + + // Capture logs + var logBuffer bytes.Buffer + originalOutput := log.Writer() + log.SetOutput(&logBuffer) + defer log.SetOutput(originalOutput) + + // Corrupt the database by dropping the maintenance table + _, err := db.Exec(`DROP TABLE maintenance`) + if err != nil { + t.Fatalf("Failed to drop maintenance table: %v", err) + } + + // Call updateSpan + updateSpan("test-node-maint", "test-host", "1.0.0", 50.0, 4096, 8192, 50000, 100000, 0.5, 3600) + + // Verify ERR-TELEMETRY-010 appears in logs + logs := logBuffer.String() + if !strings.Contains(logs, "ERR-TELEMETRY-010") { + t.Errorf("Expected ERR-TELEMETRY-010 in logs when maintenance check fails, got:\n%s", logs) + } + if !strings.Contains(logs, "Failed to check maintenance mode") { + t.Errorf("Expected 'Failed to check maintenance mode' message, got:\n%s", logs) + } +} + +// TestUpdateSpan_UptimeSpanQueryError specifically tests ERR-TELEMETRY-011 +func TestUpdateSpan_UptimeSpanQueryError(t *testing.T) { + setupTestDB(t) + defer cleanupTestDB() + + // Capture logs + var logBuffer bytes.Buffer + originalOutput := log.Writer() + log.SetOutput(&logBuffer) + defer log.SetOutput(originalOutput) + + // Corrupt the database by making it read-only via closing and reopening with bad path + db.Close() + var err error + db, err = sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("Failed to reopen database: %v", err) + } + // Don't create tables - queries will fail + + // Call updateSpan + updateSpan("test-node-span", "test-host", "1.0.0", 50.0, 4096, 8192, 50000, 100000, 0.5, 3600) + + // Verify ERR-TELEMETRY-011 appears in logs + logs := logBuffer.String() + if !strings.Contains(logs, "ERR-TELEMETRY-011") { + t.Errorf("Expected ERR-TELEMETRY-011 in logs when uptime span query fails, got:\n%s", logs) + } + if !strings.Contains(logs, "Failed to query latest uptime span") { + t.Errorf("Expected 'Failed to query latest uptime span' message, got:\n%s", logs) + } +} + +// TestSendKumaPush_DatabaseError tests ERR-TELEMETRY-033 +func TestSendKumaPush_DatabaseError(t *testing.T) { + // Setup test database then corrupt it + var err error + db, err = sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("Failed to open test database: %v", err) + } + defer db.Close() + // Don't create tables - this will cause queries to fail + + // Capture logs + var logBuffer bytes.Buffer + originalOutput := log.Writer() + log.SetOutput(&logBuffer) + defer log.SetOutput(originalOutput) + + // Create a test server that accepts Kuma pushes + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + // Call sendKumaPush + sendKumaPush(server.URL) + + // Verify ERR-TELEMETRY-033 appears in logs (failed to query last telemetry) + logs := logBuffer.String() + if !strings.Contains(logs, "ERR-TELEMETRY-033") { + t.Errorf("Expected ERR-TELEMETRY-033 in logs when telemetry timestamp query fails, got:\n%s", logs) + } +} + +// TestSendKumaPush_NetworkError tests ERR-TELEMETRY-030 +func TestSendKumaPush_NetworkError(t *testing.T) { + setupTestDB(t) + defer cleanupTestDB() + + // Capture logs + var logBuffer bytes.Buffer + originalOutput := log.Writer() + log.SetOutput(&logBuffer) + defer log.SetOutput(originalOutput) + + // Use an invalid URL that will cause network error + invalidURL := "http://localhost:1/invalid" // Port 1 is typically invalid/unused + + // Call sendKumaPush with invalid URL + sendKumaPush(invalidURL) + + // Verify ERR-TELEMETRY-030 appears in logs + logs := logBuffer.String() + if !strings.Contains(logs, "ERR-TELEMETRY-030") { + t.Errorf("Expected ERR-TELEMETRY-030 in logs when Kuma push fails, got:\n%s", logs) + } + if !strings.Contains(logs, "Failed to push health status to Kuma") { + t.Errorf("Expected 'Failed to push health status to Kuma' message, got:\n%s", logs) + } +} + +// TestSendKumaPush_NonOKStatus tests ERR-TELEMETRY-031 +func TestSendKumaPush_NonOKStatus(t *testing.T) { + setupTestDB(t) + defer cleanupTestDB() + + // Capture logs + var logBuffer bytes.Buffer + originalOutput := log.Writer() + log.SetOutput(&logBuffer) + defer log.SetOutput(originalOutput) + + // Create a test server that returns non-OK status + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) // 503 + w.Write([]byte("Service Unavailable")) + })) + defer server.Close() + + // Call sendKumaPush + sendKumaPush(server.URL) + + // Verify ERR-TELEMETRY-031 appears in logs + logs := logBuffer.String() + if !strings.Contains(logs, "ERR-TELEMETRY-031") { + t.Errorf("Expected ERR-TELEMETRY-031 in logs when Kuma returns non-OK status, got:\n%s", logs) + } + if !strings.Contains(logs, "non-OK status 503") { + t.Errorf("Expected 'non-OK status 503' message, got:\n%s", logs) + } +} + +// TestSendKumaPush_ResponseBodyCloseError tests response body handling +func TestSendKumaPush_ResponseBodyCloseError(t *testing.T) { + setupTestDB(t) + defer cleanupTestDB() + + // Capture logs + var logBuffer bytes.Buffer + originalOutput := log.Writer() + log.SetOutput(&logBuffer) + defer log.SetOutput(originalOutput) + + // Create a test server that returns OK + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + // Call sendKumaPush + sendKumaPush(server.URL) + + // Should complete without ERR-TELEMETRY-032 (successful close doesn't log error) + logs := logBuffer.String() + if strings.Contains(logs, "ERR-TELEMETRY-032") { + t.Errorf("Did not expect ERR-TELEMETRY-032 for successful response body close, got:\n%s", logs) + } +} + +// TestTarpit_EarlyDisconnect tests that tarpit handles client disconnect gracefully +func TestTarpit_EarlyDisconnect(t *testing.T) { + // Create a custom ResponseWriter that simulates disconnect after first write + disconnectWriter := &disconnectSimulatingWriter{ + headers: make(http.Header), + failAfter: 1, // Fail after first write + } + + req := httptest.NewRequest("GET", "/unknown", nil) + + // Use a goroutine since tarpit blocks + done := make(chan bool) + go func() { + tarpit(disconnectWriter, req) + done <- true + }() + + // Wait for tarpit to complete (should be quick due to disconnect) + select { + case <-done: + // Expected - tarpit returned early due to disconnect + case <-time.After(2 * time.Second): + t.Error("tarpit did not return early on client disconnect") + } + + // Verify that at least one write was attempted + if !disconnectWriter.written { + t.Error("tarpit should have attempted at least one write") + } +} + +// disconnectSimulatingWriter simulates a client that disconnects after N writes +type disconnectSimulatingWriter struct { + headers http.Header + status int + written bool + writeCount int + failAfter int + flushCount int +} + +func (m *disconnectSimulatingWriter) Header() http.Header { + return m.headers +} + +func (m *disconnectSimulatingWriter) Write(p []byte) (int, error) { + m.written = true + m.writeCount++ + if m.writeCount >= m.failAfter { + return 0, io.ErrClosedPipe // Simulate client disconnect + } + return len(p), nil +} + +func (m *disconnectSimulatingWriter) WriteHeader(status int) { + m.status = status +} + +func (m *disconnectSimulatingWriter) Flush() { + m.flushCount++ +} + +// TestErrorCodes_Unique verifies that all error codes are unique +func TestErrorCodes_Unique(t *testing.T) { + // Read the main.go and kuma.go files to extract error codes + mainContent, err := os.ReadFile("main.go") + if err != nil { + t.Fatalf("Failed to read main.go: %v", err) + } + kumaContent, err := os.ReadFile("kuma.go") + if err != nil { + t.Fatalf("Failed to read kuma.go: %v", err) + } + + content := string(mainContent) + string(kumaContent) + + // Extract all ERR-TELEMETRY-XXX codes + codes := make(map[string]int) + for i := 0; i < len(content)-15; i++ { + if content[i:i+14] == "ERR-TELEMETRY-" { + // Find the end of the code (3 digits) + end := i + 14 + for end < len(content) && content[end] >= '0' && content[end] <= '9' { + end++ + } + if end > i+14 { + code := content[i:end] + codes[code]++ + } + } + } + + // Verify each code appears only once (unique) + for code, count := range codes { + if count > 1 { + t.Errorf("Error code %s appears %d times - should be unique", code, count) + } + } + + // Verify expected codes exist + expectedCodes := []string{ + "ERR-TELEMETRY-001", // Failed to open operations.db + "ERR-TELEMETRY-002", // Failed to load CA chain + "ERR-TELEMETRY-003", // Invalid certificate + "ERR-TELEMETRY-004", // Failed to insert telemetry + "ERR-TELEMETRY-005", // Failed to create telemetry table + "ERR-TELEMETRY-006", // Failed to create telemetry index + "ERR-TELEMETRY-007", // Failed to create telemetry node_latest index + "ERR-TELEMETRY-008", // Failed to create uptime_spans table + "ERR-TELEMETRY-009", // Failed to create uptime_spans index + "ERR-TELEMETRY-010", // Failed to check maintenance mode + "ERR-TELEMETRY-011", // Failed to query latest uptime span + "ERR-TELEMETRY-012", // Failed to extend uptime span + "ERR-TELEMETRY-013", // Failed to extend early-judgment span + "ERR-TELEMETRY-014", // Failed to insert new uptime span + "ERR-TELEMETRY-015", // Failed to create maintenance table + "ERR-TELEMETRY-020", // Failed to create ntfy alert request + "ERR-TELEMETRY-021", // Failed to send ntfy alert + "ERR-TELEMETRY-022", // Failed to close ntfy response body + "ERR-TELEMETRY-030", // Failed to push health to Kuma + "ERR-TELEMETRY-031", // Kuma returned non-OK status + "ERR-TELEMETRY-032", // Failed to close Kuma response body (OK status) + "ERR-TELEMETRY-033", // Failed to query last telemetry timestamp + "ERR-TELEMETRY-034", // Failed to close Kuma response body (non-OK status) + "ERR-TELEMETRY-040", // tarpit called without Flusher + } + + for _, code := range expectedCodes { + if _, exists := codes[code]; !exists { + t.Errorf("Expected error code %s not found in source", code) + } + } + + t.Logf("Found %d unique error codes", len(codes)) +} + +// TestErrorCodes_Format verifies error codes follow the correct format +func TestErrorCodes_Format(t *testing.T) { + // Read source files + mainContent, err := os.ReadFile("main.go") + if err != nil { + t.Fatalf("Failed to read main.go: %v", err) + } + kumaContent, err := os.ReadFile("kuma.go") + if err != nil { + t.Fatalf("Failed to read kuma.go: %v", err) + } + + content := string(mainContent) + string(kumaContent) + + // Check that error messages follow format: ERR-TELEMETRY-XXX: Actionable message + lines := strings.Split(content, "\n") + for i, line := range lines { + if strings.Contains(line, "ERR-TELEMETRY-") && strings.Contains(line, "log.") { + // Verify format includes colon after error code + if !strings.Contains(line, "ERR-TELEMETRY-") || !strings.Contains(line, ":") { + t.Errorf("Line %d: Error message should follow format 'ERR-TELEMETRY-XXX: message', got: %s", i+1, strings.TrimSpace(line)) + } + } + } +} + +// TestHandleTelemetry_DatabaseInsertError tests ERR-TELEMETRY-004 +func TestHandleTelemetry_DatabaseInsertError(t *testing.T) { + setupTestDB(t) + defer cleanupTestDB() + + // Temporarily disable mTLS + oldCAPool := caPool + caPool = nil + defer func() { caPool = oldCAPool }() + + // Corrupt database by making it read-only + db.Exec(`PRAGMA query_only = ON`) + + payload := map[string]interface{}{ + "node_id": "test-node-db-err", + "version": "1.0.0", + "hostname": "test-host", + "uptime_seconds": 3600, + "cpu_percent": 25.5, + "memory_total_mb": 8192, + "memory_used_mb": 4096, + "disk_total_mb": 100000, + "disk_used_mb": 50000, + "load_1m": 0.5, + "vault_count": 5, + "vault_size_mb": 10.5, + "vault_entries": 100, + "mode": "commercial", + } + + body, _ := json.Marshal(payload) + req := httptest.NewRequest("POST", "/telemetry", bytes.NewReader(body)) + w := httptest.NewRecorder() + + // Capture logs + var logBuffer bytes.Buffer + originalOutput := log.Writer() + log.SetOutput(&logBuffer) + defer log.SetOutput(originalOutput) + + handleTelemetry(w, req) + + // Restore query_only mode + db.Exec(`PRAGMA query_only = OFF`) + + // Should return 500 error + resp := w.Result() + if resp.StatusCode != 500 { + t.Errorf("Expected 500 status for database error, got %d", resp.StatusCode) + } + + // Verify ERR-TELEMETRY-004 appears in logs + logs := logBuffer.String() + if !strings.Contains(logs, "ERR-TELEMETRY-004") { + t.Errorf("Expected ERR-TELEMETRY-004 in logs when telemetry insert fails, got:\n%s", logs) + } +} + +// TestConcurrentErrorHandling verifies error handling is thread-safe +func TestConcurrentErrorHandling(t *testing.T) { + setupTestDB(t) + defer cleanupTestDB() + + // Capture logs + var logBuffer bytes.Buffer + originalOutput := log.Writer() + log.SetOutput(&logBuffer) + defer log.SetOutput(originalOutput) + + // Run multiple goroutines that trigger error handling + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + nodeID := fmt.Sprintf("concurrent-node-%d", id) + updateSpan(nodeID, "test-host", "1.0.0", 50.0, 4096, 8192, 50000, 100000, 0.5, 3600) + }(i) + } + wg.Wait() + + // Verify no panics occurred and logs were written safely + logs := logBuffer.String() + // Should have OUTAGE SPAN logs for each node + for i := 0; i < 10; i++ { + expectedNode := fmt.Sprintf("concurrent-node-%d", i) + if !strings.Contains(logs, expectedNode) { + t.Errorf("Expected logs to contain node %s", expectedNode) + } + } +} + + diff --git a/clavis/clavis-telemetry/kuma.go b/clavis/clavis-telemetry/kuma.go index bf2c09f..46b4673 100644 --- a/clavis/clavis-telemetry/kuma.go +++ b/clavis/clavis-telemetry/kuma.go @@ -64,7 +64,7 @@ func sendKumaPush(kumaURL string) { if resp.StatusCode != http.StatusOK { log.Printf("ERR-TELEMETRY-031: Kuma returned non-OK status %d from %s", resp.StatusCode, kumaURL) if err := resp.Body.Close(); err != nil { - log.Printf("ERR-TELEMETRY-032: Failed to close Kuma response body after non-OK status - %v", err) + log.Printf("ERR-TELEMETRY-034: Failed to close Kuma response body after non-OK status - %v", err) } return } diff --git a/clavis/clavis-telemetry/main.go b/clavis/clavis-telemetry/main.go index a8ab305..ba49142 100644 --- a/clavis/clavis-telemetry/main.go +++ b/clavis/clavis-telemetry/main.go @@ -204,7 +204,7 @@ func ensureTables() { started_by TEXT NOT NULL DEFAULT '', ended_by TEXT NOT NULL DEFAULT '' )`); err != nil { - log.Fatalf("ERR-TELEMETRY-010: Failed to create maintenance table - %v", err) + log.Fatalf("ERR-TELEMETRY-015: Failed to create maintenance table - %v", err) } } From 3be8a683a750fb24e9f87bca39f9c282a21dd903 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 9 Apr 2026 03:36:54 -0400 Subject: [PATCH 3/3] =?UTF-8?q?ui:=20remove=20strikethrough=20pricing=20?= =?UTF-8?q?=E2=80=94=20always=20show=20$12/yr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes crossed-out "$20" pricing from all templates and test files. The hosted plan is now permanently $12/year. Files modified: - test-index.html: Header CTA button - test-hosted.html: Header CTA and hero text - base.tmpl: Header CTA button - hosted.tmpl: Hero text and CTA section - index.tmpl: Hero button and hosted CTA section - upgrade.tmpl: Pricing comparison text - integrations.tmpl: All 4 CTA buttons (English + Chinese) - install.tmpl: Hosted option section fixes #9 Author: Emma --- clavitor.ai/templates/base.tmpl | 2 +- clavitor.ai/templates/hosted.tmpl | 4 ++-- clavitor.ai/templates/index.tmpl | 4 ++-- clavitor.ai/templates/install.tmpl | 2 +- clavitor.ai/templates/integrations.tmpl | 8 ++++---- clavitor.ai/templates/upgrade.tmpl | 2 +- clavitor.ai/test-hosted.html | 4 ++-- clavitor.ai/test-index.html | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/clavitor.ai/templates/base.tmpl b/clavitor.ai/templates/base.tmpl index bfd56be..6218b75 100644 --- a/clavitor.ai/templates/base.tmpl +++ b/clavitor.ai/templates/base.tmpl @@ -120,7 +120,7 @@ Sign in - Get hosted — $20 $12/yr + Get hosted — $12/yr diff --git a/clavitor.ai/templates/hosted.tmpl b/clavitor.ai/templates/hosted.tmpl index dddecfc..69359a2 100644 --- a/clavitor.ai/templates/hosted.tmpl +++ b/clavitor.ai/templates/hosted.tmpl @@ -3,7 +3,7 @@

clavitor hosted

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.

+

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. $12/yr.

@@ -159,7 +159,7 @@

Ready?

-

$20 $12/yr. 7-day money-back. Every feature included. Price for life — your rate never increases.

+

$12/yr. 7-day money-back. Every feature included. Price for life — your rate never increases.

Get started Compare plans → diff --git a/clavitor.ai/templates/index.tmpl b/clavitor.ai/templates/index.tmpl index b6af235..ca08f79 100644 --- a/clavitor.ai/templates/index.tmpl +++ b/clavitor.ai/templates/index.tmpl @@ -6,7 +6,7 @@

"If you want to keep a secret, you must also hide it from yourself."

We did. Your Identity Encryption key is derived in your browser from your WebAuthn authenticator — fingerprint, face, or hardware key. Our servers have never seen it. They could not decrypt your private fields even if they wanted to. Or anybody else.

@@ -439,7 +439,7 @@ ghp_a3f8...

Your vault needs to be everywhere you are.

A password manager that only works on your home network isn't a password manager. Your laptop moves. Your phone moves. Your browser extension needs your vault at the coffee shop, on the plane, at the client's office.

Self-hosting that means a server with a public IP, DNS, TLS certificates, uptime monitoring, and backups. That's not a weekend project — that's infrastructure.

-

We run clavitor across {{len .Pops}} regions on every continent. $20 $12/yr. Your Identity Encryption keys never leave your browser — we mathematically cannot read your private fields.

+

We run clavitor across {{len .Pops}} regions on every continent. $12/yr. Your Identity Encryption keys never leave your browser — we mathematically cannot read your private fields.

Get hosted → Self-host anyway diff --git a/clavitor.ai/templates/install.tmpl b/clavitor.ai/templates/install.tmpl index 245f494..0ac6ab3 100644 --- a/clavitor.ai/templates/install.tmpl +++ b/clavitor.ai/templates/install.tmpl @@ -121,7 +121,7 @@ WantedBy=multi-user.target

Rather not manage it yourself?

-

Same vault, same features. We handle updates, backups, and TLS. $20 $12/yr.

+

Same vault, same features. We handle updates, backups, and TLS. $12/yr.

See hosted option →
diff --git a/clavitor.ai/templates/integrations.tmpl b/clavitor.ai/templates/integrations.tmpl index 5228f8a..3bf3c2d 100644 --- a/clavitor.ai/templates/integrations.tmpl +++ b/clavitor.ai/templates/integrations.tmpl @@ -79,7 +79,7 @@

Three-tier encryption. Scoped access. Your AI gets what it needs — nothing more.

- Get hosted — $20 $12/yr + Get hosted — $12/yr Self-host free →
@@ -152,7 +152,7 @@

Scoped access for every agent. Your secrets stay yours.

- Get hosted — $20 $12/yr + Get hosted — $12/yr Self-host free →
@@ -200,7 +200,7 @@

Multi-agent. Scoped. Encrypted. Built for autonomous workflows.

- Get hosted — $20 $12/yr + Get hosted — $12/yr Self-host free →
@@ -253,7 +253,7 @@

多智能体。范围限定。加密。为自主工作流构建。

- 托管服务 — $20 $12/年 + 托管服务 — $12/年 免费自托管 →
diff --git a/clavitor.ai/templates/upgrade.tmpl b/clavitor.ai/templates/upgrade.tmpl index 9599d5b..fa4f375 100644 --- a/clavitor.ai/templates/upgrade.tmpl +++ b/clavitor.ai/templates/upgrade.tmpl @@ -137,7 +137,7 @@

Ready to upgrade?

-

Self-host for free, or let us run it for $20 $12/yr.

+

Self-host for free, or let us run it for $12/yr.

Self-host → Get hosted → diff --git a/clavitor.ai/test-hosted.html b/clavitor.ai/test-hosted.html index e15bc72..1d6cec3 100644 --- a/clavitor.ai/test-hosted.html +++ b/clavitor.ai/test-hosted.html @@ -41,7 +41,7 @@ EUR €
- Get hosted — $20 $12/yr + Get hosted — $12/yr @@ -49,7 +49,7 @@

clavitor hosted

Zero cache. Every request hits the vault.

-

Clavitor never caches credentials. To make it fast, we run 4 regions across every continent. $20 $12/yr.

+

Clavitor never caches credentials. To make it fast, we run 4 regions across every continent. $12/yr.

diff --git a/clavitor.ai/test-index.html b/clavitor.ai/test-index.html index 75dabc9..7a85164 100644 --- a/clavitor.ai/test-index.html +++ b/clavitor.ai/test-index.html @@ -59,7 +59,7 @@
Sign in - Get hosted — $20 $12/yr + Get hosted — $12/yr