from __future__ import annotations import inspect from collections.abc import Awaitable, Callable from datetime import datetime from typing import Any from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import JSONResponse from pydantic import BaseModel, ConfigDict, Field from .manager import UIBuilderManager DataProvider = Callable[[], dict[str, Any] | Awaitable[dict[str, Any]]] class ConfigPayload(BaseModel): model_config = ConfigDict(extra="allow") version: int = 20 project_name: str data_source: str canvas: dict[str, Any] = Field(default_factory=dict) tabs: list[dict[str, Any]] = Field(default_factory=list) components: list[dict[str, Any]] = Field(default_factory=list) triggers: list[dict[str, Any]] = Field(default_factory=list) shortcut_sequences: list[dict[str, Any]] = Field(default_factory=list) prematch_groups: list[dict[str, Any]] = Field(default_factory=list) prematch_buttons: list[dict[str, Any]] = Field(default_factory=list) quick_panel_selectors: list[dict[str, Any]] = Field(default_factory=list) class BackupPayload(BaseModel): name: str def create_ui_builder_router( manager: UIBuilderManager, data_providers: dict[str, DataProvider], *, source_labels: dict[str, str] | None = None, prefix: str = "/api/ui-builder", access_dependency: Callable[..., Any] | None = None, read_only: bool = False, publish_manager: UIBuilderManager | None = None, ) -> APIRouter: dependencies = [Depends(access_dependency)] if access_dependency else [] router = APIRouter( prefix=prefix, tags=["UI Builder"], dependencies=dependencies, ) source_labels = source_labels or {} @router.get("/config") async def get_config() -> dict[str, Any]: return manager.load() if not read_only: @router.post("/config") async def save_config(payload: ConfigPayload) -> dict[str, Any]: saved = manager.save(payload.model_dump()) return {"ok": True, "config": saved} @router.post("/import") async def import_config(payload: ConfigPayload) -> dict[str, Any]: saved = manager.import_config(payload.model_dump()) return {"ok": True, "config": saved} @router.get("/export") async def export_config() -> JSONResponse: return JSONResponse( manager.load(), headers={ "Content-Disposition": 'attachment; filename="ui_builder_draft.json"' }, ) @router.get("/backups") async def list_backups() -> dict[str, Any]: return {"items": manager.list_backups()} @router.post("/backups/restore") async def restore_backup(payload: BackupPayload) -> dict[str, Any]: try: restored = manager.restore_backup(payload.name) except (FileNotFoundError, ValueError, OSError): raise HTTPException(status_code=404, detail="Backup not found") return {"ok": True, "config": restored} if publish_manager is not None: @router.post("/publish") async def publish_config() -> dict[str, Any]: published = publish_manager.save( manager.load(), create_backup=True, ) return { "ok": True, "config": published, "published_at": datetime.now().isoformat(timespec="seconds"), } @router.get("/sources") async def list_sources() -> dict[str, Any]: return { "items": [ {"id": key, "label": source_labels.get(key, key)} for key in data_providers ] } @router.get("/data/{source}") async def get_data(source: str) -> dict[str, Any]: provider = data_providers.get(source) if provider is None: raise HTTPException(status_code=404, detail="Unknown data source") try: result = provider() if inspect.isawaitable(result): result = await result if not isinstance(result, dict): raise TypeError("Data provider must return a dict") return result except HTTPException: raise except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) from exc return router