// Dynamic Admin - Auto-generates pages from SQLite schema // Q&D: Query sqlite_master for tables, pragma table_info for columns package main import ( "database/sql" "fmt" "html/template" "log" "net/http" "strings" "time" "github.com/go-chi/chi/v5" _ "github.com/mattn/go-sqlite3" ) var db *sql.DB var isSandboxMode bool func main() { var err error db, err = sql.Open("sqlite3", "corporate.db") if err != nil { log.Fatal(err) } defer db.Close() // Detect sandbox mode by IP isSandboxMode = !isProductionServer() if isSandboxMode { fmt.Println("๐Ÿงช SANDBOX MODE DETECTED (non-production IP)") } else { fmt.Println("๐Ÿญ PRODUCTION MODE (running on clavitor.ai)") } initDB() r := chi.NewRouter() // Dynamic routes - auto-generated from schema r.Get("/", handleDynamicHome) r.Get("/table/{table}", handleDynamicList) r.Get("/table/{table}/new", handleDynamicForm) r.Post("/table/{table}", handleDynamicCreate) r.Get("/table/{table}/{id}", handleDynamicView) r.Get("/table/{table}/{id}/edit", handleDynamicEdit) r.Post("/table/{table}/{id}", handleDynamicUpdate) // Static file for checkout r.Get("/checkout", handleCheckoutPage) fmt.Println("Dynamic Admin starting...") fmt.Println("Local: http://localhost:1985") fmt.Println("Tables auto-detected from SQLite schema") log.Fatal(http.ListenAndServe(":1985", r)) } // Schema introspection type ColumnInfo struct { Name string Type string NotNull bool Default interface{} PK bool } func getTables() []string { rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name") if err != nil { return []string{} } defer rows.Close() var tables []string for rows.Next() { var name string rows.Scan(&name) tables = append(tables, name) } return tables } func getTableInfo(table string) []ColumnInfo { rows, err := db.Query(fmt.Sprintf("PRAGMA table_info(%s)", table)) if err != nil { return nil } defer rows.Close() var cols []ColumnInfo for rows.Next() { var cid int var col ColumnInfo rows.Scan(&cid, &col.Name, &col.Type, &col.NotNull, &col.Default, &col.PK) cols = append(cols, col) } return cols } func getForeignKeys(table string) map[string]string { rows, err := db.Query(fmt.Sprintf("PRAGMA foreign_key_list(%s)", table)) if err != nil { return nil } defer rows.Close() fks := make(map[string]string) for rows.Next() { var id, seq int var tableName, from, to, onUpdate, onDelete, match string rows.Scan(&id, &seq, &tableName, &from, &to, &onUpdate, &onDelete, &match) fks[from] = tableName } return fks } // Auto-generate home page with all tables func handleDynamicHome(w http.ResponseWriter, r *http.Request) { tables := getTables() html := ` Clavitor Dynamic Admin{{if .IsSandbox}} [SANDBOX]{{end}} {{if .IsSandbox}}
๐Ÿงช SANDBOX MODE Test environment - not production
{{end}}

Clavitor Corporate Admin{{if .IsSandbox}} [SANDBOX]{{end}}

Auto-generated from SQLite schema โ€ข 95% Paddle + 5% Extensions{{if .IsSandbox}} โ€ข TEST ENVIRONMENT{{end}}

{{range .Cards}}

{{.Name}}

{{.Count}}
records
{{end}}

๐Ÿ’ฐ USD Pricing (from database)

{{range .Prices}}

{{.ProductName}}

