"""FlowDeck — /api/v2/sync endpoints (v6.0.0 PWA offline sync). Pairs with ``app/services/sync_engine.py``. All routes require an authenticated session (``flowdeck_session`` cookie). """ from __future__ import annotations import logging from fastapi import APIRouter, HTTPException, Query, Request from fastapi.responses import JSONResponse from app.auth.session import SessionManager from app.services.sync_engine import SyncEngine logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v2/sync", tags=["sync"]) _engine = SyncEngine() def _user(request: Request) -> dict: user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) if not user: raise HTTPException(status_code=401, detail="Authentication required") return user @router.get("/delta") async def sync_delta( request: Request, since: float = Query(default=0, description="Epoch seconds (ou ms) du dernier sync"), workspace_id: int = Query(default=None), ): """Pull server-side changes since `since` (for the given workspace).""" user = _user(request) if workspace_id is None: raise HTTPException(status_code=400, detail="workspace_id is required") result = await _engine.get_delta(user["id"], since, workspace_id) if result.get("error") == "forbidden": return JSONResponse({"detail": "Forbidden"}, status_code=403) return result @router.post("/batch") async def sync_batch(request: Request): """Apply a batch of offline mutations and return per-mutation results.""" user = _user(request) try: body = await request.json() except Exception: raise HTTPException(status_code=400, detail="Invalid JSON body") from None mutations = body.get("mutations") or [] device_id = body.get("device_id") or "unknown" if not isinstance(mutations, list) or not mutations: return {"results": [], "conflicts": [], "server_time": SyncEngine._now_epoch()} result = await _engine.apply_batch(user["id"], mutations, device_id) result["server_time"] = SyncEngine._now_epoch() return result @router.get("/status") async def sync_status(request: Request, workspace_id: int = Query(default=None)): """Synchronization status for the workspace (pending server queue, last sync).""" user = _user(request) from app.db import get_conn with get_conn() as conn: if not SyncEngine._can_access(conn, user["id"], workspace_id): return JSONResponse({"detail": "Forbidden"}, status_code=403) pending = conn.execute( "SELECT COUNT(*) AS n FROM offline_sync_queue WHERE user_id=? AND status='pending'", (user["id"],), ).fetchone()["n"] last = conn.execute( "SELECT MAX(created_at) AS last FROM offline_sync_queue " "WHERE user_id=? AND status='synced'", (user["id"],), ).fetchone()["last"] return { "pending_count": pending, "last_sync": last, "is_syncing": False, "server_time": SyncEngine._now_epoch(), "workspace_id": workspace_id, }