import asyncio import json from fastapi import APIRouter, Request, BackgroundTasks from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse from starlette.responses import StreamingResponse from pydantic import BaseModel from pathlib import Path from typing import Optional from ..services import sync_service, scheduler_service, sqlite_service router = APIRouter(tags=["sync"]) templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates")) class ScheduleConfig(BaseModel): enabled: bool interval_minutes: int = 5 # SSE streaming endpoint @router.get("/api/sync/stream") async def sync_stream(request: Request): """SSE stream for real-time sync progress.""" q = sync_service.subscribe() async def event_generator(): try: while True: # Check if client disconnected if await request.is_disconnected(): break try: event = await asyncio.wait_for(q.get(), timeout=15.0) yield f"data: {json.dumps(event)}\n\n" if event.get("type") in ("completed", "failed"): break except asyncio.TimeoutError: yield f"data: {json.dumps({'type': 'keepalive'})}\n\n" finally: sync_service.unsubscribe(q) return StreamingResponse( event_generator(), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"} ) # API endpoints @router.post("/api/sync/start") async def start_sync(background_tasks: BackgroundTasks): """Trigger a sync run in the background.""" result = await sync_service.prepare_sync() if result.get("error"): return {"error": result["error"], "run_id": result.get("run_id")} run_id = result["run_id"] background_tasks.add_task(sync_service.run_sync, run_id=run_id) return {"message": "Sync started", "run_id": run_id} @router.post("/api/sync/stop") async def stop_sync(): """Stop a running sync.""" sync_service.stop_sync() return {"message": "Stop signal sent"} @router.get("/api/sync/status") async def sync_status(): """Get current sync status.""" status = await sync_service.get_sync_status() stats = await sqlite_service.get_dashboard_stats() return {**status, "stats": stats} @router.get("/api/sync/history") async def sync_history(page: int = 1, per_page: int = 20): """Get sync run history.""" return await sqlite_service.get_sync_runs(page, per_page) @router.get("/logs", response_class=HTMLResponse) async def logs_page(request: Request, run: str = None): return templates.TemplateResponse("logs.html", {"request": request, "selected_run": run or ""}) @router.get("/api/sync/run/{run_id}") async def sync_run_detail(run_id: str): """Get details for a specific sync run.""" detail = await sqlite_service.get_sync_run_detail(run_id) if not detail: return {"error": "Run not found"} return detail @router.get("/api/sync/run/{run_id}/log") async def sync_run_log(run_id: str): """Get detailed log per order for a sync run.""" detail = await sqlite_service.get_sync_run_detail(run_id) if not detail: return {"error": "Run not found", "status_code": 404} orders = detail.get("orders", []) return { "run_id": run_id, "run": detail.get("run", {}), "orders": [ { "order_number": o.get("order_number"), "customer_name": o.get("customer_name"), "items_count": o.get("items_count"), "status": o.get("status"), "error_message": o.get("error_message"), "missing_skus": o.get("missing_skus"), } for o in orders ] } @router.put("/api/sync/schedule") async def update_schedule(config: ScheduleConfig): """Update scheduler configuration.""" if config.enabled: scheduler_service.start_scheduler(config.interval_minutes) else: scheduler_service.stop_scheduler() # Persist config await sqlite_service.set_scheduler_config("enabled", str(config.enabled)) await sqlite_service.set_scheduler_config("interval_minutes", str(config.interval_minutes)) return scheduler_service.get_scheduler_status() @router.get("/api/sync/schedule") async def get_schedule(): """Get current scheduler status.""" return scheduler_service.get_scheduler_status()