import asyncio import json from pathlib import Path from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from starlette.responses import Response from app.db import close_pool, fetch_all, init_pool from app.routes import ( retail, agents, analytics, documents, api, clients, dashboard, deals, herman, studio, marketing, monitor, products, reports, suppliers, voice, settings, browser, hermes, beurs, packaging, ops, ops_api, revenue_cockpit, export_intel, ) from app.routes.revenue_cockpit import api as revenue_cockpit_api from app.routes.admin_api import admin_router, ai_router, herman_api, voice_api from app.routes.settings_api import settings_router from app.routes.agents_api import router as agents_api_router from app.routes.marketing_api import router as marketing_api_router from app.routes.projects_api import router as projects_api_router BASE_DIR = Path(__file__).resolve().parent.parent templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) class NoCacheStaticFiles(StaticFiles): """Serve static assets without browser/SW long-lived caching.""" async def get_response(self, path: str, scope) -> Response: response = await super().get_response(path, scope) if path.endswith((".css", ".js", ".html")) or "/css/" in path or "/js/" in path: response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0" response.headers["Pragma"] = "no-cache" response.headers["Expires"] = "0" return response app = FastAPI(title="Foodlinkk Command Center", version="2.5.0") app.mount("/static", NoCacheStaticFiles(directory=str(BASE_DIR / "static")), name="static") for r in ( dashboard.router, beurs.router, agents.router, marketing.router, retail.router, clients.router, deals.router, products.router, suppliers.router, monitor.router, analytics.router, documents.router, reports.router, voice.router, settings.router, browser.router, herman.router, studio.router, hermes.router, packaging.router, ops.router, revenue_cockpit.router, export_intel.router, revenue_cockpit_api, api.router, ops_api.router, admin_router, ai_router, herman_api, voice_api, settings_router, agents_api_router, marketing_api_router, projects_api_router, ): app.include_router(r) @app.on_event("startup") def on_startup() -> None: init_pool() @app.on_event("shutdown") def on_shutdown() -> None: close_pool() @app.websocket("/ws/agents") async def ws_agents(websocket: WebSocket) -> None: await websocket.accept() last_payload: str | None = None try: while True: try: rows = fetch_all( """SELECT id, agent_name, event_type, title, body, status, created_at FROM agent_events ORDER BY created_at DESC LIMIT 50""" ) for row in rows: if row.get("created_at") is not None: row["created_at"] = row["created_at"].isoformat() payload = json.dumps({"events": rows}) except Exception as exc: payload = json.dumps({"error": str(exc), "events": []}) if payload != last_payload: await websocket.send_text(payload) last_payload = payload await asyncio.sleep(3) except WebSocketDisconnect: return @app.websocket("/ws/feed") async def ws_feed(websocket: WebSocket) -> None: await websocket.accept() try: while True: snapshot = {"type": "heartbeat", "events": []} try: snapshot["events"] = fetch_all( "SELECT agent_name, event_type, title, status, created_at FROM agent_events ORDER BY created_at DESC LIMIT 15" ) for row in snapshot["events"]: if row.get("created_at"): row["created_at"] = row["created_at"].isoformat() except Exception as exc: snapshot["error"] = str(exc) await websocket.send_text(json.dumps(snapshot)) await asyncio.sleep(5) except WebSocketDisconnect: return