package postgres 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, $2, $3, $4, true, NOW(), NOW()) ON CONFLICT (id) DO UPDATE SET title = $2, manifest = $4, source = $3, updated_at = 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 []byte if err := rows.Scan(&sr.ID, &sr.Title, &manifestJSON, &sr.Enabled, &sr.Source, &sr.InstalledAt, &sr.UpdatedAt); err != nil { continue } json.Unmarshal(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 []byte err := DB.QueryRowContext(ctx, `SELECT id, title, manifest, enabled, source, installed_at, updated_at FROM surface_registry WHERE id = $1`, id). Scan(&sr.ID, &sr.Title, &manifestJSON, &sr.Enabled, &sr.Source, &sr.InstalledAt, &sr.UpdatedAt) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } json.Unmarshal(manifestJSON, &sr.Manifest) return &sr, nil } func (s *SurfaceRegistryStore) SetEnabled(ctx context.Context, id string, enabled bool) error { result, err := DB.ExecContext(ctx, `UPDATE surface_registry SET enabled = $2, updated_at = NOW() WHERE id = $1`, id, enabled) 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 = $1 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 = true 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() }