package main import ( "context" "fmt" "log" "sync" "time" "forge.lthn.ai/lthn/lem/pkg/lem" "github.com/wailsapp/wails/v3/pkg/application" ) // DashboardService bridges pkg/lem CLI functions for the desktop UI. // Provides real-time status, model inventory, and scoring progress // to the frontend via Wails bindings. type DashboardService struct { influx *lem.InfluxClient dbPath string mu sync.RWMutex // Cached state (refreshed periodically). trainingStatus []TrainingRow generationStats GenerationStats modelInventory []ModelInfo lastRefresh time.Time } // TrainingRow represents a single model's training progress. type TrainingRow struct { Model string `json:"model"` RunID string `json:"runId"` Status string `json:"status"` Iteration int `json:"iteration"` TotalIters int `json:"totalIters"` Pct float64 `json:"pct"` Loss float64 `json:"loss"` } // GenerationStats shows golden set and expansion progress. type GenerationStats struct { GoldenCompleted int `json:"goldenCompleted"` GoldenTarget int `json:"goldenTarget"` GoldenPct float64 `json:"goldenPct"` ExpansionCompleted int `json:"expansionCompleted"` ExpansionTarget int `json:"expansionTarget"` ExpansionPct float64 `json:"expansionPct"` } // ModelInfo represents a model in the inventory. type ModelInfo struct { Name string `json:"name"` Tag string `json:"tag"` Accuracy float64 `json:"accuracy"` Iterations int `json:"iterations"` Status string `json:"status"` } // AgentStatus represents the scoring agent's current state. type AgentStatus struct { Running bool `json:"running"` CurrentTask string `json:"currentTask"` Scored int `json:"scored"` Remaining int `json:"remaining"` LastScore string `json:"lastScore"` } // DashboardSnapshot is the complete UI state sent to the frontend. type DashboardSnapshot struct { Training []TrainingRow `json:"training"` Generation GenerationStats `json:"generation"` Models []ModelInfo `json:"models"` Agent AgentStatus `json:"agent"` DBPath string `json:"dbPath"` UpdatedAt string `json:"updatedAt"` } // NewDashboardService creates a DashboardService. func NewDashboardService(influxURL, influxDB, dbPath string) *DashboardService { return &DashboardService{ influx: lem.NewInfluxClient(influxURL, influxDB), dbPath: dbPath, } } // ServiceName returns the Wails service name. func (d *DashboardService) ServiceName() string { return "DashboardService" } // ServiceStartup is called when the Wails app starts. func (d *DashboardService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { log.Println("DashboardService started") go d.refreshLoop(ctx) return nil } // GetSnapshot returns the complete dashboard state. func (d *DashboardService) GetSnapshot() DashboardSnapshot { d.mu.RLock() defer d.mu.RUnlock() return DashboardSnapshot{ Training: d.trainingStatus, Generation: d.generationStats, Models: d.modelInventory, DBPath: d.dbPath, UpdatedAt: d.lastRefresh.Format(time.RFC3339), } } // GetTraining returns current training status. func (d *DashboardService) GetTraining() []TrainingRow { d.mu.RLock() defer d.mu.RUnlock() return d.trainingStatus } // GetGeneration returns generation progress. func (d *DashboardService) GetGeneration() GenerationStats { d.mu.RLock() defer d.mu.RUnlock() return d.generationStats } // GetModels returns the model inventory. func (d *DashboardService) GetModels() []ModelInfo { d.mu.RLock() defer d.mu.RUnlock() return d.modelInventory } // Refresh forces an immediate data refresh. func (d *DashboardService) Refresh() error { return d.refresh() } // RunQuery executes an ad-hoc SQL query against DuckDB. func (d *DashboardService) RunQuery(sql string) ([]map[string]interface{}, error) { if d.dbPath == "" { return nil, fmt.Errorf("no database configured") } db, err := lem.OpenDB(d.dbPath) if err != nil { return nil, fmt.Errorf("open db: %w", err) } defer db.Close() rows, err := db.QueryRows(sql) if err != nil { return nil, fmt.Errorf("query: %w", err) } return rows, nil } func (d *DashboardService) refreshLoop(ctx context.Context) { // Initial refresh. if err := d.refresh(); err != nil { log.Printf("Dashboard refresh error: %v", err) } ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: if err := d.refresh(); err != nil { log.Printf("Dashboard refresh error: %v", err) } } } } func (d *DashboardService) refresh() error { d.mu.Lock() defer d.mu.Unlock() // Query training status from InfluxDB. rows, err := d.influx.QuerySQL(` SELECT model, run_id, status, iteration, total_iters, pct FROM training_status ORDER BY time DESC LIMIT 10 `) if err == nil { d.trainingStatus = nil for _, row := range rows { d.trainingStatus = append(d.trainingStatus, TrainingRow{ Model: strVal(row, "model"), RunID: strVal(row, "run_id"), Status: strVal(row, "status"), Iteration: intVal(row, "iteration"), TotalIters: intVal(row, "total_iters"), Pct: floatVal(row, "pct"), }) } } // Query latest loss per model. lossRows, err := d.influx.QuerySQL(` SELECT model, loss FROM training_loss WHERE loss_type = 'train' ORDER BY time DESC LIMIT 10 `) if err == nil { lossMap := make(map[string]float64) for _, row := range lossRows { model := strVal(row, "model") if _, exists := lossMap[model]; !exists { lossMap[model] = floatVal(row, "loss") } } for i, t := range d.trainingStatus { if loss, ok := lossMap[t.Model]; ok { d.trainingStatus[i].Loss = loss } } } // Query golden set progress. goldenRows, err := d.influx.QuerySQL(` SELECT completed, target, pct FROM golden_gen_progress ORDER BY time DESC LIMIT 1 `) if err == nil && len(goldenRows) > 0 { d.generationStats.GoldenCompleted = intVal(goldenRows[0], "completed") d.generationStats.GoldenTarget = intVal(goldenRows[0], "target") d.generationStats.GoldenPct = floatVal(goldenRows[0], "pct") } // Query expansion progress. expRows, err := d.influx.QuerySQL(` SELECT completed, target, pct FROM expansion_progress ORDER BY time DESC LIMIT 1 `) if err == nil && len(expRows) > 0 { d.generationStats.ExpansionCompleted = intVal(expRows[0], "completed") d.generationStats.ExpansionTarget = intVal(expRows[0], "target") d.generationStats.ExpansionPct = floatVal(expRows[0], "pct") } // Query model capability scores. capRows, err := d.influx.QuerySQL(` SELECT model, label, accuracy, iteration FROM capability_score WHERE category = 'overall' ORDER BY time DESC LIMIT 20 `) if err == nil { d.modelInventory = nil seen := make(map[string]bool) for _, row := range capRows { label := strVal(row, "label") if seen[label] { continue } seen[label] = true d.modelInventory = append(d.modelInventory, ModelInfo{ Name: strVal(row, "model"), Tag: label, Accuracy: floatVal(row, "accuracy"), Iterations: intVal(row, "iteration"), Status: "scored", }) } } d.lastRefresh = time.Now() return nil } func strVal(m map[string]interface{}, key string) string { if v, ok := m[key]; ok { return fmt.Sprintf("%v", v) } return "" } func intVal(m map[string]interface{}, key string) int { if v, ok := m[key]; ok { switch n := v.(type) { case float64: return int(n) case int: return n } } return 0 } func floatVal(m map[string]interface{}, key string) float64 { if v, ok := m[key]; ok { if f, ok := v.(float64); ok { return f } } return 0 }