package sqlite import ( "context" "database/sql" "encoding/json" "git.gobha.me/xcaliber/chat-switchboard/store" ) type SurfaceRegistryStore struct{} func NewSurfaceRegistryStore() *SurfaceRegistryStore { return &SurfaceRegistryStore{} } // Seed upserts a core surface. Called at startup by the page engine // to ensure core surfaces exist in the registry. Does NOT overwrite // the enabled state if the row already exists (admin toggle survives restart). func (s *SurfaceRegistryStore) Seed(ctx context.Context, id, title, source string, manifest map[string]any) error { manifestJSON := ToJSON(manifest) _, err := DB.ExecContext(ctx, ` INSERT INTO surface_registry (id, title, source, manifest, enabled, installed_at, updated_at) VALUES (?, ?, ?, ?, 1, datetime('now'), datetime('now')) ON CONFLICT (id) DO UPDATE SET title = excluded.title, manifest = excluded.manifest, source = excluded.source, updated_at = datetime('now')`, // Note: ON CONFLICT does NOT update 'enabled' — preserves admin toggle id, title, source, manifestJSON) return err } func (s *SurfaceRegistryStore) List(ctx context.Context) ([]store.SurfaceRegistration, error) { rows, err := DB.QueryContext(ctx, `SELECT id, title, manifest, enabled, source, installed_at, updated_at FROM surface_registry ORDER BY source, title`) if err != nil { return nil, err } defer rows.Close() var surfaces []store.SurfaceRegistration for rows.Next() { var sr store.SurfaceRegistration var manifestJSON string var enabledInt int if err := rows.Scan(&sr.ID, &sr.Title, &manifestJSON, &enabledInt, &sr.Source, &sr.InstalledAt, &sr.UpdatedAt); err != nil { continue } sr.Enabled = enabledInt != 0 json.Unmarshal([]byte(manifestJSON), &sr.Manifest) surfaces = append(surfaces, sr) } return surfaces, rows.Err() } func (s *SurfaceRegistryStore) Get(ctx context.Context, id string) (*store.SurfaceRegistration, error) { var sr store.SurfaceRegistration var manifestJSON string var enabledInt int err := DB.QueryRowContext(ctx, `SELECT id, title, manifest, enabled, source, installed_at, updated_at FROM surface_registry WHERE id = ?`, id). Scan(&sr.ID, &sr.Title, &manifestJSON, &enabledInt, &sr.Source, &sr.InstalledAt, &sr.UpdatedAt) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } sr.Enabled = enabledInt != 0 json.Unmarshal([]byte(manifestJSON), &sr.Manifest) return &sr, nil } func (s *SurfaceRegistryStore) SetEnabled(ctx context.Context, id string, enabled bool) error { enabledInt := 0 if enabled { enabledInt = 1 } result, err := DB.ExecContext(ctx, `UPDATE surface_registry SET enabled = ?, updated_at = datetime('now') WHERE id = ?`, enabledInt, id) if err != nil { return err } affected, _ := result.RowsAffected() if affected == 0 { return sql.ErrNoRows } return nil } func (s *SurfaceRegistryStore) Delete(ctx context.Context, id string) error { // Only allow deleting extension surfaces, not core result, err := DB.ExecContext(ctx, `DELETE FROM surface_registry WHERE id = ? AND source = 'extension'`, id) if err != nil { return err } affected, _ := result.RowsAffected() if affected == 0 { return sql.ErrNoRows } return nil } func (s *SurfaceRegistryStore) ListEnabled(ctx context.Context) ([]string, error) { rows, err := DB.QueryContext(ctx, `SELECT id FROM surface_registry WHERE enabled = 1 ORDER BY title`) if err != nil { return nil, err } defer rows.Close() var ids []string for rows.Next() { var id string if err := rows.Scan(&id); err != nil { continue } ids = append(ids, id) } return ids, rows.Err() }