- Add /logs page with per-order sync run details, filters (Toate/Importate/Fara Mapare/Erori) - Add price pre-validation (validate_prices + ensure_prices) to prevent ORA-20000 on direct articles - Add find_new_orders() to detect orders not yet in Oracle COMENZI - Extend missing_skus table with order context (order_count, order_numbers, customers) - Add server-side pagination on /api/validate/missing-skus and /missing-skus page - Replace confusing "Skip"/"Err" with "Fara Mapare"/"Erori" terminology - Add inline mapping modal on dashboard (replaces navigation to /mappings) - Add 2-row stat cards: orders (Comenzi Noi/Ready/Importate/Fara Mapare/Erori) + articles - Add ID_POL/ID_GESTIUNE/ID_SECTIE to config.py and .env - Update .gitignore (venv, *.db, api/api/, logs/) - 33/33 unit tests pass, E2E verified with Playwright Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
120 lines
3.7 KiB
Python
120 lines
3.7 KiB
Python
from fastapi import APIRouter, Request, BackgroundTasks
|
|
from fastapi.templating import Jinja2Templates
|
|
from fastapi.responses import HTMLResponse
|
|
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
|
|
|
|
|
|
# HTML pages
|
|
@router.get("/sync", response_class=HTMLResponse)
|
|
async def sync_page(request: Request):
|
|
return templates.TemplateResponse("dashboard.html", {"request": request})
|
|
|
|
|
|
@router.get("/sync/run/{run_id}", response_class=HTMLResponse)
|
|
async def sync_detail_page(request: Request, run_id: str):
|
|
return templates.TemplateResponse("sync_detail.html", {"request": request, "run_id": run_id})
|
|
|
|
|
|
# API endpoints
|
|
@router.post("/api/sync/start")
|
|
async def start_sync(background_tasks: BackgroundTasks):
|
|
"""Trigger a sync run in the background."""
|
|
status = await sync_service.get_sync_status()
|
|
if status.get("status") == "running":
|
|
return {"error": "Sync already running", "run_id": status.get("run_id")}
|
|
|
|
background_tasks.add_task(sync_service.run_sync)
|
|
return {"message": "Sync started"}
|
|
|
|
|
|
@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):
|
|
return templates.TemplateResponse("logs.html", {"request": request})
|
|
|
|
|
|
@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()
|