Built the Sentinel Tab

This commit is contained in:
Sagnik
2026-04-12 02:02:58 +05:30
parent fb656d1443
commit 075ab280ad
526 changed files with 17646 additions and 70931 deletions

View File

@@ -1,29 +1,68 @@
"""
The Catalyst — FastAPI Backend
Autonomous Digital Marketing Agency powered by Meta Marketing API.
Velocity — Unified FastAPI Backend
Covers: Catalyst (Meta Marketing), Sentinel (QD Engine), Vault (Trackable Links), Auth
GPU partitioning on AWS:
- NemoClaw / Ollama → CUDA devices 0, 1 (enforced in nemoclaw.service systemd unit)
- ComfyUI / Wan 2.2 → CUDA devices 2, 3 (enforced in comfyui.service systemd unit)
"""
import os
import json
import asyncio
import logging
from contextlib import asynccontextmanager
from datetime import datetime
from typing import Set
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from dotenv import load_dotenv
from api.routes_catalyst import router as catalyst_router
from oracle.router_v1 import router as oracle_router
from backend.api.routes_catalyst import router as catalyst_router
from backend.auth.dependencies import (
create_access_token, verify_password, get_current_user
)
from backend.db.pool import create_pool, close_pool
from backend.routers.cctv import router as cctv_router
from backend.routers.scenes import router as scenes_router
from backend.routers.videos import router as videos_router
from backend.routers.vault import router as vault_router
from backend.routers.sentinel import router as sentinel_router, broadcast_sentinel_event
load_dotenv()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("velocity.main")
# ── Lifespan: DB pool init / teardown ─────────────────────────────────────────
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
try:
app.state.db_pool = await create_pool()
logger.info("asyncpg pool created")
except Exception as exc:
logger.error("Failed to create DB pool: %s", exc)
app.state.db_pool = None
app.state.broadcast_sentinel_event = broadcast_sentinel_event
yield
# Shutdown
await close_pool()
logger.info("asyncpg pool closed")
# ── App ───────────────────────────────────────────────────────────────────────
app = FastAPI(
title="Velocity — Catalyst Backend",
description="Meta Marketing API integration for autonomous campaign management.",
version="1.0.0",
title="Velocity — Neural Core",
description="Unified backend: Catalyst, Sentinel QD Engine, Vault, Oracle, Auth.",
version="2.0.0",
lifespan=lifespan,
)
# ── CORS ──────────────────────────────────────────────────────────────────────
@@ -38,16 +77,71 @@ app.add_middleware(
allow_headers=["*"],
)
# ── Static asset serving (Vault files) ───────────────────────────────────────
ASSET_DIR = os.getenv("VELOCITY_ASSET_DIR", "/opt/dlami/nvme/assets")
if os.path.isdir(ASSET_DIR):
app.mount("/assets", StaticFiles(directory=ASSET_DIR), name="assets")
# ── Routers ───────────────────────────────────────────────────────────────────
app.include_router(catalyst_router, prefix="/api/catalyst", tags=["Catalyst"])
app.include_router(oracle_router, prefix="/api/oracle/v1", tags=["Oracle"])
app.include_router(sentinel_router, prefix="/api/sentinel", tags=["Sentinel"])
app.include_router(cctv_router, prefix="/api/cctv", tags=["CCTV"])
app.include_router(scenes_router, prefix="/api/scenes", tags=["Scenes"])
app.include_router(videos_router, prefix="/api/videos", tags=["Videos"])
app.include_router(vault_router, prefix="/api/vault", tags=["Vault"])
# ── WebSocket — Live Optimization Feed ────────────────────────────────────────
# Public vault link (no /api prefix — shared externally with prospects)
from backend.routers.vault import router as public_vault_router
app.include_router(public_vault_router, prefix="/vault", tags=["Vault Public"])
class ConnectionManager:
"""Manages active WebSocket connections for live optimization broadcasts."""
# ── Auth endpoint ─────────────────────────────────────────────────────────────
from fastapi import HTTPException, status
from pydantic import BaseModel
class LoginRequest(BaseModel):
email: str
password: str
@app.post("/api/auth/login", tags=["Auth"])
async def login(body: LoginRequest):
"""
Authenticate a user and return a JWT.
Credentials are verified against the users_and_roles table.
"""
from backend.db.pool import get_pool
from fastapi import Request
pool = app.state.db_pool
if pool is None:
raise HTTPException(status_code=503, detail="Database unavailable.")
async with pool.acquire() as conn:
row = await conn.fetchrow(
"SELECT id::text, role, password_hash FROM users_and_roles WHERE email = $1 AND is_active = TRUE",
body.email,
)
if not row or not verify_password(body.password, row["password_hash"]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password.",
)
token = create_access_token(user_id=row["id"], role=row["role"])
return {"access_token": token, "token_type": "bearer", "expires_in": 28800}
@app.get("/api/auth/me", tags=["Auth"])
async def me(user=get_current_user):
return {"user_id": user.user_id, "role": user.role}
# ── Catalyst WebSocket (preserved from v1) ────────────────────────────────────
class _CatalystManager:
def __init__(self) -> None:
self.active: Set[WebSocket] = set()
@@ -68,34 +162,21 @@ class ConnectionManager:
self.active -= dead
manager = ConnectionManager()
_catalyst_mgr = _CatalystManager()
@app.websocket("/ws/catalyst")
async def websocket_endpoint(ws: WebSocket) -> None:
"""
WebSocket endpoint for streaming live Claw Agent optimization events.
Clients connect from <LiveOptimizationFeed /> in Catalyst.tsx.
"""
await manager.connect(ws)
async def catalyst_ws(ws: WebSocket) -> None:
await _catalyst_mgr.connect(ws)
try:
while True:
# Keep-alive: wait for any incoming ping/message
data = await ws.receive_text()
# Echo back as acknowledgment (clients may send heartbeat pings)
await ws.send_text(json.dumps({"type": "ack", "data": data}))
except WebSocketDisconnect:
manager.disconnect(ws)
_catalyst_mgr.disconnect(ws)
# ── Helper: broadcast a live event (called from routes after API mutations) ───
async def broadcast_live_event(
event_type: str,
message: str,
campaign_name: str | None = None,
value: str | None = None,
) -> None:
async def broadcast_live_event(event_type, message, campaign_name=None, value=None):
payload = {
"type": event_type,
"message": message,
@@ -103,15 +184,23 @@ async def broadcast_live_event(
"value": value,
"timestamp": datetime.utcnow().isoformat(),
}
await manager.broadcast(payload)
await _catalyst_mgr.broadcast(payload)
# Attach broadcaster so routes can call it
app.state.broadcast_live_event = broadcast_live_event
# ── Health check ──────────────────────────────────────────────────────────────
# ── Health ─────────────────────────────────────────────────────────────────────
@app.get("/health", tags=["Health"])
async def health() -> dict:
return {"status": "ok", "service": "catalyst-backend", "version": "1.0.0"}
pool = app.state.db_pool
db_ok = pool is not None
return {
"status": "ok",
"service": "velocity-backend",
"version": "2.0.0",
"db_pool": "connected" if db_ok else "unavailable",
"timestamp": datetime.utcnow().isoformat(),
}