{{.Amount}} {{.Currency}}
{{end}}
` type Card struct { Name string Count int } var cards []Card for _, t := range tables { var count int db.QueryRow(fmt.Sprintf("SELECT COUNT(*) FROM %s", t)).Scan(&count) cards = append(cards, Card{Name: t, Count: count}) } // Fetch USD prices from database (Paddle JSON format) type PriceInfo struct { ProductName string Amount string Currency string } var prices []PriceInfo rows, err := db.Query(` SELECT p.name, pr.unit_price FROM products p JOIN prices pr ON p.id = pr.product_id WHERE pr.unit_price LIKE '%USD%' ORDER BY p.name `) if err == nil { defer rows.Close() for rows.Next() { var name, unitPrice string rows.Scan(&name, &unitPrice) // Parse Paddle JSON: {"amount": "1200", "currency_code": "USD"} amount := "0" currency := "USD" if idx := strings.Index(unitPrice, `"amount":`); idx >= 0 { after := unitPrice[idx+9:] if q1 := strings.Index(after, `"`); q1 >= 0 { valStart := q1 + 1 if q2 := strings.Index(after[valStart:], `"`); q2 >= 0 { amount = after[valStart : valStart+q2] } } } if idx := strings.Index(unitPrice, `"currency_code":`); idx >= 0 { after := unitPrice[idx+17:] if q1 := strings.Index(after, `"`); q1 >= 0 { valStart := q1 + 1 if q2 := strings.Index(after[valStart:], `"`); q2 >= 0 { currency = after[valStart : valStart+q2] } } } prices = append(prices, PriceInfo{ProductName: name, Amount: amount, Currency: currency}) } } tmpl := template.Must(template.New("home").Parse(html)) tmpl.Execute(w, map[string]interface{}{ "Tables": tables, "Cards": cards, "Prices": prices, "IsSandbox": isSandboxMode, }) } // Auto-generate list view for any table func handleDynamicList(w http.ResponseWriter, r *http.Request) { table := chi.URLParam(r, "table") cols := getTableInfo(table) if cols == nil { http.Error(w, "Table not found", 404) return } // Get sample data (limit 100) colNames := []string{} for _, c := range cols { colNames = append(colNames, c.Name) } rows, err := db.Query(fmt.Sprintf("SELECT %s FROM %s LIMIT 100", strings.Join(colNames, ", "), table)) if err != nil { http.Error(w, err.Error(), 500) return } defer rows.Close() // Build data as []map[string]interface{} var records []map[string]interface{} for rows.Next() { values := make([]interface{}, len(cols)) valuePtrs := make([]interface{}, len(cols)) for i := range values { valuePtrs[i] = &values[i] } rows.Scan(valuePtrs...) record := make(map[string]interface{}) for i, col := range cols { val := values[i] if val == nil { record[col.Name] = "" } else { record[col.Name] = fmt.Sprintf("%v", val) } } records = append(records, record) } // Render dynamic table htmlTemplate := ` {{.Table}} - List{{if .IsSandbox}} [SANDBOX]{{end}} {{if .IsSandbox}}
๐Ÿงช SANDBOX MODE - Test environment
{{end}}

{{.Table}} + New

{{range .Columns}}{{end}} {{range $record := .Records}} {{range $.Columns}} {{end}} {{end}}
{{.Name}}
{{.Type}}{{if .PK}} ๐Ÿ”‘{{end}}
Actions
{{$val := index $record .Name}} {{if eq $val ""}}null{{else}}{{$val}}{{end}} View
` // Find PK column pk := "id" for _, c := range cols { if c.PK { pk = c.Name break } } tables := getTables() tmpl := template.Must(template.New("list").Parse(htmlTemplate)) tmpl.Execute(w, map[string]interface{}{ "Table": table, "Tables": tables, "Columns": cols, "Records": records, "PK": pk, "IsSandbox": isSandboxMode, }) } // Auto-generate form view for creating new record func handleDynamicForm(w http.ResponseWriter, r *http.Request) { table := chi.URLParam(r, "table") cols := getTableInfo(table) if cols == nil { http.Error(w, "Table not found", 404) return } htmlTemplate := ` {{.Table}} - New{{if .IsSandbox}} [SANDBOX]{{end}} {{if .IsSandbox}}
๐Ÿงช SANDBOX MODE - Test environment
{{end}}

New {{.Table}}

