API-KA — plateforme centrale : collecte quotidienne des 8 services KA, historisation append-only et API publique sur www.api-ka.com
Python 60.9%
HTML 21%
TypeScript 7.3%
JavaScript 5.2%
CSS 4.8%
Shell 0.8%
1# ============================================2# Projet : API-KA3# Fichier : src/api/routes/monitoring.py4# Node : m3u96b5# Author : Simon-Pierre Boucher6# Contact : contact@spboucher.ai7# Date : 2026-08-188# ============================================9"""Routes publiques de supervision des connecteurs de l'écosystème KA.1011L'état est produit toutes les 2 h par ``src.monitoring.connector_health``12(job apika-scheduler) et stocké dans la table ``connector_health``.13"""1415from __future__ import annotations1617from typing import Any1819from fastapi import APIRouter, Depends, HTTPException20from sqlalchemy import select21from sqlalchemy.orm import Session2223from src.api.routes import envelope24from src.config import SERVICES25from src.database.db import get_db26from src.database.models import ConnectorHealth27from src.monitoring.connector_health import ALL_STATUSES, APP_SOURCE2829router = APIRouter(prefix="/api/v1/monitoring", tags=["monitoring"])303132def _row_to_dict(row: ConnectorHealth) -> dict[str, Any]:33 return {34 "source": row.source,35 "status": row.status,36 "checked_at": row.checked_at.isoformat() if row.checked_at else None,37 "last_success": row.last_success.isoformat() if row.last_success else None,38 "found_last": row.found_last,39 "median_found": row.median_found,40 "consecutive_failures": row.consecutive_failures,41 "message": row.message,42 "last_seen": row.last_seen.isoformat() if row.last_seen else None,43 }444546def _empty_counts() -> dict[str, int]:47 return dict.fromkeys(ALL_STATUSES, 0)484950def _group_rows(rows: list[ConnectorHealth]) -> dict[str, Any]:51 services: dict[str, Any] = {}52 for row in rows:53 bucket = services.setdefault(54 row.service,55 {"summary": _empty_counts(), "app": None, "connectors": []},56 )57 if row.status in bucket["summary"]:58 bucket["summary"][row.status] += 159 payload = _row_to_dict(row)60 if row.source == APP_SOURCE:61 bucket["app"] = payload62 else:63 bucket["connectors"].append(payload)64 return services656667@router.get("/connectors")68def connectors(db: Session = Depends(get_db)) -> dict[str, Any]:69 """État courant de tous les connecteurs, groupé par service."""70 rows = (71 db.execute(72 select(ConnectorHealth).order_by(73 ConnectorHealth.service, ConnectorHealth.source74 )75 )76 .scalars()77 .all()78 )79 services = _group_rows(rows)8081 summary = _empty_counts()82 for bucket in services.values():83 for status, count in bucket["summary"].items():84 summary[status] += count8586 data = {87 "summary": summary,88 "services": services,89 }90 return envelope(data, extra_meta={"services_total": len(services)})919293@router.get("/connectors/{service}")94def connectors_service(95 service: str, db: Session = Depends(get_db)96) -> dict[str, Any]:97 """État courant des connecteurs d'un service donné."""98 if service not in SERVICES:99 raise HTTPException(status_code=404, detail=f"Service inconnu : {service}")100 rows = (101 db.execute(102 select(ConnectorHealth)103 .where(ConnectorHealth.service == service)104 .order_by(ConnectorHealth.source)105 )106 .scalars()107 .all()108 )109 bucket = _group_rows(rows).get(110 service, {"summary": _empty_counts(), "app": None, "connectors": []}111 )112 data = {"service": service, **bucket}113 return envelope(data)114