# ============================================ # Projet : API-KA # Fichier : src/api/routes/monitoring.py # Node : m3u96b # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Date : 2026-08-18 # ============================================ """Routes publiques de supervision des connecteurs de l'écosystème KA. L'état est produit toutes les 2 h par ``src.monitoring.connector_health`` (job apika-scheduler) et stocké dans la table ``connector_health``. """ from __future__ import annotations from typing import Any from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.orm import Session from src.api.routes import envelope from src.config import SERVICES from src.database.db import get_db from src.database.models import ConnectorHealth from src.monitoring.connector_health import ALL_STATUSES, APP_SOURCE router = APIRouter(prefix="/api/v1/monitoring", tags=["monitoring"]) def _row_to_dict(row: ConnectorHealth) -> dict[str, Any]: return { "source": row.source, "status": row.status, "checked_at": row.checked_at.isoformat() if row.checked_at else None, "last_success": row.last_success.isoformat() if row.last_success else None, "found_last": row.found_last, "median_found": row.median_found, "consecutive_failures": row.consecutive_failures, "message": row.message, "last_seen": row.last_seen.isoformat() if row.last_seen else None, } def _empty_counts() -> dict[str, int]: return dict.fromkeys(ALL_STATUSES, 0) def _group_rows(rows: list[ConnectorHealth]) -> dict[str, Any]: services: dict[str, Any] = {} for row in rows: bucket = services.setdefault( row.service, {"summary": _empty_counts(), "app": None, "connectors": []}, ) if row.status in bucket["summary"]: bucket["summary"][row.status] += 1 payload = _row_to_dict(row) if row.source == APP_SOURCE: bucket["app"] = payload else: bucket["connectors"].append(payload) return services @router.get("/connectors") def connectors(db: Session = Depends(get_db)) -> dict[str, Any]: """État courant de tous les connecteurs, groupé par service.""" rows = ( db.execute( select(ConnectorHealth).order_by( ConnectorHealth.service, ConnectorHealth.source ) ) .scalars() .all() ) services = _group_rows(rows) summary = _empty_counts() for bucket in services.values(): for status, count in bucket["summary"].items(): summary[status] += count data = { "summary": summary, "services": services, } return envelope(data, extra_meta={"services_total": len(services)}) @router.get("/connectors/{service}") def connectors_service( service: str, db: Session = Depends(get_db) ) -> dict[str, Any]: """État courant des connecteurs d'un service donné.""" if service not in SERVICES: raise HTTPException(status_code=404, detail=f"Service inconnu : {service}") rows = ( db.execute( select(ConnectorHealth) .where(ConnectorHealth.service == service) .order_by(ConnectorHealth.source) ) .scalars() .all() ) bucket = _group_rows(rows).get( service, {"summary": _empty_counts(), "app": None, "connectors": []} ) data = {"service": service, **bucket} return envelope(data)