package main import ( "context" "database/sql" "fmt" "log" "os" "path/filepath" "strings" "sync" ) // App struct — each public method is an IPC binding to the frontend. type App struct { ctx context.Context config *DashboardConfig engine *QueryEngine pool map[string]*sql.DB dashboardDir string // directory to scan for YAML dashboards currentFile string // currently loaded YAML file mu sync.RWMutex } func NewApp(cfg *DashboardConfig, engine *QueryEngine, pool map[string]*sql.DB, dashDir, currentFile string) *App { return &App{ config: cfg, engine: engine, pool: pool, dashboardDir: dashDir, currentFile: currentFile, } } func (a *App) startup(ctx context.Context) { a.ctx = ctx } // GetDashboardConfig returns the full config for the frontend to render. func (a *App) GetDashboardConfig() *DashboardConfig { a.mu.RLock() defer a.mu.RUnlock() log.Printf("[IPC] GetDashboardConfig — title=%q sections=%d", a.config.Settings.Title, len(a.config.Sections)) return a.config } // GetWidgetData executes the query for a widget with current filter values. func (a *App) GetWidgetData(widgetID string, filters map[string]any) ([]map[string]any, error) { a.mu.RLock() defer a.mu.RUnlock() rows, err := a.engine.Execute(widgetID, filters) if err != nil { log.Printf("[IPC] GetWidgetData ERROR — widget=%q err=%v", widgetID, err) return nil, fmt.Errorf("widget %q: %w", widgetID, err) } log.Printf("[IPC] GetWidgetData OK — widget=%q rows=%d", widgetID, len(rows)) return rows, nil } // DashboardInfo is a summary of an available dashboard file. type DashboardInfo struct { Name string `json:"name"` File string `json:"file"` Title string `json:"title"` Theme string `json:"theme"` Current bool `json:"current"` } // ListDashboards returns all .yaml files in the dashboards directory. func (a *App) ListDashboards() []DashboardInfo { a.mu.RLock() currentFile := a.currentFile a.mu.RUnlock() var dashboards []DashboardInfo entries, err := os.ReadDir(a.dashboardDir) if err != nil { log.Printf("[IPC] ListDashboards ERROR — %v", err) return dashboards } for _, e := range entries { if e.IsDir() || (!strings.HasSuffix(e.Name(), ".yaml") && !strings.HasSuffix(e.Name(), ".yml")) { continue } filePath := filepath.Join(a.dashboardDir, e.Name()) cfg, err := LoadDashboard(filePath) title := e.Name() theme := "" if err == nil { title = cfg.Settings.Title theme = cfg.Theme } dashboards = append(dashboards, DashboardInfo{ Name: strings.TrimSuffix(strings.TrimSuffix(e.Name(), ".yaml"), ".yml"), File: e.Name(), Title: title, Theme: theme, Current: filePath == currentFile || e.Name() == filepath.Base(currentFile), }) } log.Printf("[IPC] ListDashboards — found %d", len(dashboards)) return dashboards } // SwitchDashboard loads a different dashboard YAML by filename. func (a *App) SwitchDashboard(fileName string) (*DashboardConfig, error) { filePath := filepath.Join(a.dashboardDir, fileName) log.Printf("[IPC] SwitchDashboard — loading %q", filePath) cfg, err := LoadDashboard(filePath) if err != nil { return nil, fmt.Errorf("loading %q: %w", fileName, err) } newPool, err := OpenConnections(cfg.Connections, filepath.Dir(filePath)) if err != nil { return nil, fmt.Errorf("connections for %q: %w", fileName, err) } a.mu.Lock() // Close old connections if a.pool != nil { CloseConnections(a.pool) } a.config = cfg a.pool = newPool a.engine = NewQueryEngine(cfg, newPool) a.currentFile = filePath a.mu.Unlock() log.Printf("[IPC] SwitchDashboard OK — title=%q sections=%d", cfg.Settings.Title, len(cfg.Sections)) return cfg, nil }