""" Design System Server (DSS) - FastAPI Server Portable API server providing: - Project management (CRUD) - Figma integration endpoints - Discovery & health endpoints - Activity tracking - Runtime configuration management - Service discovery (Storybook, etc.) Modes: - Server: Deployed remotely, serves design systems to teams - Local: Dev companion, UI advisor, local services Uses SQLite for persistence, integrates with Figma tools. """ import asyncio import subprocess import json import os from pathlib import Path from typing import Optional, List, Dict, Any from datetime import datetime from fastapi import FastAPI, HTTPException, Query, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel import sys sys.path.insert(0, str(Path(__file__).parent.parent)) from config import config from storage.database import ( Projects, Components, SyncHistory, ActivityLog, Teams, Cache, get_stats ) from figma.figma_tools import FigmaToolSuite # === Runtime Configuration === class RuntimeConfig: """ Runtime configuration that can be modified from the dashboard. Persists to .dss/runtime-config.json for portability. """ def __init__(self): self.config_path = Path(__file__).parent.parent.parent / ".dss" / "runtime-config.json" self.config_path.parent.mkdir(parents=True, exist_ok=True) self._data = self._load() def _load(self) -> dict: if self.config_path.exists(): try: return json.loads(self.config_path.read_text()) except: pass return { "mode": "local", # "local" or "server" "figma": {"token": "", "configured": False}, "services": { "storybook": {"enabled": False, "port": 6006, "url": ""}, "chromatic": {"enabled": False, "project_token": ""}, "github": {"enabled": False, "repo": ""}, }, "features": { "visual_qa": True, "token_sync": True, "code_gen": True, "ai_advisor": False, } } def _save(self): self.config_path.write_text(json.dumps(self._data, indent=2)) def get(self, key: str = None): if key is None: # Return safe copy without secrets safe = self._data.copy() if safe.get("figma", {}).get("token"): safe["figma"]["token"] = "***configured***" return safe return self._data.get(key) def set(self, key: str, value: Any): self._data[key] = value self._save() return self._data[key] def update(self, updates: dict): for key, value in updates.items(): if isinstance(value, dict) and isinstance(self._data.get(key), dict): self._data[key].update(value) else: self._data[key] = value self._save() return self.get() def set_figma_token(self, token: str): self._data["figma"]["token"] = token self._data["figma"]["configured"] = bool(token) self._save() # Also update the global config os.environ["FIGMA_TOKEN"] = token return {"configured": bool(token)} runtime_config = RuntimeConfig() # === Service Discovery === class ServiceDiscovery: """Discovers and manages companion services.""" KNOWN_SERVICES = { "storybook": {"ports": [6006, 6007], "health": "/"}, "chromatic": {"ports": [], "health": None}, "vite": {"ports": [5173, 5174, 3000], "health": "/"}, "webpack": {"ports": [8080, 8081], "health": "/"}, "nextjs": {"ports": [3000, 3001], "health": "/"}, } @classmethod async def discover(cls) -> dict: """Discover running services by checking known ports.""" import socket discovered = {} for service, info in cls.KNOWN_SERVICES.items(): for port in info["ports"]: try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(0.5) result = sock.connect_ex(('127.0.0.1', port)) sock.close() if result == 0: discovered[service] = { "running": True, "port": port, "url": f"http://localhost:{port}" } break except: pass if service not in discovered: discovered[service] = {"running": False, "port": None, "url": None} return discovered @classmethod async def check_storybook(cls) -> dict: """Check Storybook status specifically.""" import httpx configured = runtime_config.get("services").get("storybook", {}) port = configured.get("port", 6006) url = configured.get("url") or f"http://localhost:{port}" try: async with httpx.AsyncClient(timeout=2.0) as client: resp = await client.get(url) return { "running": resp.status_code == 200, "url": url, "port": port } except: return {"running": False, "url": url, "port": port} # === App Setup === app = FastAPI( title="Design System Server (DSS)", description="API for design system management and Figma integration", version="1.0.0" ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Mount Admin UI static files UI_DIR = Path(__file__).parent.parent.parent / "admin-ui" if UI_DIR.exists(): app.mount("/admin-ui", StaticFiles(directory=str(UI_DIR), html=True), name="admin-ui") # Initialize Figma tools figma_suite = FigmaToolSuite(output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output")) # === Request/Response Models === class ProjectCreate(BaseModel): name: str description: str = "" figma_file_key: str = "" class ProjectUpdate(BaseModel): name: Optional[str] = None description: Optional[str] = None figma_file_key: Optional[str] = None status: Optional[str] = None class FigmaExtractRequest(BaseModel): file_key: str format: str = "css" class FigmaSyncRequest(BaseModel): file_key: str target_path: str format: str = "css" class TeamCreate(BaseModel): name: str description: str = "" # === Root & Health === @app.get("/") async def root(): """Redirect to Admin UI dashboard.""" from fastapi.responses import RedirectResponse return RedirectResponse(url="/admin-ui/index.html") @app.get("/health") async def health(): """Health check endpoint.""" return { "status": "ok", "name": "dss-api", "version": "1.0.0", "timestamp": datetime.utcnow().isoformat() + "Z", "figma_mode": figma_suite.mode, "config": config.summary() } @app.get("/api/stats") async def get_statistics(): """Get database and system statistics.""" db_stats = get_stats() return { "database": db_stats, "figma": { "mode": figma_suite.mode, "configured": config.figma.is_configured } } # === Projects === @app.get("/api/projects") async def list_projects(status: Optional[str] = None): """List all projects.""" projects = Projects.list(status=status) return projects @app.get("/api/projects/{project_id}") async def get_project(project_id: str): """Get a specific project.""" project = Projects.get(project_id) if not project: raise HTTPException(status_code=404, detail="Project not found") return project @app.post("/api/projects") async def create_project(project: ProjectCreate): """Create a new project.""" project_id = f"proj-{int(datetime.utcnow().timestamp() * 1000)}" created = Projects.create( id=project_id, name=project.name, description=project.description, figma_file_key=project.figma_file_key ) ActivityLog.log( action="project_created", entity_type="project", entity_id=project_id, project_id=project_id, details={"name": project.name} ) return created @app.put("/api/projects/{project_id}") async def update_project(project_id: str, update: ProjectUpdate): """Update a project.""" existing = Projects.get(project_id) if not existing: raise HTTPException(status_code=404, detail="Project not found") update_data = {k: v for k, v in update.dict().items() if v is not None} if not update_data: return existing updated = Projects.update(project_id, **update_data) ActivityLog.log( action="project_updated", entity_type="project", entity_id=project_id, project_id=project_id, details=update_data ) return updated @app.delete("/api/projects/{project_id}") async def delete_project(project_id: str): """Delete a project.""" if not Projects.delete(project_id): raise HTTPException(status_code=404, detail="Project not found") ActivityLog.log( action="project_deleted", entity_type="project", entity_id=project_id ) return {"success": True} # === Components === @app.get("/api/projects/{project_id}/components") async def list_components(project_id: str): """List components for a project.""" if not Projects.get(project_id): raise HTTPException(status_code=404, detail="Project not found") return Components.list(project_id) # === Figma Integration === @app.post("/api/figma/extract-variables") async def extract_variables(request: FigmaExtractRequest, background_tasks: BackgroundTasks): """Extract design tokens from Figma file.""" try: result = await figma_suite.extract_variables(request.file_key, request.format) ActivityLog.log( action="figma_extract_variables", entity_type="figma", details={"file_key": request.file_key, "format": request.format, "count": result.get("tokens_count")} ) return result except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/figma/extract-components") async def extract_components(request: FigmaExtractRequest): """Extract components from Figma file.""" try: result = await figma_suite.extract_components(request.file_key) ActivityLog.log( action="figma_extract_components", entity_type="figma", details={"file_key": request.file_key, "count": result.get("components_count")} ) return result except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/figma/extract-styles") async def extract_styles(request: FigmaExtractRequest): """Extract styles from Figma file.""" try: result = await figma_suite.extract_styles(request.file_key) return result except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/figma/sync-tokens") async def sync_tokens(request: FigmaSyncRequest): """Sync tokens from Figma to target path.""" try: result = await figma_suite.sync_tokens(request.file_key, request.target_path, request.format) ActivityLog.log( action="figma_sync_tokens", entity_type="figma", details={"file_key": request.file_key, "target": request.target_path, "synced": result.get("tokens_synced")} ) return result except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/figma/validate") async def validate_components(request: FigmaExtractRequest): """Validate components against design system rules.""" try: result = await figma_suite.validate_components(request.file_key) return result except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/figma/generate-code") async def generate_code(file_key: str, component_name: str, framework: str = "webcomponent"): """Generate component code from Figma.""" try: result = await figma_suite.generate_code(file_key, component_name, framework) return result except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # === Discovery === @app.get("/api/discovery") async def run_discovery(path: str = "."): """Run project discovery.""" script_path = Path(__file__).parent.parent / "discovery" / "discover.sh" try: result = subprocess.run( [str(script_path), path], capture_output=True, text=True, timeout=30 ) if result.returncode == 0: return json.loads(result.stdout) else: return {"error": result.stderr} except subprocess.TimeoutExpired: raise HTTPException(status_code=504, detail="Discovery timed out") except json.JSONDecodeError: return {"raw_output": result.stdout} @app.get("/api/discovery/ports") async def discover_ports(): """Discover listening ports and services.""" script_path = Path(__file__).parent.parent / "discovery" / "discover-ports.sh" try: result = subprocess.run( [str(script_path)], capture_output=True, text=True, timeout=10 ) return json.loads(result.stdout) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/discovery/env") async def discover_env(path: str = "."): """Analyze environment configuration.""" script_path = Path(__file__).parent.parent / "discovery" / "discover-env.sh" try: result = subprocess.run( [str(script_path), path], capture_output=True, text=True, timeout=10 ) return json.loads(result.stdout) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # === Activity & Sync History === @app.get("/api/activity") async def get_activity(limit: int = Query(default=50, le=100)): """Get recent activity log.""" return ActivityLog.recent(limit=limit) @app.get("/api/sync-history") async def get_sync_history(project_id: Optional[str] = None, limit: int = Query(default=20, le=100)): """Get sync history.""" return SyncHistory.recent(project_id=project_id, limit=limit) # === Teams === @app.get("/api/teams") async def list_teams(): """List all teams.""" return Teams.list() @app.post("/api/teams") async def create_team(team: TeamCreate): """Create a new team.""" team_id = f"team-{int(datetime.utcnow().timestamp() * 1000)}" created = Teams.create(team_id, team.name, team.description) return created @app.get("/api/teams/{team_id}") async def get_team(team_id: str): """Get a specific team.""" team = Teams.get(team_id) if not team: raise HTTPException(status_code=404, detail="Team not found") return team # === Cache Management === @app.post("/api/cache/clear") async def clear_cache(): """Clear expired cache entries.""" count = Cache.clear_expired() return {"cleared": count} @app.delete("/api/cache") async def purge_cache(): """Purge all cache entries.""" Cache.clear_all() return {"success": True} # === Configuration Management === class ConfigUpdate(BaseModel): mode: Optional[str] = None figma_token: Optional[str] = None services: Optional[Dict[str, Any]] = None features: Optional[Dict[str, bool]] = None @app.get("/api/config") async def get_config(): """Get current runtime configuration (secrets masked).""" return { "config": runtime_config.get(), "env": config.summary(), "mode": runtime_config.get("mode") } @app.put("/api/config") async def update_config(update: ConfigUpdate): """Update runtime configuration.""" updates = {} if update.mode: updates["mode"] = update.mode if update.figma_token is not None: runtime_config.set_figma_token(update.figma_token) # Reinitialize Figma tools with new token global figma_suite figma_suite = FigmaToolSuite(output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output")) ActivityLog.log( action="figma_token_updated", entity_type="config", details={"configured": bool(update.figma_token)} ) if update.services: updates["services"] = update.services if update.features: updates["features"] = update.features if updates: runtime_config.update(updates) ActivityLog.log( action="config_updated", entity_type="config", details={"keys": list(updates.keys())} ) return runtime_config.get() @app.get("/api/config/figma") async def get_figma_config(): """Get Figma configuration status.""" figma_cfg = runtime_config.get("figma") return { "configured": figma_cfg.get("configured", False), "mode": figma_suite.mode, "features": { "extract_variables": True, "extract_components": True, "extract_styles": True, "sync_tokens": True, "validate": True, "generate_code": True, } } @app.post("/api/config/figma/test") async def test_figma_connection(): """Test Figma API connection.""" try: # Try to make a simple API call if not runtime_config.get("figma").get("configured"): return {"success": False, "error": "Figma token not configured"} # Test with a minimal API call import httpx token = runtime_config._data["figma"]["token"] async with httpx.AsyncClient() as client: resp = await client.get( "https://api.figma.com/v1/me", headers={"X-Figma-Token": token} ) if resp.status_code == 200: user = resp.json() return { "success": True, "user": user.get("email", "connected"), "handle": user.get("handle") } else: return {"success": False, "error": f"API returned {resp.status_code}"} except Exception as e: return {"success": False, "error": str(e)} # === Service Discovery === @app.get("/api/services") async def list_services(): """List configured and discovered services.""" configured = runtime_config.get("services") discovered = await ServiceDiscovery.discover() return { "configured": configured, "discovered": discovered, "storybook": await ServiceDiscovery.check_storybook() } @app.put("/api/services/{service_name}") async def configure_service(service_name: str, config_data: Dict[str, Any]): """Configure a service.""" services = runtime_config.get("services") or {} services[service_name] = {**services.get(service_name, {}), **config_data} runtime_config.set("services", services) ActivityLog.log( action="service_configured", entity_type="service", entity_id=service_name, details={"keys": list(config_data.keys())} ) return services[service_name] @app.get("/api/services/storybook") async def get_storybook_status(): """Get Storybook service status.""" return await ServiceDiscovery.check_storybook() # === DSS Mode === @app.get("/api/mode") async def get_mode(): """Get current DSS mode.""" mode = runtime_config.get("mode") return { "mode": mode, "description": "Local dev companion" if mode == "local" else "Remote design system server", "features": runtime_config.get("features") } @app.put("/api/mode") async def set_mode(mode: str): """Set DSS mode (local or server).""" if mode not in ["local", "server"]: raise HTTPException(status_code=400, detail="Mode must be 'local' or 'server'") runtime_config.set("mode", mode) ActivityLog.log( action="mode_changed", entity_type="config", details={"mode": mode} ) return {"mode": mode, "success": True} # === Run Server === # === Static Files (Admin UI) === # Mount at the end so API routes take precedence # This enables portable mode: ./dss start serves everything on one port UI_DIR = Path(__file__).parent.parent.parent / "admin-ui" if UI_DIR.exists(): app.mount("/", StaticFiles(directory=str(UI_DIR), html=True), name="ui") if __name__ == "__main__": import uvicorn port = int(os.getenv("PORT", "3456")) host = os.getenv("HOST", "0.0.0.0") url = f"http://{host}:{port}" print(f""" ╔═══════════════════════════════════════════════════════════════╗ ║ Design System Server (DSS) - Portable Server ║ ╠═══════════════════════════════════════════════════════════════╣ ║ Dashboard: {url + '/':^47}║ ║ API: {url + '/api':^47}║ ║ Docs: {url + '/docs':^47}║ ║ Environment: {config.server.env:^47}║ ║ Figma Mode: {figma_suite.mode:^47}║ ╚═══════════════════════════════════════════════════════════════╝ """) uvicorn.run( "server:app", host=host, port=port, reload=config.server.env == "development" )