{{range .Columns}} {{if not .PK}} {{end}} {{end}}
` tables := getTables() tmpl := template.Must(template.New("form").Parse(htmlTemplate)) tmpl.Execute(w, map[string]interface{}{ "Table": table, "Tables": tables, "Columns": cols, "IsSandbox": isSandboxMode, }) } // Handle create - with Dual-Write Layer for Paddle entities func handleDynamicCreate(w http.ResponseWriter, r *http.Request) { table := chi.URLParam(r, "table") cols := getTableInfo(table) if cols == nil { http.Error(w, "Table not found", 404) return } if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), 400) return } // Build data map from form values data := make(map[string]interface{}) for _, col := range cols { if col.PK && col.Type == "INTEGER" { continue // Auto-increment } val := r.FormValue(col.Name) if val != "" { data[col.Name] = val } } // Check if this is a Paddle-managed entity if _, ok := EntityRegistry[table]; ok { // Use Dual-Write Layer (auto-detects sandbox mode by IP) sync := NewSyncLayer("", db) result := sync.Create(table, data) if !result.Success { http.Error(w, fmt.Sprintf("Sync failed: DB: %v, Paddle: %v", result.DBError, result.PaddleError), 500) return } // Log the sync result log.Printf("Created %s: our_id=%s, paddle_id=%s", table, result.OurID, result.PaddleID) http.Redirect(w, r, "/table/"+table, 302) return } // Non-Paddle entity: direct SQL insert var colNames []string var placeholders []string var values []interface{} for _, col := range cols { if col.PK && col.Type == "INTEGER" { continue } val := r.FormValue(col.Name) if val == "" && col.Default != nil { val = fmt.Sprintf("%v", col.Default) } if val != "" { colNames = append(colNames, col.Name) placeholders = append(placeholders, "?") values = append(values, val) } } // Add timestamps now := time.Now().Unix() for _, col := range cols { if col.Name == "created_at" { colNames = append(colNames, "created_at") placeholders = append(placeholders, "?") values = append(values, now) } if col.Name == "updated_at" { colNames = append(colNames, "updated_at") placeholders = append(placeholders, "?") values = append(values, now) } } sql := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", table, strings.Join(colNames, ", "), strings.Join(placeholders, ", ")) _, err := db.Exec(sql, values...) if err != nil { http.Error(w, err.Error(), 500) return } http.Redirect(w, r, "/table/"+table, 302) } // Auto-generate detail view func handleDynamicView(w http.ResponseWriter, r *http.Request) { table := chi.URLParam(r, "table") id := chi.URLParam(r, "id") cols := getTableInfo(table) if cols == nil { http.Error(w, "Table not found", 404) return } // Find PK column pk := "id" for _, c := range cols { if c.PK { pk = c.Name break } } // Get single record colNames := []string{} for _, c := range cols { colNames = append(colNames, c.Name) } row := db.QueryRow(fmt.Sprintf("SELECT %s FROM %s WHERE %s = ? LIMIT 1", strings.Join(colNames, ", "), table, pk), id) values := make([]interface{}, len(cols)) valuePtrs := make([]interface{}, len(cols)) for i := range values { valuePtrs[i] = &values[i] } if err := row.Scan(valuePtrs...); err != nil { http.Error(w, "Record not found", 404) return } record := make(map[string]interface{}) for i, col := range cols { val := values[i] if val == nil { record[col.Name] = "" } else { record[col.Name] = fmt.Sprintf("%v", val) } } htmlTemplate := ` {{.Table}} - {{.ID}}{{if .IsSandbox}} [SANDBOX]{{end}} {{if .IsSandbox}}
๐Ÿงช SANDBOX MODE - Test environment
{{end}}

{{.Table}}: {{.ID}}

Edit Back to List
{{range .Columns}} {{end}}
{{.Name}} {{if .PK}}๐Ÿ”‘{{end}} {{$val := index $.Record .Name}} {{if eq $val ""}}null{{else}}{{$val}}{{end}}
` tables := getTables() tmpl := template.Must(template.New("view").Parse(htmlTemplate)) tmpl.Execute(w, map[string]interface{}{ "Table": table, "ID": id, "Tables": tables, "Columns": cols, "Record": record, "IsSandbox": isSandboxMode, }) } func handleDynamicEdit(w http.ResponseWriter, r *http.Request) { table := chi.URLParam(r, "table") id := chi.URLParam(r, "id") cols := getTableInfo(table) if cols == nil { http.Error(w, "Table not found", 404) return } pk := "id" for _, c := range cols { if c.PK { pk = c.Name break } } // Get record colNames := []string{} for _, c := range cols { colNames = append(colNames, c.Name) } row := db.QueryRow(fmt.Sprintf("SELECT %s FROM %s WHERE %s = ?", strings.Join(colNames, ", "), table, pk), id) values := make([]interface{}, len(cols)) valuePtrs := make([]interface{}, len(cols)) for i := range values { valuePtrs[i] = &values[i] } if err := row.Scan(valuePtrs...); err != nil { http.Error(w, "Record not found", 404) return } record := make(map[string]interface{}) for i, col := range cols { val := values[i] if val == nil { record[col.Name] = "" } else { record[col.Name] = fmt.Sprintf("%v", val) } } htmlTemplate := ` {{.Table}} - Edit {{.ID}}{{if .IsSandbox}} [SANDBOX]{{end}} {{if .IsSandbox}}
๐Ÿงช SANDBOX MODE - Test environment
{{end}}

