clavitor/clavitor.ai/admin/main.go.bak

1530 lines
48 KiB
Go

// Clavitor Corporate Admin - Clean Q&D implementation
// 95% Paddle format, 5% our extensions
package main
import (
"database/sql"
"fmt"
"html/template"
"log"
"net/http"
"time"
"github.com/go-chi/chi/v5"
_ "github.com/mattn/go-sqlite3"
)
var db *sql.DB
func main() {
var err error
db, err = sql.Open("sqlite3", "corporate.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Initialize schema
initDB()
r := chi.NewRouter()
// Routes
r.Get("/", handleHome)
r.Get("/customers", handleCustomersList)
r.Get("/customers/{id}", handleCustomerView)
r.Get("/customers/new", handleCustomerForm)
r.Post("/customers", handleCustomerCreate)
r.Get("/subscriptions", handleSubscriptionsList)
r.Get("/subscriptions/{id}", handleSubscriptionView)
r.Get("/transactions", handleTransactionsList)
r.Get("/transactions/{id}", handleTransactionView)
r.Get("/vaults", handleVaultsList)
r.Get("/vaults/{l0}", handleVaultView)
r.Get("/discounts", handleDiscountsList)
r.Get("/discounts/new", handleDiscountForm)
r.Post("/discounts", handleDiscountCreate)
r.Get("/events", handleEventsList)
// P0/PRF Credential Registration API
// TODO: Add SUPER DUPER SECURE POP AUTH middleware
// These handlers are defined in credentials_api.go
r.Post("/api/v1/credentials/register", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Credential API - implement in credentials_api.go", 501)
})
r.Get("/api/v1/credentials", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Credential API - implement in credentials_api.go", 501)
})
r.Get("/api/v1/credentials/wl3", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Credential API - implement in credentials_api.go", 501)
})
r.Get("/api/v1/credentials/sync", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Credential API - implement in credentials_api.go", 501)
})
// Paddle Checkout API
r.Post("/api/checkout", HandleCheckout)
r.Get("/checkout/success", HandleCheckoutSuccess)
r.Post("/webhooks/paddle", HandlePaddleWebhook)
fmt.Println("Admin server starting...")
fmt.Println("Local: http://localhost:1985")
fmt.Println("Production: https://admin.dev.clavitor.ai (via Caddy reverse proxy)")
fmt.Println("Paddle Sandbox: https://sandbox-vendors.paddle.com")
log.Fatal(http.ListenAndServe(":1985", r))
}
func initDB() {
// Read and execute schema
// For now just ensure tables exist with basic structure
schema := `
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,
billing_email TEXT,
parent_id TEXT,
level INTEGER DEFAULT 1,
custom_data TEXT,
created_at INTEGER,
updated_at INTEGER
);
CREATE TABLE IF NOT EXISTS addresses (
id TEXT PRIMARY KEY,
customer_id TEXT NOT NULL,
country_code TEXT NOT NULL,
city TEXT,
created_at INTEGER
);
CREATE TABLE IF NOT EXISTS subscriptions (
id TEXT PRIMARY KEY,
paddle_id TEXT,
customer_id TEXT NOT NULL,
address_id TEXT,
business_id TEXT,
vault_l0 TEXT,
status TEXT,
plan TEXT,
currency_code TEXT,
collection_mode TEXT DEFAULT 'automatic',
billing_cycle TEXT,
current_billing_period TEXT,
discount TEXT,
items TEXT,
created_at INTEGER,
started_at INTEGER,
first_billed_at INTEGER,
next_billed_at INTEGER,
paused_at INTEGER,
canceled_at INTEGER,
updated_at INTEGER,
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,
price_id TEXT,
quantity INTEGER DEFAULT 1,
created_at INTEGER
);
CREATE TABLE IF NOT EXISTS transactions (
id TEXT PRIMARY KEY,
paddle_id TEXT,
subscription_id TEXT,
customer_id TEXT NOT NULL,
address_id TEXT,
business_id TEXT,
status TEXT,
origin TEXT,
currency_code TEXT,
amount_cents INTEGER,
details TEXT,
payments TEXT,
adjusted_totals TEXT,
payout_totals TEXT,
adjusted_payout_totals TEXT,
line_items TEXT,
invoice_number TEXT,
invoice_url 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 vaults (
l0 TEXT PRIMARY KEY,
customer_id TEXT,
subscription_id TEXT,
status TEXT DEFAULT 'active',
created_at INTEGER,
last_seen_at INTEGER
);
CREATE TABLE IF NOT EXISTS credentials (
id TEXT PRIMARY KEY,
l0 TEXT NOT NULL,
p0 TEXT NOT NULL,
wl3_path TEXT,
created_at INTEGER
);
CREATE TABLE IF NOT EXISTS discounts (
id TEXT PRIMARY KEY,
paddle_id TEXT,
status TEXT DEFAULT 'active',
description TEXT,
enabled_for_checkout INTEGER DEFAULT 0,
code TEXT,
type TEXT,
mode TEXT,
amount TEXT,
currency_code TEXT,
recur INTEGER DEFAULT 0,
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,
sync_status TEXT DEFAULT 'pending',
sync_error TEXT,
last_paddle_sync_at INTEGER,
pending_since INTEGER
);
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
occurred_at INTEGER,
event_type TEXT,
entity_type TEXT,
entity_id TEXT,
customer_id TEXT,
human_id INTEGER,
vault_l0 TEXT,
amount_cents INTEGER,
currency_code TEXT,
details TEXT,
source TEXT,
ip_address TEXT,
user_agent TEXT,
paddle_event_id TEXT
);
`
_, err := db.Exec(schema)
if err != nil {
log.Printf("Schema init: %v", err)
}
// Migrate existing tables - add missing columns
migrations := []string{
// Customers table
`ALTER TABLE customers ADD COLUMN marketing_consent INTEGER DEFAULT 0`,
`ALTER TABLE customers ADD COLUMN billing_email TEXT`,
`ALTER TABLE customers ADD COLUMN paddle_id TEXT`,
`ALTER TABLE customers ADD COLUMN sync_status TEXT DEFAULT 'pending'`,
`ALTER TABLE customers ADD COLUMN sync_error TEXT`,
`ALTER TABLE customers ADD COLUMN last_paddle_sync_at INTEGER`,
`ALTER TABLE customers ADD COLUMN pending_since INTEGER`,
// Subscriptions table
`ALTER TABLE subscriptions ADD COLUMN paddle_id TEXT`,
`ALTER TABLE subscriptions ADD COLUMN business_id TEXT`,
`ALTER TABLE subscriptions ADD COLUMN billing_cycle TEXT`,
`ALTER TABLE subscriptions ADD COLUMN current_billing_period TEXT`,
`ALTER TABLE subscriptions ADD COLUMN discount TEXT`,
`ALTER TABLE subscriptions ADD COLUMN started_at INTEGER`,
`ALTER TABLE subscriptions ADD COLUMN first_billed_at INTEGER`,
`ALTER TABLE subscriptions ADD COLUMN paused_at INTEGER`,
`ALTER TABLE subscriptions ADD COLUMN canceled_at INTEGER`,
`ALTER TABLE subscriptions ADD COLUMN updated_at INTEGER`,
`ALTER TABLE subscriptions ADD COLUMN sync_status TEXT DEFAULT 'pending'`,
`ALTER TABLE subscriptions ADD COLUMN sync_error TEXT`,
`ALTER TABLE subscriptions ADD COLUMN last_paddle_sync_at INTEGER`,
`ALTER TABLE subscriptions ADD COLUMN pending_since INTEGER`,
// Transactions table
`ALTER TABLE transactions ADD COLUMN paddle_id TEXT`,
`ALTER TABLE transactions ADD COLUMN address_id TEXT`,
`ALTER TABLE transactions ADD COLUMN business_id TEXT`,
`ALTER TABLE transactions ADD COLUMN origin TEXT`,
`ALTER TABLE transactions ADD COLUMN details TEXT`,
`ALTER TABLE transactions ADD COLUMN payments TEXT`,
`ALTER TABLE transactions ADD COLUMN adjusted_totals TEXT`,
`ALTER TABLE transactions ADD COLUMN payout_totals TEXT`,
`ALTER TABLE transactions ADD COLUMN adjusted_payout_totals TEXT`,
`ALTER TABLE transactions ADD COLUMN line_items TEXT`,
`ALTER TABLE transactions ADD COLUMN invoice_url TEXT`,
`ALTER TABLE transactions ADD COLUMN billing_details TEXT`,
`ALTER TABLE transactions ADD COLUMN additional_information TEXT`,
`ALTER TABLE transactions ADD COLUMN purchase_order_number TEXT`,
`ALTER TABLE transactions ADD COLUMN checkout_url TEXT`,
`ALTER TABLE transactions ADD COLUMN billed_at INTEGER`,
`ALTER TABLE transactions ADD COLUMN completed_at INTEGER`,
`ALTER TABLE transactions ADD COLUMN revised_at INTEGER`,
`ALTER TABLE transactions ADD COLUMN updated_at INTEGER`,
`ALTER TABLE transactions ADD COLUMN processed_at INTEGER`,
`ALTER TABLE transactions ADD COLUMN processing_error TEXT`,
// Vaults table
`ALTER TABLE vaults ADD COLUMN subscription_id TEXT`,
`ALTER TABLE vaults ADD COLUMN last_seen_at INTEGER`,
// Discounts table
`ALTER TABLE discounts ADD COLUMN paddle_id TEXT`,
`ALTER TABLE discounts ADD COLUMN enabled_for_checkout INTEGER DEFAULT 0`,
`ALTER TABLE discounts ADD COLUMN code TEXT`,
`ALTER TABLE discounts ADD COLUMN mode TEXT`,
`ALTER TABLE discounts ADD COLUMN currency_code TEXT`,
`ALTER TABLE discounts ADD COLUMN recur INTEGER DEFAULT 0`,
`ALTER TABLE discounts ADD COLUMN maximum_recurring_intervals INTEGER`,
`ALTER TABLE discounts ADD COLUMN usage_limit INTEGER`,
`ALTER TABLE discounts ADD COLUMN restrict_to TEXT`,
`ALTER TABLE discounts ADD COLUMN expires_at INTEGER`,
`ALTER TABLE discounts ADD COLUMN times_used INTEGER DEFAULT 0`,
`ALTER TABLE discounts ADD COLUMN discount_group_id TEXT`,
`ALTER TABLE discounts ADD COLUMN updated_at INTEGER`,
`ALTER TABLE discounts ADD COLUMN sync_status TEXT DEFAULT 'pending'`,
`ALTER TABLE discounts ADD COLUMN sync_error TEXT`,
`ALTER TABLE discounts ADD COLUMN last_paddle_sync_at INTEGER`,
`ALTER TABLE discounts ADD COLUMN pending_since INTEGER`,
// Events table
`ALTER TABLE events ADD COLUMN entity_type TEXT`,
`ALTER TABLE events ADD COLUMN entity_id TEXT`,
`ALTER TABLE events ADD COLUMN human_id INTEGER`,
`ALTER TABLE events ADD COLUMN vault_l0 TEXT`,
`ALTER TABLE events ADD COLUMN amount_cents INTEGER`,
`ALTER TABLE events ADD COLUMN currency_code TEXT`,
`ALTER TABLE events ADD COLUMN source TEXT`,
`ALTER TABLE events ADD COLUMN ip_address TEXT`,
`ALTER TABLE events ADD COLUMN user_agent TEXT`,
`ALTER TABLE events ADD COLUMN paddle_event_id TEXT`,
}
for _, migration := range migrations {
db.Exec(migration) // Ignore errors - columns may already exist
}
}
// ========== HOME / DASHBOARD ==========
func handleHome(w http.ResponseWriter, r *http.Request) {
stats := map[string]interface{}{
"customers": 0,
"subscriptions": 0,
"transactions": 0,
"vaults": 0,
}
var count int
db.QueryRow("SELECT COUNT(*) FROM customers").Scan(&count)
stats["customers"] = count
db.QueryRow("SELECT COUNT(*) FROM subscriptions").Scan(&count)
stats["subscriptions"] = count
db.QueryRow("SELECT COUNT(*) FROM transactions").Scan(&count)
stats["transactions"] = count
db.QueryRow("SELECT COUNT(*) FROM vaults").Scan(&count)
stats["vaults"] = count
render(w, "home", stats)
}
// ========== CUSTOMERS ==========
func handleCustomersList(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query(`
SELECT c.id, c.email, c.name, c.locale, c.status, c.marketing_consent,
c.parent_id, c.level, c.billing_email, c.custom_data,
c.created_at, c.updated_at,
COALESCE(p.email, '') as parent_email,
(SELECT COUNT(*) FROM subscriptions WHERE customer_id = c.id) as subs,
(SELECT COUNT(*) FROM addresses WHERE customer_id = c.id) as addrs,
(SELECT COUNT(*) FROM vaults WHERE customer_id = c.id) as vaults
FROM customers c
LEFT JOIN customers p ON c.parent_id = p.id
ORDER BY c.created_at DESC
LIMIT 100
`)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer rows.Close()
var customers []map[string]interface{}
for rows.Next() {
var id, email, name, locale, status, billingEmail, customData, parentEmail string
var parentID string
var level, subs, addrs, vaultCount int
var marketingConsent int
var createdAt, updatedAt int64
rows.Scan(&id, &email, &name, &locale, &status, &marketingConsent,
&parentID, &level, &billingEmail, &customData, &createdAt, &updatedAt,
&parentEmail, &subs, &addrs, &vaultCount)
customers = append(customers, map[string]interface{}{
"id": id, "email": email, "name": name, "locale": locale, "status": status,
"marketing_consent": marketingConsent == 1, "level": level,
"parent_id": parentID, "parent_email": parentEmail,
"billing_email": billingEmail, "custom_data": customData,
"subs": subs, "addresses": addrs, "vaults": vaultCount,
"created": time.Unix(createdAt, 0).Format("2006-01-02"),
"updated": time.Unix(updatedAt, 0).Format("2006-01-02"),
})
}
render(w, "customers", customers)
}
func handleCustomerView(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
var c struct {
ID, Email, Name, Status string
Level int
ParentID string
CreatedAt int64
}
db.QueryRow("SELECT id, email, name, status, level, parent_id, created_at FROM customers WHERE id = ?", id).Scan(
&c.ID, &c.Email, &c.Name, &c.Status, &c.Level, &c.ParentID, &c.CreatedAt)
// Get subscriptions
rows, _ := db.Query("SELECT id, status, plan, currency_code, next_billed_at FROM subscriptions WHERE customer_id = ?", id)
defer rows.Close()
var subs []map[string]interface{}
for rows.Next() {
var sid, status, plan, currency string
var nextBilled int
rows.Scan(&sid, &status, &plan, &currency, &nextBilled)
subs = append(subs, map[string]interface{}{
"id": sid, "status": status, "plan": plan, "currency": currency,
"next_billed": time.Unix(int64(nextBilled), 0).Format("2006-01-02"),
})
}
// Get addresses
addRows, _ := db.Query("SELECT id, country_code, city FROM addresses WHERE customer_id = ?", id)
defer addRows.Close()
var addresses []map[string]interface{}
for addRows.Next() {
var aid, country, city string
addRows.Scan(&aid, &country, &city)
addresses = append(addresses, map[string]interface{}{"id": aid, "country": country, "city": city})
}
render(w, "customer_view", map[string]interface{}{
"customer": c,
"subscriptions": subs,
"addresses": addresses,
})
}
func handleCustomerForm(w http.ResponseWriter, r *http.Request) {
// Get potential parent customers (MSPs)
rows, _ := db.Query("SELECT id, email, name, level FROM customers WHERE level = 1 ORDER BY name")
defer rows.Close()
var parents []map[string]interface{}
for rows.Next() {
var id, email, name string
var level int
rows.Scan(&id, &email, &name, &level)
parents = append(parents, map[string]interface{}{"id": id, "email": email, "name": name})
}
render(w, "customer_form", parents)
}
func handleCustomerCreate(w http.ResponseWriter, r *http.Request) {
email := r.FormValue("email")
name := r.FormValue("name")
parentID := r.FormValue("parent_id")
level := 1
if parentID != "" {
var parentLevel int
db.QueryRow("SELECT level FROM customers WHERE id = ?", parentID).Scan(&parentLevel)
level = parentLevel + 1
}
// Generate ID (ctm_ prefix for Paddle compatibility)
id := fmt.Sprintf("ctm_%d", time.Now().UnixNano())
_, err := db.Exec(`
INSERT INTO customers (id, email, name, status, parent_id, level, created_at)
VALUES (?, ?, ?, 'active', ?, ?, ?)
`, id, email, name, parentID, level, time.Now().Unix())
if err != nil {
http.Error(w, err.Error(), 500)
return
}
http.Redirect(w, r, "/customers", 302)
}
// ========== SUBSCRIPTIONS ==========
func handleSubscriptionsList(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query(`
SELECT s.id, s.paddle_id, s.customer_id, s.address_id, s.business_id,
s.currency_code, s.status, s.collection_mode, s.plan, s.vault_l0,
s.billing_cycle, s.current_billing_period, s.discount,
s.created_at, s.started_at, s.first_billed_at, s.next_billed_at,
s.paused_at, s.canceled_at, s.updated_at,
s.sync_status, s.sync_error, s.last_paddle_sync_at,
c.email as customer_email,
(SELECT COUNT(*) FROM subscription_items WHERE subscription_id = s.id) as items
FROM subscriptions s
JOIN customers c ON s.customer_id = c.id
ORDER BY s.created_at DESC
LIMIT 100
`)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer rows.Close()
var subs []map[string]interface{}
for rows.Next() {
var id, paddleID, customerID, addressID, businessID, currency, status, mode, plan, vaultL0 string
var billingCycle, billingPeriod, discount, syncStatus, syncError string
var items int
var created, started, firstBilled, nextBilled, paused, canceled, updated, lastSync int64
var customerEmail string
rows.Scan(&id, &paddleID, &customerID, &addressID, &businessID,
&currency, &status, &mode, &plan, &vaultL0,
&billingCycle, &billingPeriod, &discount,
&created, &started, &firstBilled, &nextBilled, &paused, &canceled, &updated,
&syncStatus, &syncError, &lastSync,
&customerEmail, &items)
subs = append(subs, map[string]interface{}{
"id": id, "paddle_id": paddleID, "customer_id": customerID,
"address_id": addressID, "business_id": businessID,
"currency": currency, "status": status, "mode": mode,
"plan": plan, "vault": vaultL0,
"items_count": items,
"sync_status": syncStatus,
"customer": customerEmail,
"created": formatTime(created),
"next_bill": formatTime(nextBilled),
})
}
render(w, "subscriptions", subs)
}
func handleSubscriptionView(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
var s struct {
ID, CustomerID, AddressID, Status, Plan, Currency, Mode, VaultL0, Items string
Created, NextBilled int
}
db.QueryRow(`
SELECT id, customer_id, address_id, status, plan, currency_code,
collection_mode, vault_l0, items, created_at, next_billed_at
FROM subscriptions WHERE id = ?
`, id).Scan(&s.ID, &s.CustomerID, &s.AddressID, &s.Status, &s.Plan, &s.Currency, &s.Mode, &s.VaultL0, &s.Items, &s.Created, &s.NextBilled)
render(w, "subscription_view", s)
}
// ========== TRANSACTIONS ==========
func handleTransactionsList(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query(`
SELECT t.id, t.paddle_id, t.subscription_id, t.customer_id,
t.address_id, t.business_id, t.status, t.origin,
t.currency_code, t.amount_cents, t.details, t.payments,
t.adjusted_totals, t.payout_totals, t.adjusted_payout_totals,
t.line_items, t.invoice_number, t.invoice_url,
t.billing_details, t.additional_information, t.purchase_order_number,
t.checkout_url, t.billed_at, t.completed_at, t.revised_at,
t.created_at, t.updated_at, t.processed_at, t.processing_error,
c.email as customer_email
FROM transactions t
JOIN customers c ON t.customer_id = c.id
ORDER BY t.created_at DESC
LIMIT 100
`)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer rows.Close()
var txs []map[string]interface{}
for rows.Next() {
var id, paddleID, subID, customerID, addressID, businessID, status, origin, currency string
var amount int
var invoiceNum, invoiceURL, checkoutURL, customerEmail string
var billed, completed, created int64
rows.Scan(&id, &paddleID, &subID, &customerID, &addressID, &businessID,
&status, &origin, &currency, &amount,
new(string), new(string), new(string), new(string), // Skip large JSON fields for list
&invoiceNum, &invoiceURL, new(string), new(string), new(string),
&checkoutURL, &billed, &completed,
&created, new(int64), new(int64), new(string),
&customerEmail)
txs = append(txs, map[string]interface{}{
"id": id, "paddle_id": paddleID, "subscription_id": subID,
"status": status, "origin": origin,
"currency": currency, "amount": float64(amount) / 100,
"invoice": invoiceNum, "checkout": checkoutURL != "",
"customer": customerEmail,
"created": formatTime(created),
"billed": formatTime(billed),
})
}
render(w, "transactions", txs)
}
func handleTransactionView(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
var t struct {
ID, Status, Currency, Invoice string
AmountCents int
Created int
}
db.QueryRow("SELECT id, status, amount_cents, currency_code, invoice_number, created_at FROM transactions WHERE id = ?", id).Scan(
&t.ID, &t.Status, &t.AmountCents, &t.Currency, &t.Invoice, &t.Created)
render(w, "transaction_view", t)
}
// ========== VAULTS ==========
func handleVaultsList(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query(`
SELECT v.l0, v.customer_id, v.subscription_id, v.status,
v.created_at, v.last_seen_at,
c.email as customer_email,
(SELECT COUNT(*) FROM credentials WHERE l0 = v.l0) as devices
FROM vaults v
LEFT JOIN customers c ON v.customer_id = c.id
ORDER BY v.created_at DESC
`)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer rows.Close()
var vaults []map[string]interface{}
for rows.Next() {
var l0, customerID, subID, status, customerEmail string
var created, lastSeen int64
var devices int
rows.Scan(&l0, &customerID, &subID, &status, &created, &lastSeen, &customerEmail, &devices)
vaults = append(vaults, map[string]interface{}{
"l0": l0, "customer_id": customerID, "subscription_id": subID,
"status": status, "customer": customerEmail,
"devices": devices,
"created": formatTime(created), "last_seen": formatTime(lastSeen),
})
}
render(w, "vaults", vaults)
}
func handleVaultView(w http.ResponseWriter, r *http.Request) {
l0 := chi.URLParam(r, "l0")
var v struct {
L0, CustomerID, Status string
Created int
}
db.QueryRow("SELECT l0, customer_id, status, created_at FROM vaults WHERE l0 = ?", l0).Scan(
&v.L0, &v.CustomerID, &v.Status, &v.Created)
// Get credentials
rows, _ := db.Query("SELECT id, p0, wl3_path, created_at FROM credentials WHERE l0 = ?", l0)
defer rows.Close()
var creds []map[string]interface{}
for rows.Next() {
var id, p0, path string
var created int
rows.Scan(&id, &p0, &path, &created)
creds = append(creds, map[string]interface{}{
"id": id, "p0": p0, "path": path,
"created": time.Unix(int64(created), 0).Format("2006-01-02"),
})
}
render(w, "vault_view", map[string]interface{}{"vault": v, "credentials": creds})
}
// ========== DISCOUNTS (MSP Margins) ==========
func handleDiscountsList(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query(`
SELECT id, paddle_id, status, description, enabled_for_checkout,
code, type, mode, amount, currency_code, recur,
maximum_recurring_intervals, usage_limit, restrict_to,
expires_at, times_used, discount_group_id,
custom_data, created_at, updated_at,
sync_status, sync_error, last_paddle_sync_at
FROM discounts
ORDER BY created_at DESC
`)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer rows.Close()
var discounts []map[string]interface{}
for rows.Next() {
var id, paddleID, status, desc, dtype, mode, amount, currency string
var code, restrictTo, discountGroupID, customData, syncStatus string
var enabledForCheckout, recur int
var maxRecurring, usageLimit, timesUsed int
var created int64
rows.Scan(&id, &paddleID, &status, &desc, &enabledForCheckout,
&code, &dtype, &mode, &amount, &currency, &recur,
&maxRecurring, &usageLimit, &restrictTo,
new(int64), &timesUsed, &discountGroupID,
&customData, &created, new(int64),
&syncStatus, new(string), new(int64))
discounts = append(discounts, map[string]interface{}{
"id": id, "paddle_id": paddleID, "status": status,
"description": desc, "type": dtype, "amount": amount,
"currency": currency, "recur": recur == 1,
"usage_limit": usageLimit, "times_used": timesUsed,
"code": code, "sync_status": syncStatus,
"created": formatTime(created),
})
}
render(w, "discounts", discounts)
}
func handleDiscountForm(w http.ResponseWriter, r *http.Request) {
render(w, "discount_form", nil)
}
func handleDiscountCreate(w http.ResponseWriter, r *http.Request) {
desc := r.FormValue("description")
dtype := r.FormValue("type") // percentage or flat
amount := r.FormValue("amount")
mspID := r.FormValue("msp_id") // Optional: which MSP this is for
id := fmt.Sprintf("dsc_%d", time.Now().UnixNano())
customData := ""
if mspID != "" {
customData = fmt.Sprintf(`{"msp_id": "%s", "tier": "standard"}`, mspID)
}
_, err := db.Exec(`
INSERT INTO discounts (id, description, type, amount, status, custom_data, created_at)
VALUES (?, ?, ?, ?, 'active', ?, ?)
`, id, desc, dtype, amount, customData, time.Now().Unix())
if err != nil {
http.Error(w, err.Error(), 500)
return
}
http.Redirect(w, r, "/discounts", 302)
}
// ========== EVENTS / AUDIT LOG ==========
func handleEventsList(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query(`
SELECT e.id, e.occurred_at, e.event_type, e.entity_type, e.entity_id,
e.customer_id, e.human_id, e.vault_l0, e.amount_cents, e.currency_code,
e.details, e.source, e.ip_address, e.user_agent,
c.email as customer_email
FROM events e
LEFT JOIN customers c ON e.customer_id = c.id
ORDER BY e.occurred_at DESC
LIMIT 200
`)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer rows.Close()
var events []map[string]interface{}
for rows.Next() {
var id, eventType, entityType, entityID, customerID, vaultL0 string
var customerEmail string
var source, ipAddress, userAgent string
var occurred int64
var amountCents int
rows.Scan(&id, &occurred, &eventType, &entityType, &entityID,
&customerID, new(int64), &vaultL0, &amountCents, new(string),
new(string), &source, &ipAddress, &userAgent,
&customerEmail)
events = append(events, map[string]interface{}{
"id": id, "occurred": time.Unix(occurred, 0).Format("2006-01-02 15:04:05"),
"event": eventType, "entity": entityType, "entity_id": entityID,
"customer": customerEmail, "customer_id": customerID,
"vault_l0": vaultL0, "amount_cents": amountCents,
"source": source, "ip": ipAddress,
})
}
render(w, "events", events)
}
// ========== TEMPLATES ==========
func render(w http.ResponseWriter, name string, data interface{}) {
tmpl := template.Must(template.New(name).Parse(templates[name]))
tmpl.Execute(w, data)
}
var templates = map[string]string{
"home": `<!DOCTYPE html>
<html>
<head>
<title>Clavitor Admin</title>
<style>
body { font-family: system-ui; max-width: 1200px; margin: 0 auto; padding: 20px; }
nav { background: #333; padding: 15px; margin: -20px -20px 30px -20px; }
nav a { color: white; margin-right: 25px; text-decoration: none; font-weight: 500; }
h1 { margin-bottom: 10px; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 30px 0; }
.stat { background: #f5f5f5; padding: 25px; border-radius: 8px; text-align: center; }
.stat h2 { margin: 0; font-size: 42px; color: #0066cc; }
.stat p { margin: 10px 0 0 0; color: #666; font-size: 16px; }
.section { margin: 40px 0; }
.section h3 { border-bottom: 2px solid #eee; padding-bottom: 10px; }
</style>
</head>
<body>
<nav>
<a href="/">Dashboard</a>
<a href="/customers">Customers</a>
<a href="/subscriptions">Subscriptions</a>
<a href="/transactions">Transactions</a>
<a href="/vaults">Vaults</a>
<a href="/discounts">Discounts</a>
<a href="/events">Events</a>
</nav>
<h1>Clavitor Corporate Admin</h1>
<p>95% Paddle format + 5% our extensions</p>
<div class="stats">
<div class="stat"><h2>{{.customers}}</h2><p>Customers</p></div>
<div class="stat"><h2>{{.subscriptions}}</h2><p>Subscriptions</p></div>
<div class="stat"><h2>{{.transactions}}</h2><p>Transactions</p></div>
<div class="stat"><h2>{{.vaults}}</h2><p>Vaults</p></div>
</div>
<div class="section">
<h3>Quick Actions</h3>
<p><a href="/customers/new">+ New Customer</a> | <a href="/discounts/new">+ New Discount (MSP)</a></p>
</div>
</body>
</html>`,
"customers": `<!DOCTYPE html>
<html>
<head>
<title>Customers</title>
<style>
body { font-family: system-ui; max-width: 1400px; margin: 0 auto; padding: 20px; }
nav { background: #333; padding: 15px; margin: -20px -20px 30px -20px; }
nav a { color: white; margin-right: 25px; text-decoration: none; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 13px; }
th, td { text-align: left; padding: 10px; border-bottom: 1px solid #ddd; }
th { background: #f5f5f5; }
.btn { display: inline-block; padding: 4px 8px; background: #0066cc; color: white; text-decoration: none; border-radius: 4px; font-size: 12px; }
.level { display: inline-block; padding: 2px 6px; background: #eee; border-radius: 4px; font-size: 11px; }
.status { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 11px; }
.status.active { background: #d4edda; }
.status.archived { background: #f8d7da; }
.id { font-family: monospace; font-size: 11px; color: #666; }
.email { font-size: 12px; }
.count { display: inline-block; padding: 2px 6px; background: #e9ecef; border-radius: 10px; font-size: 11px; min-width: 20px; text-align: center; }
</style>
</head>
<body>
<nav>
<a href="/">Dashboard</a>
<a href="/customers">Customers</a>
<a href="/subscriptions">Subscriptions</a>
<a href="/transactions">Transactions</a>
<a href="/vaults">Vaults</a>
<a href="/discounts">Discounts</a>
<a href="/events">Events</a>
</nav>
<h1>Customers <a href="/customers/new" class="btn">+ New</a></h1>
<table>
<tr>
<th>ID</th>
<th>Email</th>
<th>Name</th>
<th>Locale</th>
<th>Status</th>
<th>Marketing</th>
<th>Level</th>
<th>Parent</th>
<th>Billing Email</th>
<th>Subs</th>
<th>Addrs</th>
<th>Vaults</th>
<th>Created</th>
<th>Updated</th>
<th>Actions</th>
</tr>
{{range .}}
<tr>
<td class="id">{{.id}}</td>
<td class="email">{{.email}}</td>
<td>{{.name}}</td>
<td>{{.locale}}</td>
<td><span class="status {{.status}}">{{.status}}</span></td>
<td>{{if .marketing_consent}}Yes{{else}}No{{end}}</td>
<td><span class="level">L{{.level}}</span></td>
<td>{{if .parent_email}}{{.parent_email}}{{else}}-{{end}}</td>
<td>{{if .billing_email}}{{.billing_email}}{{else}}-{{end}}</td>
<td><span class="count">{{.subs}}</span></td>
<td><span class="count">{{.addresses}}</span></td>
<td><span class="count">{{.vaults}}</span></td>
<td>{{.created}}</td>
<td>{{.updated}}</td>
<td><a href="/customers/{{.id}}" class="btn">View</a></td>
</tr>
{{end}}
</table>
</body>
</html>`,
"customer_view": `<!DOCTYPE html>
<html>
<head>
<title>Customer</title>
<style>
body { font-family: system-ui; max-width: 1200px; margin: 0 auto; padding: 20px; }
nav { background: #333; padding: 15px; margin: -20px -20px 30px -20px; }
nav a { color: white; margin-right: 25px; text-decoration: none; }
.section { background: #f5f5f5; padding: 20px; margin: 20px 0; border-radius: 8px; }
h2 { margin-top: 30px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 10px; border-bottom: 1px solid #ddd; }
</style>
</head>
<body>
<nav>
<a href="/">Dashboard</a>
<a href="/customers">Customers</a>
<a href="/subscriptions">Subscriptions</a>
<a href="/transactions">Transactions</a>
<a href="/vaults">Vaults</a>
<a href="/discounts">Discounts</a>
<a href="/events">Events</a>
</nav>
<h1>Customer: {{.customer.Email}}</h1>
<div class="section">
<p><strong>ID:</strong> {{.customer.ID}}</p>
<p><strong>Name:</strong> {{.customer.Name}}</p>
<p><strong>Status:</strong> {{.customer.Status}}</p>
<p><strong>Level:</strong> {{.customer.Level}}</p>
<p><strong>Created:</strong> {{.customer.CreatedAt}}</p>
</div>
<h2>Subscriptions</h2>
<table>
<tr><th>ID</th><th>Plan</th><th>Status</th><th>Currency</th><th>Next Bill</th></tr>
{{range .subscriptions}}
<tr>
<td>{{.id}}</td>
<td>{{.plan}}</td>
<td>{{.status}}</td>
<td>{{.currency}}</td>
<td>{{.next_billed}}</td>
</tr>
{{end}}
</table>
<h2>Addresses</h2>
<table>
<tr><th>ID</th><th>Country</th><th>City</th></tr>
{{range .addresses}}
<tr><td>{{.id}}</td><td>{{.country}}</td><td>{{.city}}</td></tr>
{{end}}
</table>
</body>
</html>`,
"customer_form": `<!DOCTYPE html>
<html>
<head>
<title>New Customer</title>
<style>
body { font-family: system-ui; max-width: 600px; margin: 50px auto; padding: 20px; }
nav { background: #333; padding: 15px; margin: -20px -20px 30px -20px; }
nav a { color: white; margin-right: 25px; text-decoration: none; }
label { display: block; margin: 20px 0 5px 0; font-weight: 500; }
input, select { width: 100%; padding: 10px; font-size: 16px; }
button { margin-top: 25px; padding: 12px 30px; font-size: 16px; background: #0066cc; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>
</head>
<body>
<nav>
<a href="/">Dashboard</a>
<a href="/customers">Customers</a>
<a href="/subscriptions">Subscriptions</a>
<a href="/transactions">Transactions</a>
<a href="/vaults">Vaults</a>
<a href="/discounts">Discounts</a>
<a href="/events">Events</a>
</nav>
<h1>New Customer</h1>
<form method="post" action="/customers">
<label>Email:</label>
<input type="email" name="email" required>
<label>Name:</label>
<input type="text" name="name">
<label>Parent (MSP):</label>
<select name="parent_id">
<option value="">None (Root)</option>
{{range .}}
<option value="{{.id}}">{{.email}} ({{.name}})</option>
{{end}}
</select>
<button type="submit">Create Customer</button>
</form>
</body>
</html>`,
"subscriptions": `<!DOCTYPE html>
<html>
<head>
<title>Subscriptions</title>
<style>
body { font-family: system-ui; max-width: 1600px; margin: 0 auto; padding: 20px; }
nav { background: #333; padding: 15px; margin: -20px -20px 30px -20px; }
nav a { color: white; margin-right: 25px; text-decoration: none; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 12px; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }
th { background: #f5f5f5; }
.status { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 11px; }
.status.active { background: #d4edda; }
.status.past_due { background: #f8d7da; }
.status.paused { background: #fff3cd; }
.status.canceled { background: #f5f5f5; }
.id { font-family: monospace; font-size: 10px; color: #666; max-width: 120px; overflow: hidden; text-overflow: ellipsis; }
.mode { font-size: 11px; }
.mode.automatic { color: #28a745; }
.mode.manual { color: #fd7e14; }
.sync { font-size: 11px; }
.sync.synced { color: #28a745; }
.sync.pending { color: #fd7e14; }
.sync.error { color: #dc3545; }
</style>
</head>
<body>
<nav>
<a href="/">Dashboard</a>
<a href="/customers">Customers</a>
<a href="/subscriptions">Subscriptions</a>
<a href="/transactions">Transactions</a>
<a href="/vaults">Vaults</a>
<a href="/discounts">Discounts</a>
<a href="/events">Events</a>
</nav>
<h1>Subscriptions</h1>
<table>
<tr>
<th>ID</th>
<th>Paddle ID</th>
<th>Customer</th>
<th>Plan</th>
<th>Currency</th>
<th>Status</th>
<th>Mode</th>
<th>Vault</th>
<th>Items</th>
<th>Sync</th>
<th>Created</th>
<th>Next Bill</th>
</tr>
{{range .}}
<tr>
<td class="id">{{.id}}</td>
<td class="id">{{.paddle_id}}</td>
<td>{{.customer}}</td>
<td>{{.plan}}</td>
<td>{{.currency}}</td>
<td><span class="status {{.status}}">{{.status}}</span></td>
<td><span class="mode {{.mode}}">{{.mode}}</span></td>
<td class="id">{{.vault}}</td>
<td>{{.items_count}}</td>
<td><span class="sync {{.sync_status}}">{{.sync_status}}</span></td>
<td>{{.created}}</td>
<td>{{.next_bill}}</td>
</tr>
{{end}}
</table>
</body>
</html>`,
"subscription_view": `<!DOCTYPE html>
<html>
<head>
<title>Subscription</title>
<style>
body { font-family: system-ui; max-width: 800px; margin: 0 auto; padding: 20px; }
nav { background: #333; padding: 15px; margin: -20px -20px 30px -20px; }
nav a { color: white; margin-right: 25px; text-decoration: none; }
.section { background: #f5f5f5; padding: 20px; margin: 20px 0; border-radius: 8px; }
pre { background: white; padding: 15px; overflow-x: auto; }
</style>
</head>
<body>
<nav>
<a href="/">Dashboard</a>
<a href="/customers">Customers</a>
<a href="/subscriptions">Subscriptions</a>
<a href="/transactions">Transactions</a>
<a href="/vaults">Vaults</a>
<a href="/discounts">Discounts</a>
<a href="/events">Events</a>
</nav>
<h1>Subscription: {{.ID}}</h1>
<div class="section">
<p><strong>Customer ID:</strong> {{.CustomerID}}</p>
<p><strong>Address ID:</strong> {{.AddressID}}</p>
<p><strong>Status:</strong> {{.Status}}</p>
<p><strong>Plan:</strong> {{.Plan}}</p>
<p><strong>Currency:</strong> {{.Currency}}</p>
<p><strong>Collection Mode:</strong> {{.Mode}}</p>
<p><strong>Vault L0:</strong> {{.VaultL0}}</p>
<p><strong>Created:</strong> {{.Created}}</p>
<p><strong>Next Bill:</strong> {{.NextBilled}}</p>
</div>
<div class="section">
<h3>Items (JSON)</h3>
<pre>{{.Items}}</pre>
</div>
</body>
</html>`,
"transactions": `<!DOCTYPE html>
<html>
<head>
<title>Transactions</title>
<style>
body { font-family: system-ui; max-width: 1600px; margin: 0 auto; padding: 20px; }
nav { background: #333; padding: 15px; margin: -20px -20px 30px -20px; }
nav a { color: white; margin-right: 25px; text-decoration: none; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 12px; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }
th { background: #f5f5f5; }
.amount { font-weight: 500; text-align: right; }
.status { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 11px; }
.status.ready { background: #fff3cd; }
.status.billed { background: #cce5ff; }
.status.completed { background: #d4edda; }
.status.past_due { background: #f8d7da; }
.status.canceled { background: #f5f5f5; }
.id { font-family: monospace; font-size: 10px; color: #666; max-width: 100px; overflow: hidden; text-overflow: ellipsis; }
.origin { font-size: 11px; color: #666; }
.bool { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 11px; }
.bool.yes { background: #d4edda; }
.bool.no { background: #f5f5f5; }
</style>
</head>
<body>
<nav>
<a href="/">Dashboard</a>
<a href="/customers">Customers</a>
<a href="/subscriptions">Subscriptions</a>
<a href="/transactions">Transactions</a>
<a href="/vaults">Vaults</a>
<a href="/discounts">Discounts</a>
<a href="/events">Events</a>
</nav>
<h1>Transactions</h1>
<table>
<tr>
<th>ID</th>
<th>Paddle ID</th>
<th>Customer</th>
<th>Amount</th>
<th>Status</th>
<th>Origin</th>
<th>Invoice</th>
<th>Checkout</th>
<th>Created</th>
<th>Billed</th>
</tr>
{{range .}}
<tr>
<td class="id">{{.id}}</td>
<td class="id">{{.paddle_id}}</td>
<td>{{.customer}}</td>
<td class="amount">{{.currency}} {{printf "%.2f" .amount}}</td>
<td><span class="status {{.status}}">{{.status}}</span></td>
<td><span class="origin">{{.origin}}</span></td>
<td>{{if .invoice}}{{.invoice}}{{else}}-{{end}}</td>
<td><span class="bool {{if .checkout}}yes{{else}}no{{end}}">{{if .checkout}}Yes{{else}}No{{end}}</span></td>
<td>{{.created}}</td>
<td>{{.billed}}</td>
</tr>
{{end}}
</table>
</body>
</html>`,
"transaction_view": `<!DOCTYPE html>
<html>
<head>
<title>Transaction</title>
<style>
body { font-family: system-ui; max-width: 800px; margin: 0 auto; padding: 20px; }
nav { background: #333; padding: 15px; margin: -20px -20px 30px -20px; }
nav a { color: white; margin-right: 25px; text-decoration: none; }
.section { background: #f5f5f5; padding: 20px; margin: 20px 0; border-radius: 8px; }
</style>
</head>
<body>
<nav>
<a href="/">Dashboard</a>
<a href="/customers">Customers</a>
<a href="/subscriptions">Subscriptions</a>
<a href="/transactions">Transactions</a>
<a href="/vaults">Vaults</a>
<a href="/discounts">Discounts</a>
<a href="/events">Events</a>
</nav>
<h1>Transaction: {{.ID}}</h1>
<div class="section">
<p><strong>Status:</strong> {{.Status}}</p>
<p><strong>Amount:</strong> {{.Currency}} {{printf "%.2f" .AmountCents}}</p>
<p><strong>Invoice:</strong> {{.Invoice}}</p>
<p><strong>Created:</strong> {{.Created}}</p>
</div>
</body>
</html>`,
"vaults": `<!DOCTYPE html>
<html>
<head>
<title>Vaults</title>
<style>
body { font-family: system-ui; max-width: 1400px; margin: 0 auto; padding: 20px; }
nav { background: #333; padding: 15px; margin: -20px -20px 30px -20px; }
nav a { color: white; margin-right: 25px; text-decoration: none; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 12px; }
th, td { text-align: left; padding: 10px; border-bottom: 1px solid #ddd; }
th { background: #f5f5f5; }
.l0 { font-family: monospace; font-size: 12px; }
.id { font-family: monospace; font-size: 10px; color: #666; max-width: 100px; overflow: hidden; text-overflow: ellipsis; }
.status { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 11px; }
.status.active { background: #d4edda; }
.status.suspended { background: #fff3cd; }
.status.archived { background: #f8d7da; }
.devices { display: inline-block; padding: 2px 8px; background: #e9ecef; border-radius: 10px; font-size: 11px; }
</style>
</head>
<body>
<nav>
<a href="/">Dashboard</a>
<a href="/customers">Customers</a>
<a href="/subscriptions">Subscriptions</a>
<a href="/transactions">Transactions</a>
<a href="/vaults">Vaults</a>
<a href="/discounts">Discounts</a>
<a href="/events">Events</a>
</nav>
<h1>Vaults</h1>
<table>
<tr>
<th>L0</th>
<th>Customer</th>
<th>Customer ID</th>
<th>Subscription ID</th>
<th>Devices</th>
<th>Status</th>
<th>Created</th>
<th>Last Seen</th>
</tr>
{{range .}}
<tr>
<td class="l0"><a href="/vaults/{{.l0}}">{{.l0}}</a></td>
<td>{{.customer}}</td>
<td class="id">{{.customer_id}}</td>
<td class="id">{{if .subscription_id}}{{.subscription_id}}{{else}}-{{end}}</td>
<td><span class="devices">{{.devices}}</span></td>
<td><span class="status {{.status}}">{{.status}}</span></td>
<td>{{.created}}</td>
<td>{{.last_seen}}</td>
</tr>
{{end}}
</table>
</body>
</html>`,
"vault_view": `<!DOCTYPE html>
<html>
<head>
<title>Vault</title>
<style>
body { font-family: system-ui; max-width: 800px; margin: 0 auto; padding: 20px; }
nav { background: #333; padding: 15px; margin: -20px -20px 30px -20px; }
nav a { color: white; margin-right: 25px; text-decoration: none; }
.section { background: #f5f5f5; padding: 20px; margin: 20px 0; border-radius: 8px; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 10px; border-bottom: 1px solid #ddd; }
.p0 { font-family: monospace; }
</style>
</head>
<body>
<nav>
<a href="/">Dashboard</a>
<a href="/customers">Customers</a>
<a href="/subscriptions">Subscriptions</a>
<a href="/transactions">Transactions</a>
<a href="/vaults">Vaults</a>
<a href="/discounts">Discounts</a>
<a href="/events">Events</a>
</nav>
<h1>Vault: {{.vault.L0}}</h1>
<div class="section">
<p><strong>Customer ID:</strong> {{.vault.CustomerID}}</p>
<p><strong>Status:</strong> {{.vault.Status}}</p>
<p><strong>Created:</strong> {{.vault.Created}}</p>
</div>
<h2>Credentials (WL3s)</h2>
<table>
<tr><th>ID</th><th>P0</th><th>Path</th><th>Created</th></tr>
{{range .credentials}}
<tr>
<td>{{.id}}</td>
<td class="p0">{{.p0}}</td>
<td>{{.path}}</td>
<td>{{.created}}</td>
</tr>
{{end}}
</table>
</body>
</html>`,
"discounts": `<!DOCTYPE html>
<html>
<head>
<title>Discounts</title>
<style>
body { font-family: system-ui; max-width: 1600px; margin: 0 auto; padding: 20px; }
nav { background: #333; padding: 15px; margin: -20px -20px 30px -20px; }
nav a { color: white; margin-right: 25px; text-decoration: none; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 11px; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }
th { background: #f5f5f5; }
.btn { display: inline-block; padding: 6px 12px; background: #0066cc; color: white; text-decoration: none; border-radius: 4px; font-size: 12px; }
.id { font-family: monospace; font-size: 10px; color: #666; max-width: 80px; overflow: hidden; text-overflow: ellipsis; }
.status { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 11px; }
.status.active { background: #d4edda; }
.status.archived { background: #f8d7da; }
.type { display: inline-block; padding: 2px 6px; background: #e9ecef; border-radius: 4px; font-size: 11px; }
.recur { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 11px; }
.recur.yes { background: #d4edda; }
.recur.no { background: #f5f5f5; }
.sync { font-size: 11px; }
.sync.synced { color: #28a745; }
.sync.pending { color: #fd7e14; }
.usage { font-family: monospace; font-size: 11px; }
</style>
</head>
<body>
<nav>
<a href="/">Dashboard</a>
<a href="/customers">Customers</a>
<a href="/subscriptions">Subscriptions</a>
<a href="/transactions">Transactions</a>
<a href="/vaults">Vaults</a>
<a href="/discounts">Discounts</a>
<a href="/events">Events</a>
</nav>
<h1>Discounts <a href="/discounts/new" class="btn">+ New MSP Discount</a></h1>
<p>Use for MSP margins and promotional discounts</p>
<table>
<tr>
<th>ID</th>
<th>Paddle ID</th>
<th>Description</th>
<th>Type</th>
<th>Amount</th>
<th>Currency</th>
<th>Recur</th>
<th>Usage</th>
<th>Code</th>
<th>Sync</th>
<th>Created</th>
</tr>
{{range .}}
<tr>
<td class="id">{{.id}}</td>
<td class="id">{{.paddle_id}}</td>
<td>{{.description}}</td>
<td><span class="type">{{.type}}</span></td>
<td>{{.amount}}</td>
<td>{{.currency}}</td>
<td><span class="recur {{if .recur}}yes{{else}}no{{end}}">{{if .recur}}Yes{{else}}No{{end}}</span></td>
<td class="usage">{{.times_used}}/{{if .usage_limit}}{{.usage_limit}}{{else}}{{end}}</td>
<td>{{if .code}}{{.code}}{{else}}-{{end}}</td>
<td><span class="sync {{.sync_status}}">{{.sync_status}}</span></td>
<td>{{.created}}</td>
</tr>
{{end}}
</table>
</body>
</html>`,
"discount_form": `<!DOCTYPE html>
<html>
<head>
<title>New Discount</title>
<style>
body { font-family: system-ui; max-width: 600px; margin: 50px auto; padding: 20px; }
nav { background: #333; padding: 15px; margin: -20px -20px 30px -20px; }
nav a { color: white; margin-right: 25px; text-decoration: none; }
label { display: block; margin: 20px 0 5px 0; font-weight: 500; }
input, select, textarea { width: 100%; padding: 10px; font-size: 16px; }
button { margin-top: 25px; padding: 12px 30px; font-size: 16px; background: #0066cc; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>
</head>
<body>
<nav>
<a href="/">Dashboard</a>
<a href="/customers">Customers</a>
<a href="/subscriptions">Subscriptions</a>
<a href="/transactions">Transactions</a>
<a href="/vaults">Vaults</a>
<a href="/discounts">Discounts</a>
<a href="/events">Events</a>
</nav>
<h1>New Discount (MSP Margin)</h1>
<form method="post" action="/discounts">
<label>Description:</label>
<input type="text" name="description" placeholder="e.g., Johan MSP - 30% reseller margin" required>
<label>Type:</label>
<select name="type">
<option value="percentage">Percentage</option>
<option value="flat">Flat Amount</option>
</select>
<label>Amount:</label>
<input type="text" name="amount" placeholder="30 for 30% or 1000 for $10.00" required>
<label>MSP Customer ID (optional):</label>
<input type="text" name="msp_id" placeholder="ctm_xxx - links discount to MSP">
<button type="submit">Create Discount</button>
</form>
</body>
</html>`,
"events": `<!DOCTYPE html>
<html>
<head>
<title>Events</title>
<style>
body { font-family: system-ui; max-width: 1600px; margin: 0 auto; padding: 20px; }
nav { background: #333; padding: 15px; margin: -20px -20px 30px -20px; }
nav a { color: white; margin-right: 25px; text-decoration: none; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 12px; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }
th { background: #f5f5f5; }
.time { font-family: monospace; white-space: nowrap; }
.id { font-family: monospace; font-size: 10px; color: #666; max-width: 100px; overflow: hidden; text-overflow: ellipsis; }
.source { display: inline-block; padding: 2px 6px; background: #e9ecef; border-radius: 4px; font-size: 11px; }
</style>
</head>
<body>
<nav>
<a href="/">Dashboard</a>
<a href="/customers">Customers</a>
<a href="/subscriptions">Subscriptions</a>
<a href="/transactions">Transactions</a>
<a href="/vaults">Vaults</a>
<a href="/discounts">Discounts</a>
<a href="/events">Events</a>
</nav>
<h1>Events (Audit Log)</h1>
<table>
<tr>
<th>Time</th>
<th>Event</th>
<th>Entity</th>
<th>Entity ID</th>
<th>Customer</th>
<th>Customer ID</th>
<th>Vault</th>
<th>Amount</th>
<th>Source</th>
<th>IP</th>
</tr>
{{range .}}
<tr>
<td class="time">{{.occurred}}</td>
<td>{{.event}}</td>
<td>{{.entity}}</td>
<td class="id">{{.entity_id}}</td>
<td>{{if .customer}}{{.customer}}{{else}}-{{end}}</td>
<td class="id">{{.customer_id}}</td>
<td class="id">{{if .vault_l0}}{{.vault_l0}}{{else}}-{{end}}</td>
<td>{{if .amount_cents}}{{.amount_cents}}{{else}}-{{end}}</td>
<td><span class="source">{{.source}}</span></td>
<td class="id">{{if .ip}}{{.ip}}{{else}}-{{end}}</td>
</tr>
{{end}}
</table>
</body>
</html>`,
}
func formatTime(unix int64) string {
if unix == 0 {
return "-"
}
return time.Unix(unix, 0).Format("2006-01-02")
}