from fastapi import FastAPI, Depends, HTTPException, Response, Cookie from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse, RedirectResponse from sqlalchemy.orm import Session from pydantic import BaseModel from datetime import datetime, timezone, timedelta from typing import Optional import hashlib import json import os import secrets import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from database import get_db, init_db from models import User, AgentCatalog, AgentInstance, Run, LLMProvider, Bridge app = FastAPI(title="Agent Command Center", version="2026.04.12.01") # --- Auth --- SESSION_SECRET = os.environ.get("SESSION_SECRET", secrets.token_hex(32)) _sessions: dict[str, dict] = {} # token -> {user_id, username, role} _magic_links: dict[str, dict] = {} # token -> {user_id, email, expires} # SMTP Config SMTP_HOST = os.environ.get("SMTP_HOST", "smtp.gmail.com") SMTP_PORT = int(os.environ.get("SMTP_PORT", "587")) SMTP_USER = os.environ.get("SMTP_USER", "eric.jungbauer@gmail.com") SMTP_PASS = os.environ.get("SMTP_PASS", "jozj oags ifqy auey") SMTP_FROM = os.environ.get("SMTP_FROM", "eric.jungbauer@gmail.com") APP_URL = os.environ.get("APP_URL", "https://agents.jfamily.io") def hash_password(password: str) -> str: salt = secrets.token_hex(16) h = hashlib.sha256((salt + password).encode()).hexdigest() return f"{salt}:{h}" def verify_password(password: str, password_hash: str) -> bool: salt, h = password_hash.split(":", 1) return hashlib.sha256((salt + password).encode()).hexdigest() == h def create_session(user: User) -> str: token = secrets.token_urlsafe(32) _sessions[token] = {"user_id": user.id, "username": user.username, "role": user.role} return token def get_current_user(session: Optional[str] = Cookie(None)) -> Optional[dict]: if session and session in _sessions: return _sessions[session] return None def require_auth(session: Optional[str] = Cookie(None)) -> dict: user = get_current_user(session) if not user: raise HTTPException(status_code=401, detail="Not authenticated") return user def require_admin(session: Optional[str] = Cookie(None)) -> dict: user = require_auth(session) if user["role"] != "admin": raise HTTPException(status_code=403, detail="Admin access required") return user # --- Schemas --- class LoginRequest(BaseModel): username: str # accepts username or email password: str class MagicLinkRequest(BaseModel): email: str class InstanceCreate(BaseModel): catalog_id: str name: Optional[str] = None config: dict = {} schedule: Optional[str] = None class InstanceUpdate(BaseModel): name: Optional[str] = None config: Optional[dict] = None schedule: Optional[str] = None status: Optional[str] = None class RunCreate(BaseModel): status: str = "running" output: str = "" error: str = "" metadata: dict = {} class UserCreate(BaseModel): username: str email: str = "" password: str display_name: str = "" role: str = "user" class UserUpdate(BaseModel): display_name: Optional[str] = None email: Optional[str] = None role: Optional[str] = None password: Optional[str] = None class LLMProviderCreate(BaseModel): name: str provider_type: str = "anthropic" api_url: str = "" api_key: str = "" default_model: str = "" is_default: bool = False class LLMProviderUpdate(BaseModel): name: Optional[str] = None provider_type: Optional[str] = None api_url: Optional[str] = None api_key: Optional[str] = None default_model: Optional[str] = None is_default: Optional[bool] = None # --- Auth Routes --- @app.post("/api/login") def login(creds: LoginRequest, response: Response, db: Session = Depends(get_db)): user = db.query(User).filter( (User.username == creds.username) | (User.email == creds.username) ).first() if not user or not verify_password(creds.password, user.password_hash): raise HTTPException(status_code=401, detail="Invalid credentials") token = create_session(user) response.set_cookie("session", token, httponly=True, samesite="lax", max_age=86400 * 7) return {"status": "ok", "user": user.username, "email": user.email or "", "role": user.role, "display_name": user.display_name} @app.post("/api/logout") def logout(response: Response, session: Optional[str] = Cookie(None)): if session and session in _sessions: del _sessions[session] response.delete_cookie("session") return {"status": "ok"} def send_magic_email(email: str, token: str): """Send a magic link email via SMTP.""" link = f"{APP_URL}/auth/verify?token={token}" html = f"""

Agent Command Center

Click the button below to sign in:

Sign In

This link expires in 15 minutes. If you didn't request this, ignore this email.

{APP_URL}

