- Add SSE event bus in sync_service (subscribe/unsubscribe/_emit) - Add GET /api/sync/stream SSE endpoint for real-time sync progress - Rewrite logs.html: unified runs table + live feed + summary + filters - Rewrite logs.js: SSE EventSource client, run selection, pagination - Dashboard: clickable runs navigate to /logs?run=, sync started banner - Remove "Import Comenzi" nav item, delete sync_detail.html - Add error_message column to sync_runs table with migration - Fix: export TNS_ADMIN as OS env var so oracledb finds tnsnames.ora - Fix: use get_oracle_connection() instead of direct pool.acquire() - Fix: CRM_POLITICI_PRET_ART INSERT to match actual table schema Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
143 lines
4.5 KiB
Python
143 lines
4.5 KiB
Python
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()
|