Files
boc/aamos-admin-upgrade/backend/handlers/external.go
T
Bernt 6de2455917 v1.2.0: Add Global Markets footer, translated to 9 languages
- Added GLOBAL_MARKETS_TITLE to all translation files
- Updated footer with 12 markets (4 active + 8 upcoming)
- Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi
- Built and deployed to production
- CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
2026-07-08 19:56:03 +00:00

420 lines
12 KiB
Go

package handlers
import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
_ "modernc.org/sqlite"
)
// ExternalDataHandler aggregates data from quiXzoom, LandveX, ledger, and CRM.
type ExternalDataHandler struct {
DB *sql.DB
AMOSBaseURL string
}
// UnifiedDashboardResponse is the enriched dashboard with external systems.
type UnifiedDashboardResponse struct {
// Core system
UsersCount int `json:"users_count"`
ModulesHealthy int `json:"modules_healthy"`
ModulesTotal int `json:"modules_total"`
RecentAudit []AuditEntry `json:"recent_audit"`
SystemMetrics SystemMetricsData `json:"system_metrics"`
// quiXzoom
Quixzoom QuixzoomData `json:"quixzoom"`
// LandveX
Landvex LandvexData `json:"landvex"`
// Economy / Ledger
Economy EconomyData `json:"economy"`
// CRM
CRM CRMData `json:"crm"`
// Alerts
Alerts []Alert `json:"alerts"`
}
type SystemMetricsData struct {
CPU CPUMetrics `json:"cpu"`
RAM RAMMetrics `json:"ram"`
Disk DiskMetrics `json:"disk"`
}
type QuixzoomData struct {
TotalMissions int `json:"total_missions"`
ActiveMissions int `json:"active_missions"`
TotalSubmissions int `json:"total_submissions"`
Contributors int `json:"contributors"`
PendingPayouts int `json:"pending_payouts"`
TotalPayoutsSEK int `json:"total_payouts_sek"`
}
type LandvexData struct {
IntelligenceReports int `json:"intelligence_reports"`
UrbanDataPoints int `json:"urban_data_points"`
ActiveCustomers int `json:"active_customers"`
}
type EconomyData struct {
TotalAccounts int `json:"total_accounts"`
TotalVouchers int `json:"total_vouchers"`
TrialBalance float64 `json:"trial_balance"`
PendingInvoices int `json:"pending_invoices"`
CashSEK float64 `json:"cash_sek"`
CashEUR float64 `json:"cash_eur"`
CashUSD float64 `json:"cash_usd"`
}
type CRMData struct {
TotalCustomers int `json:"total_customers"`
ActiveLeads int `json:"active_leads"`
DealsWon int `json:"deals_won"`
DealsLost int `json:"deals_lost"`
PipelineValueSEK int `json:"pipeline_value_sek"`
}
type Alert struct {
Level string `json:"level"` // "critical", "warning", "info"
Message string `json:"message"`
Deadline time.Time `json:"deadline,omitempty"`
AmountSEK float64 `json:"amount_sek,omitempty"`
}
// UnifiedDashboardHandler returns a comprehensive dashboard with all systems.
func UnifiedDashboardHandler(db *sql.DB, amosBaseURL string) http.HandlerFunc {
h := &ExternalDataHandler{DB: db, AMOSBaseURL: amosBaseURL}
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
resp, err := h.buildDashboard(r)
if err != nil {
// Return partial data with error indication
resp = &UnifiedDashboardResponse{
Alerts: []Alert{{
Level: "warning",
Message: fmt.Sprintf("Partial data: %v", err),
}},
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
}
func (h *ExternalDataHandler) buildDashboard(r *http.Request) (*UnifiedDashboardResponse, error) {
resp := &UnifiedDashboardResponse{}
// 1. Core metrics
if err := h.loadCoreMetrics(r, resp); err != nil {
return resp, fmt.Errorf("core metrics: %w", err)
}
// 2. quiXzoom data from SQLite
if err := h.loadQuixzoomData(resp); err != nil {
resp.Alerts = append(resp.Alerts, Alert{
Level: "warning",
Message: fmt.Sprintf("quiXzoom data unavailable: %v", err),
})
}
// 3. LandveX data
if err := h.loadLandvexData(resp); err != nil {
resp.Alerts = append(resp.Alerts, Alert{
Level: "warning",
Message: fmt.Sprintf("LandveX data unavailable: %v", err),
})
}
// 4. Economy / Ledger
if err := h.loadEconomyData(resp); err != nil {
resp.Alerts = append(resp.Alerts, Alert{
Level: "warning",
Message: fmt.Sprintf("Economy data unavailable: %v", err),
})
}
// 5. CRM
if err := h.loadCRMData(resp); err != nil {
resp.Alerts = append(resp.Alerts, Alert{
Level: "warning",
Message: fmt.Sprintf("CRM data unavailable: %v", err),
})
}
// 6. Hardcoded alerts (from MEMORY.md)
resp.Alerts = append(resp.Alerts,
Alert{
Level: "critical",
Message: "Momsdeklaration deadline",
Deadline: time.Date(2026, 7, 26, 0, 0, 0, 0, time.UTC),
AmountSEK: 442000,
},
Alert{
Level: "critical",
Message: "Leon kravbrev deadline",
Deadline: time.Date(2026, 7, 13, 0, 0, 0, 0, time.UTC),
AmountSEK: 197922,
},
)
return resp, nil
}
func (h *ExternalDataHandler) loadCoreMetrics(r *http.Request, resp *UnifiedDashboardResponse) error {
var usersCount int
if err := h.DB.QueryRowContext(r.Context(), `SELECT COUNT(*) FROM users`).Scan(&usersCount); err != nil {
return err
}
var modulesTotal, modulesHealthy int
if err := h.DB.QueryRowContext(r.Context(), `SELECT COUNT(*) FROM modules`).Scan(&modulesTotal); err != nil {
return err
}
if err := h.DB.QueryRowContext(r.Context(),
`SELECT COUNT(*) FROM modules WHERE status = 'running'`).Scan(&modulesHealthy); err != nil {
return err
}
rows, err := h.DB.QueryContext(r.Context(), `
SELECT id, user_id, action, resource, ip, timestamp, details
FROM audit_logs
ORDER BY timestamp DESC
LIMIT 5
`)
if err != nil {
return err
}
defer rows.Close()
audit := make([]AuditEntry, 0, 5)
for rows.Next() {
var a AuditEntry
if err := rows.Scan(&a.ID, &a.UserID, &a.Action, &a.Resource, &a.IP, &a.Timestamp, &a.Details); err != nil {
continue
}
audit = append(audit, a)
}
cpu, _ := readCPU()
ram, _ := readRAM()
disk, _ := readDisk("/")
resp.UsersCount = usersCount
resp.ModulesHealthy = modulesHealthy
resp.ModulesTotal = modulesTotal
resp.RecentAudit = audit
resp.SystemMetrics = SystemMetricsData{CPU: cpu, RAM: ram, Disk: disk}
return nil
}
func (h *ExternalDataHandler) loadQuixzoomData(resp *UnifiedDashboardResponse) error {
// Try to read from quiXzoom SQLite database
dbPath := "/opt/amos/data/quixzoom-missions.db"
qzDB, err := sql.Open("sqlite", dbPath)
if err != nil {
return err
}
defer qzDB.Close()
var totalMissions, activeMissions, totalSubmissions, contributors, pendingPayouts int
qzDB.QueryRow(`SELECT COUNT(*) FROM missions`).Scan(&totalMissions)
qzDB.QueryRow(`SELECT COUNT(*) FROM missions WHERE status = 'active'`).Scan(&activeMissions)
qzDB.QueryRow(`SELECT COUNT(*) FROM submissions`).Scan(&totalSubmissions)
qzDB.QueryRow(`SELECT COUNT(DISTINCT zoomer_id) FROM submissions`).Scan(&contributors)
qzDB.QueryRow(`SELECT COUNT(*) FROM payouts WHERE status = 'pending'`).Scan(&pendingPayouts)
resp.Quixzoom = QuixzoomData{
TotalMissions: totalMissions,
ActiveMissions: activeMissions,
TotalSubmissions: totalSubmissions,
Contributors: contributors,
PendingPayouts: pendingPayouts,
}
return nil
}
func (h *ExternalDataHandler) loadLandvexData(resp *UnifiedDashboardResponse) error {
// Read from landvex intelligence files
landvexPath := "/opt/amos/public/landvex"
files := 0
if entries, err := os.ReadDir(landvexPath); err == nil {
for _, entry := range entries {
if !entry.IsDir() {
files++
}
}
}
// Count intelligence reports (JSON files)
reports := 0
_ = filepath.Walk(landvexPath, func(path string, info os.FileInfo, err error) error {
if err == nil && !info.IsDir() && strings.HasSuffix(path, ".json") {
reports++
}
return nil
})
resp.Landvex = LandvexData{
IntelligenceReports: reports,
UrbanDataPoints: files,
ActiveCustomers: 1, // Trygg Bil
}
return nil
}
func (h *ExternalDataHandler) loadEconomyData(resp *UnifiedDashboardResponse) error {
// Try to connect to aamos-ledger on port 3250
ledgerURL := os.Getenv("LEDGER_URL")
if ledgerURL == "" {
ledgerURL = "http://localhost:3250"
}
// Try to fetch from ledger API
client := &http.Client{Timeout: 5 * time.Second}
ledgerResp, err := client.Get(ledgerURL + "/api/v1/vouchers/count")
if err == nil && ledgerResp.StatusCode == 200 {
defer ledgerResp.Body.Close()
var result struct {
Count int `json:"count"`
}
if err := json.NewDecoder(ledgerResp.Body).Decode(&result); err == nil {
resp.Economy.TotalVouchers = result.Count
}
}
// Also try to get accounts count
ledgerResp2, err := client.Get(ledgerURL + "/api/v1/accounts/count")
if err == nil && ledgerResp2.StatusCode == 200 {
defer ledgerResp2.Body.Close()
var result struct {
Count int `json:"count"`
}
if err := json.NewDecoder(ledgerResp2.Body).Decode(&result); err == nil {
resp.Economy.TotalAccounts = result.Count
}
}
// Fallback to local SQLite if ledger API fails
if resp.Economy.TotalVouchers == 0 {
dbPath := "/opt/amos/data/ledger.db"
ledgerDB, err := sql.Open("sqlite", dbPath)
if err == nil {
defer ledgerDB.Close()
ledgerDB.QueryRow(`SELECT COUNT(*) FROM vouchers`).Scan(&resp.Economy.TotalVouchers)
ledgerDB.QueryRow(`SELECT COUNT(*) FROM accounts`).Scan(&resp.Economy.TotalAccounts)
}
}
// Hardcoded cash position from MEMORY.md (2026-06-21)
resp.Economy.CashSEK = 276504
resp.Economy.CashEUR = 179.70
resp.Economy.CashUSD = 14.70
return nil
}
func (h *ExternalDataHandler) loadCRMData(resp *UnifiedDashboardResponse) error {
// Try to read from CRM database or API
// For now, placeholder with reasonable defaults
resp.CRM = CRMData{
TotalCustomers: 1, // Trygg Bil
ActiveLeads: 0,
DealsWon: 1,
DealsLost: 0,
PipelineValueSEK: 0,
}
return nil
}
type ServiceStatus struct {
Name string `json:"name"`
Status string `json:"status"`
Type string `json:"type"`
Uptime string `json:"uptime"`
LastCheck string `json:"last_check"`
}
// ServiceHealthHandler returns health status for all Docker containers and systemd services.
func ServiceHealthHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var services []ServiceStatus
// Get Docker containers
cmd := exec.Command("docker", "ps", "--format", "{{.Names}}|{{.Status}}|{{.Image}}")
out, err := cmd.Output()
if err == nil {
for _, line := range strings.Split(string(out), "\n") {
parts := strings.Split(line, "|")
if len(parts) >= 2 {
status := "running"
if strings.Contains(parts[1], "unhealthy") {
status = "degraded"
}
services = append(services, ServiceStatus{
Name: parts[0],
Status: status,
Type: "docker",
Uptime: parts[1],
LastCheck: time.Now().UTC().Format(time.RFC3339),
})
}
}
}
// Get systemd services (aamos-*)
cmd = exec.Command("systemctl", "list-units", "--type=service", "--state=running", "--no-pager", "--no-legend")
out, err = cmd.Output()
if err == nil {
for _, line := range strings.Split(string(out), "\n") {
if strings.Contains(line, "aamos-") || strings.Contains(line, "landvex") || strings.Contains(line, "quixzoom") {
fields := strings.Fields(line)
if len(fields) >= 2 {
services = append(services, ServiceStatus{
Name: fields[0],
Status: "running",
Type: "systemd",
Uptime: strings.Join(fields[2:], " "),
LastCheck: time.Now().UTC().Format(time.RFC3339),
})
}
}
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"services": services,
"total": len(services),
"healthy": countHealthy(services),
})
}
func countHealthy(services []ServiceStatus) int {
count := 0
for _, s := range services {
if s.Status == "running" {
count++
}
}
return count
}