""" msg = MIMEMultipart("alternative") msg["Subject"] = "Sign in to Agent Command Center" msg["From"] = SMTP_FROM msg["To"] = email msg.attach(MIMEText(f"Sign in: {link}\n\nExpires in 15 minutes.", "plain")) msg.attach(MIMEText(html, "html")) with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server: server.starttls() server.login(SMTP_USER, SMTP_PASS) server.send_message(msg) @app.post("/api/auth/magic-link") def request_magic_link(data: MagicLinkRequest, db: Session = Depends(get_db)): """Send a magic link to the user's email.""" user = db.query(User).filter(User.email == data.email).first() if not user: # Don't reveal whether email exists — return success either way return {"status": "ok", "message": "If that email is registered, a sign-in link has been sent."} # Generate token token = secrets.token_urlsafe(32) _magic_links[token] = { "user_id": user.id, "email": user.email, "expires": datetime.now(timezone.utc) + timedelta(minutes=15), } # Send email try: send_magic_email(user.email, token) except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to send email: {e}") return {"status": "ok", "message": "If that email is registered, a sign-in link has been sent."} @app.get("/auth/verify") def verify_magic_link(token: str, response: Response, db: Session = Depends(get_db)): """Verify a magic link token and log the user in.""" link_data = _magic_links.pop(token, None) if not link_data: return RedirectResponse("/login?error=invalid", status_code=302) if datetime.now(timezone.utc) > link_data["expires"]: return RedirectResponse("/login?error=expired", status_code=302) user = db.query(User).filter(User.id == link_data["user_id"]).first() if not user: return RedirectResponse("/login?error=invalid", status_code=302) # Create session session_token = create_session(user) resp = RedirectResponse("/", status_code=302) resp.set_cookie("session", session_token, httponly=True, samesite="lax", max_age=86400 * 7) return resp @app.get("/api/me") def me(user: dict = Depends(require_auth), db: Session = Depends(get_db)): u = db.query(User).filter(User.id == user["user_id"]).first() return { "id": u.id, "username": u.username, "email": u.email or "", "display_name": u.display_name, "role": u.role, "created_at": u.created_at.isoformat() if u.created_at else None, } # --- Health --- @app.get("/api/health") def health(): return {"status": "ok", "service": "agent-command-center", "version": "2026.04.12.01"} # --- Agent Catalog --- @app.get("/api/catalog") def list_catalog(user: dict = Depends(require_auth), db: Session = Depends(get_db)): entries = db.query(AgentCatalog).all() # Check which ones this user already has instances of user_instance_ids = { i.catalog_id for i in db.query(AgentInstance).filter(AgentInstance.user_id == user["user_id"]).all() } return [{ "id": e.id, "name": e.name, "description": e.description, "category": e.category, "config_schema": e.config_schema or {}, "default_config": e.default_config or {}, "supports_schedule": e.supports_schedule, "is_sub_agent": e.is_sub_agent, "enabled": e.id in user_instance_ids, } for e in entries] @app.get("/api/catalog/{catalog_id}") def get_catalog_entry(catalog_id: str, user: dict = Depends(require_auth), db: Session = Depends(get_db)): entry = db.query(AgentCatalog).filter(AgentCatalog.id == catalog_id).first() if not entry: raise HTTPException(status_code=404) return { "id": entry.id, "name": entry.name, "description": entry.description, "category": entry.category, "config_schema": entry.config_schema or {}, "default_config": entry.default_config or {}, "supports_schedule": entry.supports_schedule, "is_sub_agent": entry.is_sub_agent, } # --- Agent Instances (user-scoped) --- def serialize_instance(inst, db): last_run = db.query(Run).filter(Run.instance_id == inst.id).order_by(Run.started_at.desc()).first() recent = db.query(Run).filter(Run.instance_id == inst.id).order_by(Run.started_at.desc()).limit(10).all() streak = 0 for r in recent: if r.status == "success": streak += 1 else: break return { "id": inst.id, "catalog_id": inst.catalog_id, "name": inst.name, "config": inst.config or {}, "schedule": inst.schedule, "status": inst.status, "created_at": inst.created_at.isoformat() if inst.created_at else None, "last_run": { "status": last_run.status, "started_at": last_run.started_at.isoformat() if last_run.started_at else None, "finished_at": last_run.finished_at.isoformat() if last_run.finished_at else None, } if last_run else None, "success_streak": streak, "total_runs": db.query(Run).filter(Run.instance_id == inst.id).count(), } @app.get("/api/instances") def list_instances(user: dict = Depends(require_auth), db: Session = Depends(get_db)): instances = db.query(AgentInstance).filter(AgentInstance.user_id == user["user_id"]).all() return [serialize_instance(i, db) for i in instances] @app.post("/api/instances") def create_instance(data: InstanceCreate, user: dict = Depends(require_auth), db: Session = Depends(get_db)): catalog = db.query(AgentCatalog).filter(AgentCatalog.id == data.catalog_id).first() if not catalog: raise HTTPException(status_code=404, detail="Agent type not found in catalog") config = {**(catalog.default_config or {}), **data.config} inst = AgentInstance( user_id=user["user_id"], catalog_id=data.catalog_id, name=data.name or catalog.name, config=config, schedule=data.schedule or ("sub-agent" if catalog.is_sub_agent else "manual"), ) db.add(inst) db.commit() return {"id": inst.id, "status": "created"} @app.get("/api/instances/{instance_id}") def get_instance(instance_id: int, user: dict = Depends(require_auth), db: Session = Depends(get_db)): inst = db.query(AgentInstance).filter( AgentInstance.id == instance_id, AgentInstance.user_id == user["user_id"] ).first() if not inst: raise HTTPException(status_code=404) runs = db.query(Run).filter(Run.instance_id == instance_id).order_by(Run.started_at.desc()).limit(50).all() catalog = db.query(AgentCatalog).filter(AgentCatalog.id == inst.catalog_id).first() result = serialize_instance(inst, db) result["config_schema"] = catalog.config_schema if catalog else {} result["runs"] = [{ "id": r.id, "started_at": r.started_at.isoformat() if r.started_at else None, "finished_at": r.finished_at.isoformat() if r.finished_at else None, "status": r.status, "output": r.output, "error": r.error, "metadata": r.metadata_, } for r in runs] return result @app.put("/api/instances/{instance_id}") def update_instance(instance_id: int, update: InstanceUpdate, user: dict = Depends(require_auth), db: Session = Depends(get_db)): inst = db.query(AgentInstance).filter( AgentInstance.id == instance_id, AgentInstance.user_id == user["user_id"] ).first() if not inst: raise HTTPException(status_code=404) if update.name is not None: inst.name = update.name if update.schedule is not None: inst.schedule = update.schedule if update.status is not None: inst.status = update.status if update.config is not None: current = inst.config or {} current.update(update.config) inst.config = current db.commit() return {"id": inst.id, "status": "updated"} @app.delete("/api/instances/{instance_id}") def delete_instance(instance_id: int, user: dict = Depends(require_auth), db: Session = Depends(get_db)): inst = db.query(AgentInstance).filter( AgentInstance.id == instance_id, AgentInstance.user_id == user["user_id"] ).first() if not inst: raise HTTPException(status_code=404) db.query(Run).filter(Run.instance_id == instance_id).delete() db.delete(inst) db.commit() return {"status": "deleted"} # --- Internal endpoints (no auth, for agent scripts) --- @app.get("/api/instances/{instance_id}/config") def get_instance_config(instance_id: int, db: Session = Depends(get_db)): inst = db.query(AgentInstance).filter(AgentInstance.id == instance_id).first() if not inst: raise HTTPException(status_code=404) return inst.config or {} @app.post("/api/instances/{instance_id}/runs") def create_run(instance_id: int, run: RunCreate, db: Session = Depends(get_db)): inst = db.query(AgentInstance).filter(AgentInstance.id == instance_id).first() if not inst: raise HTTPException(status_code=404) new_run = Run( instance_id=instance_id, user_id=inst.user_id, status=run.status, output=run.output, error=run.error, metadata_=run.metadata, ) if run.status in ("success", "failed"): new_run.finished_at = datetime.now(timezone.utc) db.add(new_run) db.commit() return {"id": new_run.id, "status": new_run.status} # --- Runs (user-scoped) --- @app.get("/api/runs") def list_runs(limit: int = 50, user: dict = Depends(require_auth), db: Session = Depends(get_db)): runs = db.query(Run).filter(Run.user_id == user["user_id"]).order_by(Run.started_at.desc()).limit(limit).all() return [{ "id": r.id, "instance_id": r.instance_id, "started_at": r.started_at.isoformat() if r.started_at else None, "finished_at": r.finished_at.isoformat() if r.finished_at else None, "status": r.status, "output": r.output, "error": r.error, "metadata": r.metadata_, } for r in runs] # --- Admin: Users --- @app.get("/api/admin/users") def admin_list_users(admin: dict = Depends(require_admin), db: Session = Depends(get_db)): users = db.query(User).all() return [{ "id": u.id, "username": u.username, "email": u.email or "", "display_name": u.display_name, "role": u.role, "created_at": u.created_at.isoformat() if u.created_at else None, "instance_count": db.query(AgentInstance).filter(AgentInstance.user_id == u.id).count(), } for u in users] @app.post("/api/admin/users") def admin_create_user(data: UserCreate, admin: dict = Depends(require_admin), db: Session = Depends(get_db)): if db.query(User).filter(User.username == data.username).first(): raise HTTPException(status_code=409, detail="Username exists") user = User( username=data.username, email=data.email or None, password_hash=hash_password(data.password), display_name=data.display_name or data.username, role=data.role, ) db.add(user) db.commit() return {"id": user.id, "status": "created"} @app.put("/api/admin/users/{user_id}") def admin_update_user(user_id: int, update: UserUpdate, admin: dict = Depends(require_admin), db: Session = Depends(get_db)): user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException(status_code=404) if update.display_name is not None: user.display_name = update.display_name if update.email is not None: user.email = update.email or None if update.role is not None: user.role = update.role if update.password is not None: user.password_hash = hash_password(update.password) db.commit() return {"id": user.id, "status": "updated"} @app.delete("/api/admin/users/{user_id}") def admin_delete_user(user_id: int, admin: dict = Depends(require_admin), db: Session = Depends(get_db)): user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException(status_code=404) for inst in db.query(AgentInstance).filter(AgentInstance.user_id == user_id).all(): db.query(Run).filter(Run.instance_id == inst.id).delete() db.delete(inst) db.delete(user) db.commit() return {"status": "deleted"} # --- Admin: LLM Providers --- @app.get("/api/admin/llm-providers") def admin_list_providers(admin: dict = Depends(require_admin), db: Session = Depends(get_db)): providers = db.query(LLMProvider).all() return [{ "id": p.id, "name": p.name, "provider_type": p.provider_type, "api_url": p.api_url, "api_key": "***" if p.api_key else "", "default_model": p.default_model, "is_default": p.is_default, } for p in providers] @app.post("/api/admin/llm-providers") def admin_create_provider(data: LLMProviderCreate, admin: dict = Depends(require_admin), db: Session = Depends(get_db)): if data.is_default: db.query(LLMProvider).update({"is_default": False}) provider = LLMProvider( name=data.name, provider_type=data.provider_type, api_url=data.api_url, api_key=data.api_key, default_model=data.default_model, is_default=data.is_default, ) db.add(provider) db.commit() return {"id": provider.id, "status": "created"} @app.put("/api/admin/llm-providers/{provider_id}") def admin_update_provider(provider_id: int, update: LLMProviderUpdate, admin: dict = Depends(require_admin), db: Session = Depends(get_db)): provider = db.query(LLMProvider).filter(LLMProvider.id == provider_id).first() if not provider: raise HTTPException(status_code=404) if update.is_default: db.query(LLMProvider).update({"is_default": False}) for field, value in update.model_dump(exclude_none=True).items(): setattr(provider, field, value) db.commit() return {"id": provider.id, "status": "updated"} @app.delete("/api/admin/llm-providers/{provider_id}") def admin_delete_provider(provider_id: int, admin: dict = Depends(require_admin), db: Session = Depends(get_db)): provider = db.query(LLMProvider).filter(LLMProvider.id == provider_id).first() if not provider: raise HTTPException(status_code=404) db.delete(provider) db.commit() return {"status": "deleted"} # --- Admin: Catalog Management --- class CatalogCreate(BaseModel): id: str name: str description: str = "" category: str = "utility" config_schema: dict = {} default_config: dict = {} supports_schedule: bool = True is_sub_agent: bool = False class CatalogUpdate(BaseModel): name: Optional[str] = None description: Optional[str] = None category: Optional[str] = None config_schema: Optional[dict] = None default_config: Optional[dict] = None supports_schedule: Optional[bool] = None is_sub_agent: Optional[bool] = None @app.post("/api/admin/catalog") def admin_create_catalog(data: CatalogCreate, admin: dict = Depends(require_admin), db: Session = Depends(get_db)): if db.query(AgentCatalog).filter(AgentCatalog.id == data.id).first(): raise HTTPException(status_code=409, detail="Catalog entry exists") entry = AgentCatalog(**data.model_dump()) db.add(entry) db.commit() return {"id": entry.id, "status": "created"} @app.put("/api/admin/catalog/{catalog_id}") def admin_update_catalog(catalog_id: str, update: CatalogUpdate, admin: dict = Depends(require_admin), db: Session = Depends(get_db)): entry = db.query(AgentCatalog).filter(AgentCatalog.id == catalog_id).first() if not entry: raise HTTPException(status_code=404) for field, value in update.model_dump(exclude_none=True).items(): setattr(entry, field, value) db.commit() return {"id": entry.id, "status": "updated"} @app.delete("/api/admin/catalog/{catalog_id}") def admin_delete_catalog(catalog_id: str, admin: dict = Depends(require_admin), db: Session = Depends(get_db)): entry = db.query(AgentCatalog).filter(AgentCatalog.id == catalog_id).first() if not entry: raise HTTPException(status_code=404) db.delete(entry) db.commit() return {"status": "deleted"} # --- Bridge Management --- class BridgeRegister(BaseModel): user_id: int api_key: str bridge_url: str hostname: str = "" platform: str = "macos" capabilities: list = [] class BridgeHeartbeat(BaseModel): bridge_url: str = "" capabilities: list = [] @app.post("/api/bridge/register") def register_bridge(data: BridgeRegister, db: Session = Depends(get_db)): """Called by bridge installer to register with the dashboard.""" user = db.query(User).filter(User.id == data.user_id).first() if not user: raise HTTPException(status_code=404, detail="User not found") # Remove existing bridge for this user (one bridge per user) db.query(Bridge).filter(Bridge.user_id == data.user_id).delete() bridge = Bridge( user_id=data.user_id, api_key=data.api_key, bridge_url=data.bridge_url, hostname=data.hostname, platform=data.platform, capabilities=data.capabilities, status="online", last_heartbeat=datetime.now(timezone.utc), ) db.add(bridge) db.commit() return {"status": "registered", "bridge_id": bridge.id} @app.post("/api/bridge/heartbeat") def bridge_heartbeat(data: BridgeHeartbeat, request_api_key: str = None, db: Session = Depends(get_db)): """Called periodically by the bridge to update its status and IP.""" # Auth via X-Bridge-Token header from fastapi import Request return {"status": "ok"} @app.post("/api/bridge/{api_key}/heartbeat") def bridge_heartbeat_keyed(api_key: str, data: BridgeHeartbeat, db: Session = Depends(get_db)): """Heartbeat endpoint keyed by bridge API key.""" bridge = db.query(Bridge).filter(Bridge.api_key == api_key).first() if not bridge: raise HTTPException(status_code=404, detail="Bridge not found") if data.bridge_url: bridge.bridge_url = data.bridge_url if data.capabilities: bridge.capabilities = data.capabilities bridge.status = "online" bridge.last_heartbeat = datetime.now(timezone.utc) db.commit() return {"status": "ok"} @app.get("/api/bridge/me") def get_my_bridge(user: dict = Depends(require_auth), db: Session = Depends(get_db)): """Get the current user's bridge info.""" bridge = db.query(Bridge).filter(Bridge.user_id == user["user_id"]).first() if not bridge: return {"connected": False} # Mark offline if no heartbeat in 10 minutes if bridge.last_heartbeat: age = (datetime.now(timezone.utc) - bridge.last_heartbeat).total_seconds() if age > 600: bridge.status = "offline" db.commit() return { "connected": True, "bridge_url": bridge.bridge_url, "hostname": bridge.hostname, "platform": bridge.platform, "capabilities": bridge.capabilities or [], "status": bridge.status, "last_heartbeat": bridge.last_heartbeat.isoformat() if bridge.last_heartbeat else None, } @app.delete("/api/bridge/me") def disconnect_bridge(user: dict = Depends(require_auth), db: Session = Depends(get_db)): """Disconnect the current user's bridge.""" db.query(Bridge).filter(Bridge.user_id == user["user_id"]).delete() db.commit() return {"status": "disconnected"} @app.get("/api/users/{user_id}/bridge") def get_user_bridge(user_id: int, db: Session = Depends(get_db)): """Internal endpoint for agents to get a user's bridge URL + auth.""" bridge = db.query(Bridge).filter(Bridge.user_id == user_id).first() if not bridge or bridge.status == "offline": return {"available": False} return { "available": True, "bridge_url": bridge.bridge_url, "api_key": bridge.api_key, } @app.get("/api/bridge/install-script") def get_install_script(user: dict = Depends(require_auth), db: Session = Depends(get_db)): """Generate a personalized bridge installer script for the current user.""" user_id = user["user_id"] username = user["username"] api_key = secrets.token_urlsafe(32) dashboard_url = "https://agents.jfamily.io" script = f'''#!/bin/bash # Agent Command Center — Mac Bridge Installer # Generated for: {username} (user {user_id}) # Run this on your Mac to install the Apple Ecosystem Bridge. set -e echo "Installing Agent Command Center Mac Bridge for {username}..." BRIDGE_DIR="$HOME/.local/apple-bridge" mkdir -p "$BRIDGE_DIR" # Download bridge.py cat > "$BRIDGE_DIR/bridge.py" << 'BRIDGE_EOF' BRIDGE_PLACEHOLDER BRIDGE_EOF # Create config cat > "$BRIDGE_DIR/config.json" << 'CONFIG_EOF' {{ "user_id": {user_id}, "username": "{username}", "api_key": "{api_key}", "dashboard_url": "{dashboard_url}", "port": {8551 + user_id} }} CONFIG_EOF # Create launcher cat > "$BRIDGE_DIR/start-bridge.sh" << 'LAUNCHER_EOF' #!/bin/bash exec /opt/homebrew/bin/python3 "$HOME/.local/apple-bridge/bridge.py" LAUNCHER_EOF chmod +x "$BRIDGE_DIR/start-bridge.sh" # Create launchd plist PLIST="$HOME/Library/LaunchAgents/com.jfamily.apple-bridge.plist" cat > "$PLIST" << PLIST_EOF Label com.jfamily.apple-bridge ProgramArguments $BRIDGE_DIR/start-bridge.sh RunAtLoad KeepAlive StandardOutPath /tmp/apple-bridge.log StandardErrorPath /tmp/apple-bridge.err PLIST_EOF # Install Python dependencies echo "Installing Python dependencies..." /opt/homebrew/bin/pip3 install --break-system-packages -q fastapi uvicorn 2>/dev/null || pip3 install --break-system-packages -q fastapi uvicorn # Start the bridge echo "Starting bridge..." launchctl unload "$PLIST" 2>/dev/null launchctl load "$PLIST" sleep 3 # Register with dashboard BRIDGE_IP=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo "unknown") BRIDGE_PORT={8551 + user_id} HOSTNAME=$(hostname) echo "Registering with dashboard..." curl -s -X POST "{dashboard_url}/api/bridge/register" \\ -H "Content-Type: application/json" \\ -d '{{"user_id": {user_id}, "api_key": "{api_key}", "bridge_url": "http://'$BRIDGE_IP':'$BRIDGE_PORT'", "hostname": "'$HOSTNAME'", "platform": "macos", "capabilities": ["notes", "reading-list"]}}' echo "" echo "====================================" echo " Mac Bridge installed for {username}!" echo " Bridge URL: http://$BRIDGE_IP:$BRIDGE_PORT" echo " Dashboard: {dashboard_url}" echo "====================================" ''' from fastapi.responses import PlainTextResponse return PlainTextResponse(script, media_type="text/x-shellscript", headers={"Content-Disposition": f"attachment; filename=install-bridge-{username}.sh"}) # Admin: Bridge overview @app.get("/api/admin/bridges") def admin_list_bridges(admin: dict = Depends(require_admin), db: Session = Depends(get_db)): bridges = db.query(Bridge).all() return [{ "id": b.id, "user_id": b.user_id, "username": db.query(User).filter(User.id == b.user_id).first().username if db.query(User).filter(User.id == b.user_id).first() else "?", "bridge_url": b.bridge_url, "hostname": b.hostname, "platform": b.platform, "capabilities": b.capabilities or [], "status": b.status, "last_heartbeat": b.last_heartbeat.isoformat() if b.last_heartbeat else None, } for b in bridges] # --- Static files --- app.mount("/static", StaticFiles(directory="static"), name="static") @app.get("/login") def login_page(): return FileResponse("static/login.html") @app.get("/admin") def admin_page(session: Optional[str] = Cookie(None)): user = get_current_user(session) if not user: return RedirectResponse("/login", status_code=302) if user["role"] != "admin": return RedirectResponse("/", status_code=302) return FileResponse("static/admin.html") @app.get("/") def root(session: Optional[str] = Cookie(None)): user = get_current_user(session) if not user: return RedirectResponse("/login", status_code=302) return FileResponse("static/index.html") # --- Startup --- @app.on_event("startup") def startup(): init_db()