diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8503990 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog + +Format : [Keep a Changelog](https://keepachangelog.com/fr/) · Versioning sémantique. + +## [1.1.0] — 2026-06-20 + +### Ajouté +- Authentification **JWT** (`/auth/login`, `/auth/me`) + hachage bcrypt (`auth.py`). +- **RBAC** : protection de tous les endpoints de données (401 sans token), routes `/admin/*` + réservées au rôle `Admin` (403 sinon). +- Endpoints d'administration : CRUD utilisateurs (`/admin/users`), reset de mot de passe, + journal d'audit (`/admin/journal`). +- **RGPD** : `GET /me/data-export` (portabilité), `DELETE /me` (droit à l'oubli + anonymisation). +- En-têtes de sécurité HTTP + rate-limit `5/min` sur le login (`slowapi`). +- Connexion DB et CORS configurables par variables d'environnement (auth SQL, Driver 18). +- `Dockerfile` + CI Gitea Actions (build → tests pytest → push image) + notifications ntfy. +- Tests `pytest` (auth, RBAC, endpoints) avec curseur SQL simulé. +- Tables `[USER]` + `JOURNAL_AUDIT` et comptes de démo. + +### Modifié +- CORS : tous les verbes + credentials (au lieu de `GET` seul). + +### Sécurité +- Plus aucun secret en dur côté déploiement (secrets via env / fichiers hors dépôt). +- La base s'exécute sur un SQL Server partagé ; compte applicatif non-`sa`. + +## [1.0.0] — 2026-04 +- Version initiale (A. Coyaud) : API de lecture sur SQL Server (référentiels, monitorings, + dashboard, historique, évolution). diff --git a/README.md b/README.md index f8d5445..673392b 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,85 @@ # Api-DataSentinel -API FastAPI pour Data Sentinel. +API **FastAPI** de monitoring de la qualité des données — Data Sentinel (XEFI). +Backend en lecture seule sur SQL Server, sécurisé par authentification JWT. -## Installation +## Stack -1. Ouvrir un terminal dans le dossier du projet : - ```powershell - cd "c:\Users\antho\Desktop\Projet de fin d'année\Projet fin d_année\DataSentinel\Api-DataSentinel" - ``` -2. Créer et activer l'environnement virtuel (si nécessaire) : - ```powershell - python -m venv .venv - .\.venv\Scripts\Activate.ps1 - ``` -3. Installer les dépendances : - ```powershell - pip install -r requirements.txt - ``` +- Python 3.12 · FastAPI 0.136 · uvicorn +- SQL Server 2022 via `pyodbc` (ODBC Driver 18) +- Auth : JWT (`python-jose`) + bcrypt (`passlib`), rate-limit (`slowapi`) -## Lancement de l'API - -Utiliser la commande suivante pour démarrer le serveur : +## Lancement en local (dev) ```powershell -.\.venv\Scripts\python.exe -m uvicorn main:app --reload --port 8000 +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +pip install -r requirements.txt +python -m uvicorn main:app --reload --port 8000 ``` -> Important : si le lanceur `\.venv\Scripts\uvicorn.exe` est cassé après un déplacement de dossier, utilisez toujours `python -m uvicorn`. +- Swagger : `http://127.0.0.1:8000/docs` · Santé : `http://127.0.0.1:8000/health` -## Vérification +Sans variables d'environnement, l'API se connecte en authentification Windows +(`LaptopCA\SQLEXPRESS`) — comportement de dev d'origine, inchangé. -- Swagger : `http://127.0.0.1:8000/docs` -- Santé : `http://127.0.0.1:8000/health` +## Configuration (variables d'environnement) + +| Variable | Rôle | Défaut | +|----------|------|--------| +| `DB_SERVER` | hôte SQL Server (active l'auth SQL si défini) | — (sinon Windows local) | +| `DB_PORT` / `DB_NAME` | port / base | `1433` / `DataSentinel` | +| `DB_USER` / `DB_PASSWORD` | compte applicatif | — | +| `DB_DRIVER` | pilote ODBC | `ODBC Driver 18 for SQL Server` | +| `CORS_ORIGINS` | origines autorisées (séparées par `,`) | `localhost:5173,localhost:3000` | +| `JWT_SECRET` | clé de signature JWT | placeholder (à définir en prod) | +| `JWT_ALGORITHM` / `JWT_EXPIRE_MINUTES` | algo / durée du token | `HS256` / `60` | + +## Authentification & rôles + +- `POST /auth/login` (form `username`/`password`) → `{access_token, token_type, user}`. Rate-limité **5/min**. +- `GET /auth/me` → profil courant. +- **Tous les endpoints de données exigent** `Authorization: Bearer ` (401 sinon). Seul `/health` est public. +- Rôles : `Admin`, `Superviseur`, `Consultant`. Les routes `/admin/*` exigent `Admin` (403 sinon). + +### Comptes de démo + +| Identifiant | Mot de passe | Rôle | +|-------------|--------------|------| +| `admin` | `Admin2026!` | Admin | +| `superviseur` | `Super2026!` | Superviseur | +| `consultant` | `Conseil2026!` | Consultant | + +## Endpoints (résumé) + +- **Données** (protégés) : `/categories`, `/services`, `/contacts`, `/monitorings[...]`, + `/dashboard[...]`, `/historique[...]`, `/evolution/*`. +- **Admin** (`Admin`) : `GET/POST /admin/users`, `PUT/DELETE /admin/users/{id}`, + `POST /admin/users/{id}/reset-password`, `GET /admin/journal`. +- **RGPD** : `GET /me/data-export` (portabilité), `DELETE /me` (droit à l'oubli + anonymisation). + +Spécification complète : `GET /openapi.json` (export dans `docs/openapi.json`). + +## Sécurité + +- En-têtes : `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Strict-Transport-Security`. +- CORS restreint aux origines `CORS_ORIGINS`, tous verbes + credentials. +- Requêtes SQL **paramétrées** (noms de tables/colonnes whitelistés) ; le compte applicatif + n'est pas `sa`. Tables d'auth : `[USER]` + `JOURNAL_AUDIT` (`sql/data_sentinel_auth.sql`). +- Journal d'audit alimenté à chaque login + action admin. + +## Tests + +```bash +pip install pytest httpx && pytest -q +``` +Curseur SQL simulé (aucune vraie BDD) : santé, auth (succès/échec), protection 401, RBAC 403, 404. + +## Docker & CI/CD + +- `Dockerfile` : image Python 3.12 + `msodbcsql18`. +- CI Gitea Actions (`.gitea/workflows/build.yml`) : **build → pytest (dans l'image) → push** + vers `git.nfteam.ovh/neckfire/datasentinel-api` + notification ntfy. Déclenché sur push `main`. +- Déploiement, exploitation, sauvegarde : voir `RUNBOOK.md` et le stack d'hébergement + (`homelab/dev/datasentinel/`). La base tourne sur un **SQL Server partagé** (`dev-mssql`), + pas dédiée à l'app. diff --git a/RUNBOOK.md b/RUNBOOK.md new file mode 100644 index 0000000..6aeec86 --- /dev/null +++ b/RUNBOOK.md @@ -0,0 +1,80 @@ +# RUNBOOK — Data Sentinel (déploiement & exploitation) + +Hébergement sur le homelab `nfteam.ovh`. Stack d'exécution : +`~/Documents/homelab/dev/datasentinel/` (API + Front) et `~/Documents/homelab/dev/mssql/` +(SQL Server partagé `dev-mssql`, réseau externe `dev-shared`). + +## Architecture + +| Composant | URL publique | Port interne | Conteneur | +|-----------|--------------|--------------|-----------| +| Front (React) | https://datasentinel.nfteam.ovh | 8086 | `datasentinel-front` | +| API (FastAPI) | https://datasentinel-api.nfteam.ovh | 8001 | `datasentinel-api` | +| Base | — (réseau `dev-shared`) | 1433 | `dev-mssql` (partagé) | + +Reverse proxy : Nginx Proxy Manager (HTTPS Let's Encrypt). CI : Gitea Actions → +images poussées au registre `git.nfteam.ovh`, déployées par `docker compose` / Watchtower. + +## Déploiement / mise à jour + +Un push sur `main` (API ou Front) déclenche la CI (build → tests → push image). +Récupérer la dernière image et redéployer : +```bash +cd ~/Documents/homelab/dev/datasentinel +docker compose pull && docker compose up -d +``` +Watchtower met aussi à jour automatiquement (~5 min). + +## Diagnostic incidents + +| Symptôme | Diagnostic | Résolution | +|----------|-----------|------------| +| Front KO | `curl -I https://datasentinel.nfteam.ovh` ; `docker logs datasentinel-front` | `docker compose restart datasentinel-front` | +| API 5xx | `curl https://datasentinel-api.nfteam.ovh/health` → champ `database` | si `error` → voir BDD ci-dessous ; `docker logs datasentinel-api` | +| BDD injoignable | `docker ps | grep dev-mssql` ; tester la connexion (cf. ci-dessous) | `docker compose -f ~/Documents/homelab/dev/mssql/docker-compose.yml up -d` | +| Token invalide / 401 partout | vérifier `JWT_SECRET` dans `api.env` (ne pas le changer à chaud : invalide les sessions) | re-login | +| Login 429 | rate-limit 5/min atteint | attendre 1 min | + +Connexion BDD (admin) : +```bash +SA=$(grep ^SA_PASSWORD= ~/Documents/homelab/dev/mssql/.env | cut -d= -f2-) +docker run --rm --network dev-shared mcr.microsoft.com/mssql-tools \ + /opt/mssql-tools/bin/sqlcmd -S dev-mssql -U sa -P "$SA" -d DataSentinel -Q "SELECT COUNT(*) FROM [USER];" +``` + +## (Re)chargement du schéma + +```bash +SA=$(grep ^SA_PASSWORD= ~/Documents/homelab/dev/mssql/.env | cut -d= -f2-) +for f in data_sentinel_init.sql data_sentinel_auth.sql; do + docker run --rm --network dev-shared \ + -v ~/Documents/homelab/dev/datasentinel/sql:/sql:ro mcr.microsoft.com/mssql-tools \ + /opt/mssql-tools/bin/sqlcmd -S dev-mssql -U sa -P "$SA" -i /sql/$f +done +``` + +## Sauvegarde / restauration + +> ⚠️ Stratégie à finaliser avec le disque de sauvegarde dédié du homelab. + +Sauvegarde logique recommandée (quotidienne) : +```bash +SA=$(grep ^SA_PASSWORD= ~/Documents/homelab/dev/mssql/.env | cut -d= -f2-) +docker exec dev-mssql /opt/mssql-tools*/bin/sqlcmd -S localhost -U sa -P "$SA" \ + -Q "BACKUP DATABASE DataSentinel TO DISK='/var/opt/mssql/backup/DataSentinel.bak' WITH FORMAT, INIT, COMPRESSION" +``` +(monter un volume `/var/opt/mssql/backup` vers le disque de sauvegarde). Le volume +Docker `mssql_data` contient les fichiers de la base. RTO visé < 4h, RPO < 24h. + +## Rollback + +Redéployer une image précise par son tag SHA (au lieu de `latest`) : +```bash +# dans dev/datasentinel/docker-compose.yml : image: .../datasentinel-api: +docker compose up -d datasentinel-api +``` + +## Contacts + +- Hébergement / infra : administrateur homelab (neckfire). +- Application / code : A. Coyaud (auteur, dépôts GitHub). diff --git a/docs/openapi.json b/docs/openapi.json new file mode 100644 index 0000000..49aba95 --- /dev/null +++ b/docs/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"Data Sentinel API","description":"API de monitoring de la qualité des données — XEFI","version":"1.0.0"},"paths":{"/health":{"get":{"tags":["Système"],"summary":"Health Check","description":"Ping API + test connexion SQL Server.","operationId":"health_check_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/auth/login":{"post":{"tags":["Auth"],"summary":"Login","description":"Authentifie un utilisateur et retourne un JWT.","operationId":"login_auth_login_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_login_auth_login_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/me":{"get":{"tags":["Auth"],"summary":"Me","description":"Profil de l'utilisateur connecté.","operationId":"me_auth_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/admin/users":{"get":{"tags":["Admin"],"summary":"List Users","operationId":"list_users_admin_users_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"post":{"tags":["Admin"],"summary":"Create User","operationId":"create_user_admin_users_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/admin/users/{id_user}":{"put":{"tags":["Admin"],"summary":"Update User","operationId":"update_user_admin_users__id_user__put","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_user","in":"path","required":true,"schema":{"type":"integer","title":"Id User"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Admin"],"summary":"Delete User","description":"Suppression douce (actif = 0).","operationId":"delete_user_admin_users__id_user__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_user","in":"path","required":true,"schema":{"type":"integer","title":"Id User"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{id_user}/reset-password":{"post":{"tags":["Admin"],"summary":"Reset Password","operationId":"reset_password_admin_users__id_user__reset_password_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_user","in":"path","required":true,"schema":{"type":"integer","title":"Id User"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordReset"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/journal":{"get":{"tags":["Admin"],"summary":"Get Journal","operationId":"get_journal_admin_journal_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":200,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/me/data-export":{"get":{"tags":["RGPD"],"summary":"Export My Data","description":"Droit à la portabilité (art. 20) : export des données de l'utilisateur.","operationId":"export_my_data_me_data_export_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/me":{"delete":{"tags":["RGPD"],"summary":"Delete My Account","description":"Droit à l'oubli (art. 17) : désactivation + anonymisation.","operationId":"delete_my_account_me_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/categories":{"get":{"tags":["Référentiels"],"summary":"Get Categories","description":"Toutes les catégories de monitoring.","operationId":"get_categories_categories_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/services":{"get":{"tags":["Référentiels"],"summary":"Get Services","description":"Tous les services.","operationId":"get_services_services_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/contacts":{"get":{"tags":["Référentiels"],"summary":"Get Contacts","description":"Contacts, filtrables par service.","operationId":"get_contacts_contacts_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_service","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filtrer par service","title":"Id Service"},"description":"Filtrer par service"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/monitorings":{"get":{"tags":["Monitorings"],"summary":"Get Monitorings","description":"Liste des monitorings actifs avec table_source et bdd_source.","operationId":"get_monitorings_monitorings_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_service","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filtrer par service","title":"Id Service"},"description":"Filtrer par service"},{"name":"id_categorie","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filtrer par catégorie","title":"Id Categorie"},"description":"Filtrer par catégorie"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/monitorings/{id_monito}":{"get":{"tags":["Monitorings"],"summary":"Get Monitoring By Id","description":"Détail d'un monitoring.","operationId":"get_monitoring_by_id_monitorings__id_monito__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"path","required":true,"schema":{"type":"integer","title":"Id Monito"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/monitorings/{id_monito}/details":{"get":{"tags":["Monitorings"],"summary":"Get Monitoring Details","description":"Retourne toutes les lignes en erreur depuis la table MONITO_[NOM] dédiée.\nToutes les colonnes métier sont exposées → utilisable pour export CSV côté frontend.","operationId":"get_monitoring_details_monitorings__id_monito__details_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"path","required":true,"schema":{"type":"integer","title":"Id Monito"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Recherche sur colonnes texte (ct_intitule, agence, STE...)","title":"Search"},"description":"Recherche sur colonnes texte (ct_intitule, agence, STE...)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":5000,"minimum":1,"description":"Lignes max — mettre 5000 pour export CSV complet","default":500,"title":"Limit"},"description":"Lignes max — mettre 5000 pour export CSV complet"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Offset pagination","default":0,"title":"Offset"},"description":"Offset pagination"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/monitorings/{id_monito}/count":{"get":{"tags":["Monitorings"],"summary":"Get Monitoring Count","description":"Nombre d'erreurs dans la table dédiée du monitoring.","operationId":"get_monitoring_count_monitorings__id_monito__count_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"path","required":true,"schema":{"type":"integer","title":"Id Monito"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/monitorings/{id_monito}/columns":{"get":{"tags":["Monitorings"],"summary":"Get Monitoring Columns","description":"Retourne les colonnes de la table dédiée.\nPermet au frontend de générer dynamiquement les en-têtes du tableau.","operationId":"get_monitoring_columns_monitorings__id_monito__columns_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"path","required":true,"schema":{"type":"integer","title":"Id Monito"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard":{"get":{"tags":["Dashboard"],"summary":"Get Dashboard","description":"Vue consolidée — source principale du dashboard.","operationId":"get_dashboard_dashboard_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"service","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filtrer par service","title":"Service"},"description":"Filtrer par service"},{"name":"categorie","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filtrer par catégorie","title":"Categorie"},"description":"Filtrer par catégorie"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/dashboard/summary":{"get":{"tags":["Dashboard"],"summary":"Get Dashboard Summary","description":"KPI globaux pour les 4 cartes du dashboard.","operationId":"get_dashboard_summary_dashboard_summary_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/historique":{"get":{"tags":["Historique"],"summary":"Get Historique","description":"Snapshots journaliers (TABLE_FINAL).","operationId":"get_historique_historique_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Id Monito"}},{"name":"date_debut","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Debut"},"description":"YYYY-MM-DD"},{"name":"date_fin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Fin"},"description":"YYYY-MM-DD"},{"name":"service","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Service"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/historique/{id_monito}/evolution":{"get":{"tags":["Historique"],"summary":"Get Evolution","description":"Évolution d'un monitoring — format optimisé Recharts.","operationId":"get_evolution_historique__id_monito__evolution_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"id_monito","in":"path","required":true,"schema":{"type":"integer","title":"Id Monito"}},{"name":"date_debut","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Debut"}},{"name":"date_fin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Fin"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/historique/comparaison":{"get":{"tags":["Historique"],"summary":"Get Comparaison","description":"Déprécié — utiliser GET /evolution/par-monitoring à la place.\nConservé pour compatibilité ascendante.","operationId":"get_comparaison_historique_comparaison_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"date_debut","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Debut"}},{"name":"date_fin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Fin"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/evolution/global":{"get":{"tags":["Évolution globale"],"summary":"Get Evolution Global","description":"Courbe globale : total des erreurs tous monitorings confondus, par jour.\nSource : VUE_TABLE_FINAL_CONSO WHERE type_agregat = 'GLOBAL'\nFormat optimisé Recharts → [{date, nb_erreurs}]","operationId":"get_evolution_global_evolution_global_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"date_debut","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Debut"},"description":"YYYY-MM-DD"},{"name":"date_fin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Fin"},"description":"YYYY-MM-DD"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/evolution/par-monitoring":{"get":{"tags":["Évolution globale"],"summary":"Get Evolution Par Monitoring","description":"Évolution par monitoring — toutes les courbes individuelles.\nSource : VUE_TABLE_FINAL_CONSO WHERE type_agregat = 'MONITO'\n\nRetourne un format pivot par monitoring :\n{\n \"monitorings\": [{\"id\": 1, \"nom\": \"...\", \"serie\": [{\"date\",\"nb_erreurs\"}]}]\n}\nUtile pour un graphique multi-lignes Recharts (une ligne par monitoring).","operationId":"get_evolution_par_monitoring_evolution_par_monitoring_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"date_debut","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Debut"},"description":"YYYY-MM-DD"},{"name":"date_fin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Fin"},"description":"YYYY-MM-DD"},{"name":"id_monito","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filtrer sur un seul monitoring","title":"Id Monito"},"description":"Filtrer sur un seul monitoring"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/evolution/par-service":{"get":{"tags":["Évolution globale"],"summary":"Get Evolution Par Service","description":"Évolution par service (Contrat / Fournisseur) — 1 courbe par service.\nSource : VUE_TABLE_FINAL_CONSO WHERE type_agregat = 'SERVICE'\n\nRetourne :\n{\n \"services\": [{\"service\": \"Contrat\", \"serie\": [{\"date\",\"nb_erreurs\"}]}]\n}","operationId":"get_evolution_par_service_evolution_par_service_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"date_debut","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Debut"},"description":"YYYY-MM-DD"},{"name":"date_fin","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"YYYY-MM-DD","title":"Date Fin"},"description":"YYYY-MM-DD"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"Body_login_auth_login_post":{"properties":{"grant_type":{"anyOf":[{"type":"string","pattern":"^password$"},{"type":"null"}],"title":"Grant Type"},"username":{"type":"string","title":"Username"},"password":{"type":"string","format":"password","title":"Password"},"scope":{"type":"string","title":"Scope","default":""},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"format":"password","title":"Client Secret"}},"type":"object","required":["username","password"],"title":"Body_login_auth_login_post"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"PasswordReset":{"properties":{"password":{"type":"string","title":"Password"}},"type":"object","required":["password"],"title":"PasswordReset"},"UserCreate":{"properties":{"username":{"type":"string","title":"Username"},"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"role":{"type":"string","title":"Role"}},"type":"object","required":["username","email","password","role"],"title":"UserCreate"},"UserUpdate":{"properties":{"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"role":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role"},"actif":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Actif"}},"type":"object","title":"UserUpdate"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}},"securitySchemes":{"OAuth2PasswordBearer":{"type":"oauth2","flows":{"password":{"scopes":{},"tokenUrl":"auth/login"}}}}}} \ No newline at end of file