Edit {{.Table}}: {{.ID}}

{{range .Columns}} {{$val := index $.Record .Name}} {{end}}
` tables := getTables() tmpl := template.Must(template.New("edit").Parse(htmlTemplate)) tmpl.Execute(w, map[string]interface{}{ "Table": table, "ID": id, "Tables": tables, "Columns": cols, "Record": record, "IsSandbox": isSandboxMode, }) } func handleDynamicUpdate(w http.ResponseWriter, r *http.Request) { table := chi.URLParam(r, "table") id := chi.URLParam(r, "id") cols := getTableInfo(table) if cols == nil { http.Error(w, "Table not found", 404) return } if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), 400) return } pk := "id" for _, c := range cols { if c.PK { pk = c.Name break } } // Build UPDATE var sets []string var values []interface{} for _, col := range cols { if col.PK { continue } val := r.FormValue(col.Name) sets = append(sets, fmt.Sprintf("%s = ?", col.Name)) values = append(values, val) } // Add updated_at for _, col := range cols { if col.Name == "updated_at" { sets = append(sets, "updated_at = ?") values = append(values, time.Now().Unix()) } } values = append(values, id) sql := fmt.Sprintf("UPDATE %s SET %s WHERE %s = ?", table, strings.Join(sets, ", "), pk) _, err := db.Exec(sql, values...) if err != nil { http.Error(w, err.Error(), 500) return } http.Redirect(w, r, fmt.Sprintf("/table/%s/%s", table, id), 302) } func handleCheckoutPage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "static/checkout.html") } func initDB() { // Run the full schema from schema.sql schemaSQL := `-- Core tables CREATE TABLE IF NOT EXISTS products ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, tax_category TEXT, image_url TEXT, custom_data TEXT, status TEXT DEFAULT 'active', created_at INTEGER, updated_at INTEGER ); CREATE TABLE IF NOT EXISTS prices ( id TEXT PRIMARY KEY, product_id TEXT REFERENCES products(id), description TEXT, name TEXT, billing_cycle TEXT, trial_period TEXT, tax_mode TEXT DEFAULT 'account_setting', unit_price TEXT, unit_price_overrides TEXT, quantity TEXT, status TEXT DEFAULT 'active', custom_data TEXT, created_at INTEGER, updated_at INTEGER ); CREATE TABLE IF NOT EXISTS discounts ( id TEXT PRIMARY KEY, status TEXT DEFAULT 'active', description TEXT, enabled_for_checkout INTEGER DEFAULT 0, code TEXT, type TEXT, mode TEXT DEFAULT 'standard', amount TEXT, currency_code TEXT, recur INTEGER DEFAULT 1, maximum_recurring_intervals INTEGER, usage_limit INTEGER, restrict_to TEXT, expires_at INTEGER, times_used INTEGER DEFAULT 0, discount_group_id TEXT, custom_data TEXT, created_at INTEGER, updated_at INTEGER, paddle_id TEXT, sync_status TEXT DEFAULT 'pending', sync_error TEXT, last_paddle_sync_at INTEGER, pending_since INTEGER ); CREATE TABLE IF NOT EXISTS customers ( id TEXT PRIMARY KEY, email TEXT NOT NULL, name TEXT, locale TEXT DEFAULT 'en', status TEXT DEFAULT 'active', marketing_consent INTEGER DEFAULT 0, import_meta TEXT, parent_id TEXT REFERENCES customers(id), level INTEGER DEFAULT 1, internal_notes TEXT, custom_data TEXT, created_at INTEGER, updated_at INTEGER, paddle_id TEXT, billing_email TEXT, sync_status TEXT DEFAULT 'pending', sync_error TEXT, last_paddle_sync_at INTEGER, pending_since INTEGER ); CREATE TABLE IF NOT EXISTS addresses ( id TEXT PRIMARY KEY, customer_id TEXT NOT NULL REFERENCES customers(id), description TEXT, city TEXT, region TEXT, country_code TEXT NOT NULL, zip_code TEXT, status TEXT DEFAULT 'active', created_at INTEGER, updated_at INTEGER ); CREATE TABLE IF NOT EXISTS businesses ( id TEXT PRIMARY KEY, customer_id TEXT NOT NULL REFERENCES customers(id), name TEXT NOT NULL, company_number TEXT, tax_identifier TEXT, contacts TEXT, status TEXT DEFAULT 'active', custom_data TEXT, created_at INTEGER, updated_at INTEGER ); CREATE TABLE IF NOT EXISTS subscriptions ( id TEXT PRIMARY KEY, paddle_id TEXT, customer_id TEXT NOT NULL REFERENCES customers(id), address_id TEXT NOT NULL REFERENCES addresses(id), business_id TEXT REFERENCES businesses(id), currency_code TEXT NOT NULL, status TEXT NOT NULL, collection_mode TEXT DEFAULT 'automatic', billing_details TEXT, discount TEXT, started_at INTEGER, first_billed_at INTEGER, next_billed_at INTEGER, paused_at INTEGER, canceled_at INTEGER, vault_l0 TEXT, billing_cycle TEXT, current_billing_period TEXT, scheduled_change TEXT, items TEXT, management_urls TEXT, custom_data TEXT, import_meta TEXT, created_at INTEGER, updated_at INTEGER, consent_requirements TEXT, sync_status TEXT DEFAULT 'pending', sync_error TEXT, last_paddle_sync_at INTEGER, pending_since INTEGER ); CREATE TABLE IF NOT EXISTS subscription_items ( id TEXT PRIMARY KEY, subscription_id TEXT NOT NULL REFERENCES subscriptions(id), price_id TEXT NOT NULL, product_id TEXT NOT NULL, quantity INTEGER DEFAULT 1, status TEXT DEFAULT 'active', recurring INTEGER DEFAULT 1, created_at INTEGER, updated_at INTEGER, previously_billed_at INTEGER, next_billed_at INTEGER, trial_dates TEXT, price_snapshot TEXT, product_snapshot TEXT ); CREATE TABLE IF NOT EXISTS transactions ( id TEXT PRIMARY KEY, paddle_id TEXT, subscription_id TEXT REFERENCES subscriptions(id), customer_id TEXT NOT NULL REFERENCES customers(id), address_id TEXT REFERENCES addresses(id), business_id TEXT REFERENCES businesses(id), invoice_id TEXT, invoice_number TEXT, discount_id TEXT REFERENCES discounts(id), status TEXT NOT NULL, origin TEXT, currency_code TEXT NOT NULL, details TEXT, payments TEXT, adjusted_totals TEXT, payout_totals TEXT, adjusted_payout_totals TEXT, billing_details TEXT, additional_information TEXT, purchase_order_number TEXT, checkout_url TEXT, billed_at INTEGER, completed_at INTEGER, revised_at INTEGER, created_at INTEGER, updated_at INTEGER, processed_at INTEGER, processing_error TEXT ); CREATE TABLE IF NOT EXISTS adjustments ( id TEXT PRIMARY KEY, transaction_id TEXT NOT NULL REFERENCES transactions(id), subscription_id TEXT REFERENCES subscriptions(id), customer_id TEXT NOT NULL REFERENCES customers(id), action TEXT NOT NULL, type TEXT, amount TEXT NOT NULL, currency_code TEXT NOT NULL, totals TEXT, payout_totals TEXT, reason TEXT, credit_applied_to TEXT, created_at INTEGER ); CREATE TABLE IF NOT EXISTS vaults ( l0 TEXT PRIMARY KEY, customer_id TEXT NOT NULL REFERENCES customers(id), subscription_id TEXT REFERENCES subscriptions(id), status TEXT DEFAULT 'active', created_at INTEGER, last_seen_at INTEGER, created_by_human INTEGER ); CREATE TABLE IF NOT EXISTS credentials ( id INTEGER PRIMARY KEY, customer_id TEXT NOT NULL REFERENCES customers(id), p0 TEXT NOT NULL, l0 TEXT NOT NULL REFERENCES vaults(l0), credential_id BLOB, wl3_path TEXT, enrolled_by INTEGER, created_at INTEGER, last_used_at INTEGER ); CREATE TABLE IF NOT EXISTS roles ( id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE, permissions TEXT, description TEXT, created_at INTEGER ); CREATE TABLE IF NOT EXISTS humans ( id INTEGER PRIMARY KEY, customer_id TEXT REFERENCES customers(id), email TEXT NOT NULL UNIQUE, name TEXT NOT NULL, status TEXT DEFAULT 'active', created_at INTEGER, updated_at INTEGER ); CREATE TABLE IF NOT EXISTS role_assignments ( id INTEGER PRIMARY KEY, human_id INTEGER NOT NULL REFERENCES humans(id), customer_id TEXT NOT NULL REFERENCES customers(id), role_id INTEGER NOT NULL REFERENCES roles(id), granted_by INTEGER REFERENCES humans(id), created_at INTEGER ); CREATE TABLE IF NOT EXISTS pop_sync_state ( pop_id TEXT PRIMARY KEY, last_sync_at INTEGER, last_sync_cursor INTEGER, status TEXT DEFAULT 'active', error_message TEXT, updated_at INTEGER ); CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY, occurred_at INTEGER, event_type TEXT, entity_type TEXT, entity_id TEXT, customer_id TEXT REFERENCES customers(id), human_id INTEGER REFERENCES humans(id), vault_l0 TEXT, amount_cents INTEGER, currency_code TEXT, details TEXT, source TEXT DEFAULT 'api', ip_address TEXT, user_agent TEXT ); CREATE TABLE IF NOT EXISTS wl3_storage ( path TEXT PRIMARY KEY, content BLOB, p0 TEXT, customer_id TEXT, created_at INTEGER, sync_count INTEGER DEFAULT 0, last_sync_at INTEGER ); -- CURRENCY TABLE: Conversion metadata + pretty pricing rules -- See schema.sql for full documentation on "Pretty Commercial Prices" CREATE TABLE IF NOT EXISTS currencies ( code TEXT PRIMARY KEY, name TEXT NOT NULL, decimals INTEGER NOT NULL DEFAULT 2, exchange_rate REAL, rate_fetched_at INTEGER, symbol TEXT, symbol_position TEXT DEFAULT 'prefix', pretty_pattern TEXT DEFAULT 'x.99', is_active INTEGER DEFAULT 1, created_at INTEGER, updated_at INTEGER ); ` _, err := db.Exec(schemaSQL) if err != nil { log.Printf("Schema init error: %v", err) } // Pre-populate roles if empty var roleCount int db.QueryRow("SELECT COUNT(*) FROM roles").Scan(&roleCount) if roleCount == 0 { now := time.Now().Unix() db.Exec(`INSERT INTO roles (id, name, permissions, description, created_at) VALUES (1, 'SuperAdmin', '{"all": true}', 'Full system access', ?), (2, 'MSPAdmin', '{"customer_admin": true, "billing_view": true}', 'MSP-level admin', ?), (3, 'TenantAdmin', '{"vault_create": true, "agent_create": true, "device_enroll": true}', 'Customer admin', ?), (4, 'VaultUser', '{"vault_read": true, "vault_write": true}', 'Regular user', ?), (5, 'BillingAdmin', '{"billing_view": true, "invoices": true}', 'Can view/pay invoices', ?), (6, 'ReadOnly', '{"vault_read": true}', 'View only', ?)`, now, now, now, now, now, now) } // Pre-populate products if empty var prodCount int db.QueryRow("SELECT COUNT(*) FROM products").Scan(&prodCount) if prodCount == 0 { now := time.Now().Unix() products := []struct { ID string Name string Description string }{ {"pro_personal", "Clavitor Personal", "1 vault, 5 agents, 2 devices"}, {"pro_family", "Clavitor Family", "1 vault, 15 agents, 6 devices"}, {"pro_pro", "Clavitor Pro", "1 vault, 50 agents, unlimited devices"}, {"pro_team10", "Clavitor Team 10", "Up to 10 employees"}, {"pro_team25", "Clavitor Team 25", "Up to 25 employees"}, {"pro_team100", "Clavitor Team 100", "Up to 100 employees"}, {"pro_team250", "Clavitor Team 250", "Up to 250 employees"}, {"pro_team500", "Clavitor Team 500", "Up to 500 employees"}, } for _, p := range products { db.Exec("INSERT INTO products (id, name, description, status, created_at) VALUES (?, ?, ?, 'active', ?)", p.ID, p.Name, p.Description, now) } } // Pre-populate currencies if empty var currCount int db.QueryRow("SELECT COUNT(*) FROM currencies").Scan(&currCount) if currCount == 0 { log.Println("Currencies table empty. Run: go run scripts/fetch_rates.go") } }