Dette passe 1, lot L4 : mesures, alertes et déduplication #4

Open
Thomas wants to merge 6 commits from pr/dette-l4 into pr/dette-l3
Member

Objet

Lot L4 de la passe 1 de dette : fenêtre d'epoch des trames, délais d'alerte calculés sur l'heure de réception du serveur, plafond d'insertions d'alertes, rechargement des règles d'alerte, déduplication des mesures par empreinte stockée et propriétaire configurable des boîtiers provisionnés.

Décisions de l'utilisateur à traiter dans le lot L4-bis

Ces deux points ont été tranchés après ce lot ; ils ne sont pas dans cette PR et seront livrés par le lot L4-bis, empilé après :

  1. Une alerte part même si l'écriture de la mesure échoue. Ce lot n'émet les alertes que sur StoreOutcome::Inserted : une erreur de stockage rend DataError::Store sans alerte (aquaserveur/src/data_handler.rs).
  2. Délai unique de 600 s. Ce lot définit plusieurs délais : LEGIONELLA_COOLDOWN par sonde et hardware_cooldown(list_id) par type de capteur (aquaserveur/src/alert_limits.rs), tous à 600 s.

Dettes traitées et preuves

Dette Correction Preuve
Q8, Q13, recommandation 6 EPOCH_MAX = 2^31-1 (alert_boitiers.datetime reste un INT(11)), fenêtre de 366 jours en arrière et 300 s en avant autour de l'heure serveur, EpochError ; le serveur refuse la trame avant tout stockage aquashared/src/frame.rs (validate_epoch, check_epoch_window), aquaserveur/src/data_handler.rs
Q13, N35, délai légionellose heure de réception Reception prise par le serveur, l'epoch de la trame ne sert plus à aucun délai ; délai par sonde, démarré après une insertion réussie ; table bornée (100 000 entrées) et purgée aquaserveur/src/alert_limits.rs, data_handler.rs
N8 alertes limitées aux N capteurs connus de {id}_boitier_capteurs ; délai matériel par type ; plafond de 10 insertions d'alertes par minute et par boîtier, partagé avec l'alerte 15 des accusés en erreur alert_limits.rs, data_handler.rs, config_ack_handler.rs, database/store.rs
N35 erreur de lecture des seuils non mise en cache ; arrondi celsius_to_mpt par .round() (0.29 °C donnait 28) ; règles chargées au démarrage, rechargées sur changement de configuration et sur firstco, remplacées au lieu d'être ajoutées data_handler.rs, store.rs, alert_manager.rs, config_poller.rs, main.rs
Q16, recommandation 10 DDL unique des tables de mesures : epoch_date BIGINT UNSIGNED NOT NULL, colonne générée stockée mesure_empreinte BINARY(32) (SHA-256 des valeurs, INVISIBLE), UNIQUE KEY uq_mesure_empreinte ; store_frame rend Inserted ou Duplicate (code 1062), pas d'INSERT IGNORE ; alertes seulement sur Inserted aquaserveur/src/database/schema.rs, store.rs, data_handler.rs
N34 (epoch, propriétaire) boitiers.user_id lu dans PROVISIONING_OWNER_USER_ID (défaut 1, valeur invalide : arrêt au démarrage) aquaserveur/src/provisioning/settings.rs, handler.rs, ENV_SETUP.md, .env.example, composes

Clé unique sur epoch_date seule écartée : des mesures différentes partagent leur epoch (5 184 lignes dans le dump).

Tests et résultats

  • TDD : fenêtre d'epoch 27 échecs sur 73 avant implémentation, schema 8 sur 9, déduplication 5 sur 5, alert_limits 18 sur 18 ; contre-épreuves : délai global par boîtier, limite aux capteurs connus neutralisée, user_id en dur, chacune fait échouer son test.
  • Ajouts : bornes et fenêtre d'epoch, alert_limits.rs (13 tests), schema.rs, classify_insert, arrondi, measure_dedup.rs (5), measure_migration.rs, cooldown_bridge_integration.rs réécrit (dont une trame forgée à epoch futur qui ne fait pas taire l'alerte suivante), alert_rules_reload.rs, propriétaire configurable.
  • Modifiés : test_parse_epoch_u32_max devient test_parse_epoch_above_2038_rejected (comportement inversé volontairement) ; tests du délai sur l'epoch retirés et couverts par cooldown_bridge_integration.rs.
  • cargo fmt --check, cargo clippy ... -D warnings : OK.
  • cargo test --workspace : 288 passés, 0 échec, 57 ignorés (L3 : 242 et 45).
  • Tests ignorés sur base jetable (dump, fixtures, migration 001) : 57 passés, 0 échec, deux passages.
  • Vérification réelle avec Mosquitto et serveur jetables : doublon écarté sans seconde alerte, trame à epoch futur refusée sans écriture, rechargement des règles après modification de la fiche, arrêt au démarrage sur propriétaire invalide.

Migrations et actions de déploiement

Migration scripts/db/migrations/001_mesure_empreinte.sql (bloc BEGIN NOT ATOMIC, idempotente), état en lecture seule par 001_mesure_empreinte.check.sql, lancement par scripts/db/migrate-mesure-empreinte.sh <conteneur> [.env] [--check] (mot de passe par MYSQL_PWD, jamais en argument). Pour chaque table %_boitier_datas : epoch_date converti en BIGINT UNSIGNED NOT NULL (arrêt si NULL ou négatif), colonne d'empreinte ajoutée, doublons exacts supprimés (plus petit id gardé), index unique ajouté.

  • Essai sur le dump : 450 tables, 544 550 lignes, 223 doublons exacts supprimés, seconde exécution sans changement.
  • Base de dev : déjà migrée (sauvegarde préalable des tables de mesures, 223 doublons supprimés, seconde exécution sans changement).
  • Production, à faire après validation :
    1. répéter sur une copie du dump de prod avec mariadb:10.6 (prod en 10.6.22, essais faits en 10.11) ;
    2. vérifier qu'aucune application legacy n'écrit dans ces tables sans gérer l'erreur 1062 ;
    3. fenêtre de maintenance, arrêt des écrivains ;
    4. sauvegarde des tables %_boitier_datas hors de l'hôte ;
    5. migrate-mesure-empreinte.sh ... --check, puis migration deux fois, puis --check (compteurs à 0) ;
    6. renseigner PROVISIONING_OWNER_USER_ID si le défaut 1 ne convient pas, déployer, redémarrer.
  • Une table créée par une ancienne version du serveur après la migration n'a pas l'empreinte : relancer la migration (idempotente).

Choix faits

  • Fenêtre 366 jours et 300 s, délai matériel 600 s, plafond 10 alertes par minute et par boîtier, 100 000 entrées par table de délais.
  • Capteurs connus comptés, pas typés : les N premières valeurs sont gardées, ext d'abord.
  • capteur_id renseigné sur les alertes légionellose.
  • Colonne mesure_empreinte INVISIBLE pour ne pas changer les SELECT * des autres applications.
  • Rechargement des règles déclenché par boitiers.updated_at : une modification de alert_boitiers seule est prise en compte au rechargement suivant.
  • Ordre de migration : colonne, puis doublons, puis index (doublons trouvés par GROUP BY sur l'empreinte).

Hors périmètre

Boucle MQTT, accusé après écriture, session persistante, journalisation du payload (L5) ; limites et ACL du broker (L6) ; seuils légionellose par sonde (fetch_legionella_thresholds lit une seule paire, à décider) ; alert_boitiers.datetime en INT(11) (2038) ; documentation générale (L9).

Retour arrière

  • Code : revenir les 6 commits du lot et redéployer l'ancienne image. Les tables migrées restent lisibles et inscriptibles par l'ancien code ; un doublon y produit une erreur d'insertion journalisée.
  • Données : la suppression des doublons n'est pas réversible par script ; retour complet par restauration de la sauvegarde des tables %_boitier_datas prise avant la migration.

Pile de PR

Base de cette PR : pr/dette-l3. Fusion dans l'ordre de la pile ; apres chaque fusion, vigie.py retarget rebase la PR suivante sur main.

  1. #2 pr/dette-l2 : Dette passe 1, lot L2 : contrats partagés dans aquashared et client MQTT du boîtier
  2. #3 pr/dette-l3 : Dette passe 1, lot L3 : provisioning non destructif et identifiant validé à l'entrée MQTT
  3. #4 pr/dette-l4 : Dette passe 1, lot L4 : mesures, alertes et déduplication (cette PR)
  4. #5 pr/dette-l5 : Dette passe 1, lot L5 : boucle MQTT du serveur, livraison de la configuration et journaux
  5. #6 pr/dette-l4bis : Dette passe 1, lot L4-bis : alertes même si l'écriture échoue, délai unique de 600 s, typage des capteurs, ordre des accusés MQTT
  6. #7 pr/dette-l7 : Dette passe 1, lot L7 : builds reproductibles, dépendances, durcissement des conteneurs
  7. #8 pr/dette-l6 : Dette passe 1, lot L6 : authentification Mosquitto, ACL par compte, healthchecks réels, pile e2e isolée
  8. #9 pr/dette-l8 : Dette passe 1, lot L8 : tests non destructifs, fixtures synthétiques, script de tests sur base jetable

Commits du lot

  • 1ea0e37 feat: bound frame epochs to a window around server time
  • 1fb65ca feat: deduplicate measures with a stored fingerprint
  • 2c0622a fix: time alert delays on server reception and bound alert inserts
  • ef90407 fix: reload alert rules on config change and stop caching threshold errors
  • 144f481 feat: make the owner of provisioned boitiers configurable
  • 2886a12 fix: show a current epoch in the server startup hint
## Objet Lot L4 de la passe 1 de dette : fenêtre d'epoch des trames, délais d'alerte calculés sur l'heure de réception du serveur, plafond d'insertions d'alertes, rechargement des règles d'alerte, déduplication des mesures par empreinte stockée et propriétaire configurable des boîtiers provisionnés. ## Décisions de l'utilisateur à traiter dans le lot L4-bis Ces deux points ont été tranchés après ce lot ; ils ne sont pas dans cette PR et seront livrés par le lot L4-bis, empilé après : 1. **Une alerte part même si l'écriture de la mesure échoue.** Ce lot n'émet les alertes que sur `StoreOutcome::Inserted` : une erreur de stockage rend `DataError::Store` sans alerte (`aquaserveur/src/data_handler.rs`). 2. **Délai unique de 600 s.** Ce lot définit plusieurs délais : `LEGIONELLA_COOLDOWN` par sonde et `hardware_cooldown(list_id)` par type de capteur (`aquaserveur/src/alert_limits.rs`), tous à 600 s. ## Dettes traitées et preuves | Dette | Correction | Preuve | |---|---|---| | Q8, Q13, recommandation 6 | `EPOCH_MAX` = 2^31-1 (`alert_boitiers.datetime` reste un `INT(11)`), fenêtre de 366 jours en arrière et 300 s en avant autour de l'heure serveur, `EpochError` ; le serveur refuse la trame avant tout stockage | `aquashared/src/frame.rs` (`validate_epoch`, `check_epoch_window`), `aquaserveur/src/data_handler.rs` | | Q13, N35, délai légionellose | heure de réception `Reception` prise par le serveur, l'epoch de la trame ne sert plus à aucun délai ; délai par sonde, démarré après une insertion réussie ; table bornée (100 000 entrées) et purgée | `aquaserveur/src/alert_limits.rs`, `data_handler.rs` | | N8 | alertes limitées aux N capteurs connus de `{id}_boitier_capteurs` ; délai matériel par type ; plafond de 10 insertions d'alertes par minute et par boîtier, partagé avec l'alerte 15 des accusés en erreur | `alert_limits.rs`, `data_handler.rs`, `config_ack_handler.rs`, `database/store.rs` | | N35 | erreur de lecture des seuils non mise en cache ; arrondi `celsius_to_mpt` par `.round()` (0.29 °C donnait 28) ; règles chargées au démarrage, rechargées sur changement de configuration et sur `firstco`, remplacées au lieu d'être ajoutées | `data_handler.rs`, `store.rs`, `alert_manager.rs`, `config_poller.rs`, `main.rs` | | Q16, recommandation 10 | DDL unique des tables de mesures : `epoch_date BIGINT UNSIGNED NOT NULL`, colonne générée stockée `mesure_empreinte BINARY(32)` (SHA-256 des valeurs, `INVISIBLE`), `UNIQUE KEY uq_mesure_empreinte` ; `store_frame` rend `Inserted` ou `Duplicate` (code 1062), pas d'`INSERT IGNORE` ; alertes seulement sur `Inserted` | `aquaserveur/src/database/schema.rs`, `store.rs`, `data_handler.rs` | | N34 (epoch, propriétaire) | `boitiers.user_id` lu dans `PROVISIONING_OWNER_USER_ID` (défaut 1, valeur invalide : arrêt au démarrage) | `aquaserveur/src/provisioning/settings.rs`, `handler.rs`, `ENV_SETUP.md`, `.env.example`, composes | Clé unique sur `epoch_date` seule écartée : des mesures différentes partagent leur epoch (5 184 lignes dans le dump). ## Tests et résultats - TDD : fenêtre d'epoch 27 échecs sur 73 avant implémentation, `schema` 8 sur 9, déduplication 5 sur 5, `alert_limits` 18 sur 18 ; contre-épreuves : délai global par boîtier, limite aux capteurs connus neutralisée, `user_id` en dur, chacune fait échouer son test. - Ajouts : bornes et fenêtre d'epoch, `alert_limits.rs` (13 tests), `schema.rs`, `classify_insert`, arrondi, `measure_dedup.rs` (5), `measure_migration.rs`, `cooldown_bridge_integration.rs` réécrit (dont une trame forgée à epoch futur qui ne fait pas taire l'alerte suivante), `alert_rules_reload.rs`, propriétaire configurable. - Modifiés : `test_parse_epoch_u32_max` devient `test_parse_epoch_above_2038_rejected` (comportement inversé volontairement) ; tests du délai sur l'epoch retirés et couverts par `cooldown_bridge_integration.rs`. - `cargo fmt --check`, `cargo clippy ... -D warnings` : OK. - `cargo test --workspace` : 288 passés, 0 échec, 57 ignorés (L3 : 242 et 45). - Tests ignorés sur base jetable (dump, fixtures, migration 001) : 57 passés, 0 échec, deux passages. - Vérification réelle avec Mosquitto et serveur jetables : doublon écarté sans seconde alerte, trame à epoch futur refusée sans écriture, rechargement des règles après modification de la fiche, arrêt au démarrage sur propriétaire invalide. ## Migrations et actions de déploiement Migration `scripts/db/migrations/001_mesure_empreinte.sql` (bloc `BEGIN NOT ATOMIC`, idempotente), état en lecture seule par `001_mesure_empreinte.check.sql`, lancement par `scripts/db/migrate-mesure-empreinte.sh <conteneur> [.env] [--check]` (mot de passe par `MYSQL_PWD`, jamais en argument). Pour chaque table `%_boitier_datas` : `epoch_date` converti en `BIGINT UNSIGNED NOT NULL` (arrêt si NULL ou négatif), colonne d'empreinte ajoutée, doublons exacts supprimés (plus petit `id` gardé), index unique ajouté. - Essai sur le dump : 450 tables, 544 550 lignes, 223 doublons exacts supprimés, seconde exécution sans changement. - Base de dev : déjà migrée (sauvegarde préalable des tables de mesures, 223 doublons supprimés, seconde exécution sans changement). - Production, à faire après validation : 1. répéter sur une copie du dump de prod avec `mariadb:10.6` (prod en 10.6.22, essais faits en 10.11) ; 2. vérifier qu'aucune application legacy n'écrit dans ces tables sans gérer l'erreur 1062 ; 3. fenêtre de maintenance, arrêt des écrivains ; 4. sauvegarde des tables `%_boitier_datas` hors de l'hôte ; 5. `migrate-mesure-empreinte.sh ... --check`, puis migration deux fois, puis `--check` (compteurs à 0) ; 6. renseigner `PROVISIONING_OWNER_USER_ID` si le défaut 1 ne convient pas, déployer, redémarrer. - Une table créée par une ancienne version du serveur après la migration n'a pas l'empreinte : relancer la migration (idempotente). ## Choix faits - Fenêtre 366 jours et 300 s, délai matériel 600 s, plafond 10 alertes par minute et par boîtier, 100 000 entrées par table de délais. - Capteurs connus comptés, pas typés : les N premières valeurs sont gardées, `ext` d'abord. - `capteur_id` renseigné sur les alertes légionellose. - Colonne `mesure_empreinte` `INVISIBLE` pour ne pas changer les `SELECT *` des autres applications. - Rechargement des règles déclenché par `boitiers.updated_at` : une modification de `alert_boitiers` seule est prise en compte au rechargement suivant. - Ordre de migration : colonne, puis doublons, puis index (doublons trouvés par `GROUP BY` sur l'empreinte). ## Hors périmètre Boucle MQTT, accusé après écriture, session persistante, journalisation du payload (L5) ; limites et ACL du broker (L6) ; seuils légionellose par sonde (`fetch_legionella_thresholds` lit une seule paire, à décider) ; `alert_boitiers.datetime` en `INT(11)` (2038) ; documentation générale (L9). ## Retour arrière - Code : revenir les 6 commits du lot et redéployer l'ancienne image. Les tables migrées restent lisibles et inscriptibles par l'ancien code ; un doublon y produit une erreur d'insertion journalisée. - Données : la suppression des doublons n'est pas réversible par script ; retour complet par restauration de la sauvegarde des tables `%_boitier_datas` prise avant la migration. --- ### Pile de PR Base de cette PR : `pr/dette-l3`. Fusion dans l'ordre de la pile ; apres chaque fusion, `vigie.py retarget` rebase la PR suivante sur `main`. 1. #2 `pr/dette-l2` : Dette passe 1, lot L2 : contrats partagés dans aquashared et client MQTT du boîtier 2. #3 `pr/dette-l3` : Dette passe 1, lot L3 : provisioning non destructif et identifiant validé à l'entrée MQTT 3. #4 `pr/dette-l4` : Dette passe 1, lot L4 : mesures, alertes et déduplication (cette PR) 4. #5 `pr/dette-l5` : Dette passe 1, lot L5 : boucle MQTT du serveur, livraison de la configuration et journaux 5. #6 `pr/dette-l4bis` : Dette passe 1, lot L4-bis : alertes même si l'écriture échoue, délai unique de 600 s, typage des capteurs, ordre des accusés MQTT 6. #7 `pr/dette-l7` : Dette passe 1, lot L7 : builds reproductibles, dépendances, durcissement des conteneurs 7. #8 `pr/dette-l6` : Dette passe 1, lot L6 : authentification Mosquitto, ACL par compte, healthchecks réels, pile e2e isolée 8. #9 `pr/dette-l8` : Dette passe 1, lot L8 : tests non destructifs, fixtures synthétiques, script de tests sur base jetable ### Commits du lot - `1ea0e37` feat: bound frame epochs to a window around server time - `1fb65ca` feat: deduplicate measures with a stored fingerprint - `2c0622a` fix: time alert delays on server reception and bound alert inserts - `ef90407` fix: reload alert rules on config change and stop caching threshold errors - `144f481` feat: make the owner of provisioned boitiers configurable - `2886a12` fix: show a current epoch in the server startup hint <!-- vigie:stack -->
Absolute upper bound 2^31-1 (alert_boitiers.datetime is a signed INT),
plus check_epoch_window: at most 366 days in the past, so the EPIC-004
offline buffer can still be replayed, and at most 300 s in the future.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UH3JcaVWsmFLC6kyEsVL18
Each {id}_boitier_datas table gets an invisible stored generated column
mesure_empreinte (SHA-256 of epoch_date and every sensor column, with
COALESCE for NULLs) under a unique index. store_frame keeps a plain INSERT
and reports Inserted or Duplicate from the unique violation (1062), so a
QoS 1 resend or an EPIC-004 buffer replay is stored once and alerts only
fire for a new measure. Duplicates are counted in the logs.

Tables created by the server share one DDL (database::schema) with an
unsigned BIGINT epoch, like the legacy tables. The idempotent migration
scripts/db/migrations/001_mesure_empreinte.sql (launcher
scripts/db/migrate-mesure-empreinte.sh, credentials from .env) converts
signed epochs, adds the column, drops exact duplicates keeping the
smallest id, then adds the index. A unique key on epoch_date alone is
rejected: distinct measures share epochs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UH3JcaVWsmFLC6kyEsVL18
Frames are now refused outside the shared epoch window before anything
is stored. Alert delays no longer read the epoch sent by the boitier:
they run on the server reception instant, so a forged future epoch or a
jumping RTC cannot silence the next alert (Q13).

- legionella delay per probe (600 s) instead of per boitier, alerts
  carry the probe position in capteur_id (N35)
- only values of the boitier's known sensors are evaluated; the surplus
  is ignored and logged (N8)
- minimum delay per hardware alert type, sw420 600 s (N8)
- at most 10 alert inserts per boitier and per minute, config ACK
  error alerts included (N8)
- delay tables are purged and capped in memory

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UH3JcaVWsmFLC6kyEsVL18
- a legionella threshold read error is logged and no longer cached as
  the defaults for 5 minutes; the next frame retries (N35)
- thresholds are rounded from DOUBLE(8,2) degrees: 0.29 no longer
  becomes 28 (N35)
- AlertManager rules are loaded for every boitier at startup and
  reloaded, with the thresholds cache dropped, when the config poller
  sees a boitier configuration change or a firstco arrives; a reload
  replaces the rules instead of appending them (N35)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UH3JcaVWsmFLC6kyEsVL18
firstco no longer hardcodes boitiers.user_id = 1. The owner comes from
PROVISIONING_OWNER_USER_ID, default 1 when absent or empty; any value
that is not a positive decimal id stops the server at startup instead
of attaching new boitiers to the wrong account (N34). Documented in
ENV_SETUP.md and .env.example, passed by both compose files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UH3JcaVWsmFLC6kyEsVL18
fix: show a current epoch in the server startup hint
Some checks failed
aquaprocess/revue-statique echec : emoji dans le code ajoute
2886a1291a
The hinted frame carried a 2024 epoch that the new epoch window rejects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UH3JcaVWsmFLC6kyEsVL18
Thomas left a comment

Revue statique Vigie : PR #4

Tete 2886a1291a, base pr/dette-l3 (3c8c48e78b), 6 commit(s), 33 fichier(s) ajoutes ou modifies.

Statut aquaprocess/revue-statique : failure (emoji dans le code ajoute)

Constats : 0 bloquant, 8 majeur, 15 mineur, 2 style. Commentaires en ligne : 2 (mode important, plafond 10).

Verifications automatiques

Verification Resultat Detail
cargo fmt --check success aucune difference
cargo clippy -D warnings success aucun avertissement
cargo test --workspace success 288 passes, 0 echec(s), 57 ignores
tests ignores (base jetable) success 57 passes, 0 echec(s) ; pas de broker MQTT (tests sans TEST_MQTT_URL) ; dump charge en 27 s ; 1 fichier(s) de fixtures ; migrations du depot : 001_mesure_empreinte.sql
cargo audit attention introduite(s) : aucune ; deja presente(s) sur la base : RUSTSEC-2026-0049 (rustls-webpki), RUSTSEC-2026-0098 (rustls-webpki), RUSTSEC-2026-0099 (rustls-webpki), RUSTSEC-2026-0104 (rustls-webpki), RUSTSEC-2026-0258 (h2), RUSTSEC-2026-0285 (rustls), 2 ignoree(s) par audit.toml
semgrep (regles du depot) success 26 fichier(s) ; lignes ajoutees : 3 resultat(s) hors tests, tests : no-unwrap-in-production x73, sqlx-no-format-in-query x4 ; 196 sur lignes inchangees
gitleaks (plage de commits) success 6 commit(s) analyses, aucune fuite
format des commits success 6 commit(s) conformes
emoji dans le code ajoute failure 5 ligne(s) ajoutee(s) avec emoji

Analyse qualitative

Verdict : aucun bloquant. Le lot est solide sur la fenêtre d'epoch, la déduplication par empreinte et la migration idempotente ; 3 constats majeurs (dont un écart connu renvoyé à L4-bis), 13 mineurs et 2 de style, tous liés à des lignes ajoutées par la plage 3c8c48e..2886a12.

Sécurité

  • Injection SQL côté Rust : les noms de table dynamiques passent par BoitierId (numérique canonique) ou validate_boitier_id ; toutes les valeurs sont liées par bind. Seule réserve : datas_table_ddl est publique et accepte une &str non validée (aquaserveur/src/database/schema.rs:47).
  • Migration SQL : les noms de table viennent d'information_schema et sont concaténés entre accents graves sans échappement dans les EXECUTE IMMEDIATE (scripts/db/migrations/001_mesure_empreinte.sql:82). Risque faible (droit CREATE nécessaire), correction triviale.
  • Script shell : identifiants contrôlés par liste blanche, mot de passe transmis par MYSQL_PWD sans valeur dans la ligne de commande, rien d'affiché. Le format de .env est lu de façon fragile (scripts/db/migrate-mesure-empreinte.sh:36).
  • Mémoire bornée : CooldownTable et InsertBudget plafonnés à 100 000 entrées avec purge par minute ; les clés ne viennent que de boîtiers existants et de sondes limitées aux capteurs connus (data_handler.rs:252). Une trame de 2 000 valeurs ne produit pas plus d'alertes que de capteurs connus.
  • Déni de service par trames forgées : un epoch futur ne fait plus taire les alertes (délais sur l'Instant de réception). En revanche, le plafond par minute est partagé avec l'alerte 15 : un flot d'accusés en erreur peut affamer les alertes légionellose (aquaserveur/src/config_ack_handler.rs:127).
  • Aucun secret ni donnée client dans le diff.

Correction

  • Fenêtre d'epoch : bornes incluses, arithmétique saturante, borne absolue 2^31-1 cohérente avec alert_boitiers.datetime en INT(11). Point de vigilance : une horloge RTC en avance de plus de 300 s rend le boîtier muet, mesures et alertes comprises (aquaserveur/src/data_handler.rs:234).
  • Déduplication : INSERT simple et classify_insert, sans INSERT IGNORE. Toute violation d'unicité est toutefois classée en doublon, pas seulement uq_mesure_empreinte (aquaserveur/src/database/store.rs:55). L'empreinte ignore sw420 (aquaserveur/src/data_handler.rs:248).
  • Délai légionellose : démarré seulement après insertion réussie, mais store_temp_alerts n'est pas transactionnel ; un échec partiel laisse des alertes insérées sans délai (aquaserveur/src/database/store.rs:272).
  • Rechargement des règles : replace_rules corrige le cumul au firstco. Le déclenchement dépend du succès de la publication MQTT (aquaserveur/src/config_poller.rs:84), et un échec du chargement initial n'est pas retenté (aquaserveur/src/main.rs:108).
  • Migration : idempotente (chaque étape teste l'état), refus explicite des epochs NULL ou négatifs, suppression gardant le plus petit id. La suppression est irréversible et ne regarde que les colonnes capteur_N_* (001_mesure_empreinte.sql:106) ; le --check ignore les tables sans epoch_date (001_mesure_empreinte.check.sql:46).
  • Aucun unwrap ni expect ajouté dans le code de production.

Cohérence avec le compte rendu

Affirmations vérifiées à la tête 2886a12 : constantes et fonctions de fenêtre dans aquashared/src/frame.rs:55, :62, :69, :77, :92, :105 avec les assertions de compilation ; Reception, fenêtre avant stockage, doublon sans alerte, erreur de seuils non mise en cache dans data_handler.rs ; LEGIONELLA_COOLDOWN, hardware_cooldown, plafond, MAX_TRACKED_ENTRIES, admit_legionella, housekeeping dans alert_limits.rs ; StoreOutcome, classify_insert, fetch_sensor_ids, store_frame, arrondi dans store.rs ; propriétaire (settings.rs, main.rs:51, handler.rs) ; règles (alert_manager.rs:189, :334, :343, config_poller.rs:82). Décomptes de tests conformes (13 dans alert_limits.rs, 6 dans schema.rs, 5 dans measure_dedup.rs, 6 dans cooldown_bridge_integration.rs, 2 dans alert_rules_reload.rs).

Écarts :

  • CooldownTable est cité en alert_limits.rs:93 ; la structure est en :73. Sans conséquence.
  • « L'epoch de la trame ne sert plus à aucun délai » est vrai pour le chemin de production, mais process_data_frame (pipeline.rs:78, non modifiée, utilisée par les tests seulement) passe encore frame.epoch comme horodatage.
  • « Expression identique au caractère près » (schema.rs:32) n'est garanti que par une comparaison de fragments (schema.rs:134).
  • README.md et TESTS.md décrivent encore last_alert_epochs (renvoyé à L9).

Décisions renvoyées à L4-bis

  • Alerte même si l'écriture de la mesure échoue : le lot fait l'inverse (aquaserveur/src/data_handler.rs:239, et :236 pour la lecture des capteurs). Écart connu, validé par l'utilisateur, traité par L4-bis.
  • Délai unique de 600 s : le lot introduit des délais par sonde, par type et celui de l'AlertManager, tous à 600 s mais définis à plusieurs endroits (aquaserveur/src/alert_limits.rs:41, :32). Écart connu, validé, traité par L4-bis.
  • Suggestion : trancher aussi dans L4-bis le cas de l'epoch hors fenêtre (data_handler.rs:234), qui coupe les alertes de la même façon.

Tests

  • Bonne couverture unitaire des fonctions pures (fenêtre, limites, classify_insert avec une fausse erreur de base, limit_to_known_sensors, config_changes, réglages).
  • Les tests sur base utilisent des plages d'epoch disjointes et une horloge injectée, ce qui les rend déterministes ; la contre-épreuve « epoch futur forgé » est pertinente.
  • Faiblesses : migration non testée sur le SIGNAL ni sur plus de 9 capteurs, et exécutée sur toute la base de test (aquaserveur/tests/measure_migration.rs:58) ; startup_loads_the_rules_of_every_boitier n'affirme que « au moins un boîtier » ; aucun test de l'échec partiel de store_temp_alerts ni de la famine des alertes légionellose par le plafond partagé.

Questions ouvertes

  • Les tables %_boitier_datas héritées portent-elles d'autres colonnes que id, epoch_date et capteur_N_* ? Si oui, la suppression des doublons est trop large.
  • Compatibilité MariaDB 10.6 de BEGIN NOT ATOMIC, EXECUTE IMMEDIATE et des colonnes INVISIBLE : hypothèse du compte rendu, à confirmer par la répétition prévue sur mariadb:10.6.
  • Les boîtiers synchronisent-ils leur RTC (NTP ou équivalent) ? La marge de 300 s en dépend.
  • Les alertes calculées en mémoire par l'AlertManager ne sont que journalisées : est-ce voulu ?

Autres constats (non publies en ligne)

Gravite Emplacement Source Constat
majeur aquaserveur/src/data_handler.rs:234 revue Une trame hors fenêtre (InFuture au-delà de 300 s, TooOld au-delà de 366 jours) est refusée sans mesure ni alerte, avec un seul message d'erreur. Un boîtier dont l'horloge RTC avance de plus de 5 minutes devient donc muet, y compris pour une sonde à 120 °C. Piste : évaluer quand même les alertes sur l'heure de réception (rx.unix) quand l'epoch est hors fenêtre, ou insérer une alerte dédiée « horloge désynchronisée » soumise au plafond, et compter ces refus ; à rapprocher de la décision L4-bis sur les alertes indépendantes du stockage. (non publie en ligne : Forgejo l'ancrerait a la ligne 197 du commit 2c0622a, decalee dans la vue de la PR)
majeur aquaserveur/src/data_handler.rs:239 revue Écart connu, validé par l'utilisateur, traité par L4-bis : un échec de fetch_sensor_ids (ligne 236) ou de insert_frame rend DataError::Store avant toute évaluation d'alerte. En 3c8c48e, store_frame en erreur était journalisé et les alertes matériel et légionellose partaient quand même : c'est donc une régression du point de vue de l'alerte sanitaire. L4-bis doit faire partir les alertes même si l'écriture de la mesure échoue (seul Duplicate doit les court-circuiter). (non publie en ligne : Forgejo l'ancrerait a la ligne 136 du commit 1fb65ca, decalee dans la vue de la PR)
majeur aquaserveur/src/main.rs:107 emoji Caractere emoji U+2705 dans une ligne ajoutee (regle IA-23). (non publie en ligne : Forgejo l'ancrerait a la ligne 94 du commit ef90407, decalee dans la vue de la PR)
majeur aquaserveur/src/main.rs:108 emoji Caractere emoji U+274C dans une ligne ajoutee (regle IA-23). (non publie en ligne : Forgejo l'ancrerait a la ligne 95 du commit ef90407, decalee dans la vue de la PR)
majeur aquaserveur/src/main.rs:152 emoji Caractere emoji U+274C dans une ligne ajoutee (regle IA-23). (non publie en ligne : Forgejo l'ancrerait a la ligne 139 du commit ef90407, decalee dans la vue de la PR)
majeur aquaserveur/src/main.rs:268 emoji Caractere emoji U+274C dans une ligne ajoutee (regle IA-23). (non publie en ligne : Forgejo l'ancrerait a la ligne 255 du commit ef90407, decalee dans la vue de la PR)
mineur aquaserveur/src/alert_limits.rs:41 revue Écart connu, validé par l'utilisateur, traité par L4-bis : le lot introduit plusieurs délais (par sonde avec LEGIONELLA_COOLDOWN ligne 32, par type avec hardware_cooldown dont les deux bras du match sont identiques, AlertManager::new(600) dans main.rs) alors que la décision retenue est un délai unique de 600 s. Valeurs aujourd'hui toutes à 600 s, mais trois sources de vérité ; L4-bis doit les ramener à une constante unique.
mineur aquaserveur/src/alert_limits.rs:281 semgrep semgrep no-unwrap-in-production (WARNING) : .unwrap() ou .expect() peut paniquer en production. 1 occurrence(s) sur lignes ajoutees (281), y compris d'eventuels modules de test internes.
mineur aquaserveur/src/config_poller.rs:84 revue config_changes dépend de last_push, qui n'est mis à jour qu'après une publication MQTT réussie. Si la première publication d'un boîtier a échoué (absent de last_push), une modification ultérieure de sa configuration ne déclenche aucun rechargement des règles ; à l'inverse, une publication qui échoue en boucle provoque un rechargement à chaque passage. Piste : suivre les versions vues dans une table distincte de celle des versions publiées.
mineur aquaserveur/src/data_handler.rs:248 revue L'empreinte couvre l'epoch et les colonnes capteurs, pas le drapeau sw420 qui n'est pas stocké dans la table de mesures. Deux trames de même epoch et mêmes valeurs qui ne diffèrent que par sw420 donneraient un Duplicate et l'alerte vibration de la seconde serait perdue. Cas probablement rare (même seconde), à documenter ou à couvrir en laissant passer l'alerte matériel sur un doublon dont sw420 vaut 1.
mineur aquaserveur/src/data_handler.rs:306 semgrep semgrep no-unwrap-in-production (WARNING) : .unwrap() ou .expect() peut paniquer en production. 2 occurrence(s) sur lignes ajoutees (306, 315), y compris d'eventuels modules de test internes.
mineur aquaserveur/src/database/schema.rs:47 revue datas_table_ddl est publique et interpole boitier: &str dans un nom de table sans validation ; la sûreté repose sur la documentation de l'appelant. Piste : prendre un BoitierId, ou appeler validate_boitier_id dans la fonction (les tests qui utilisent l4null et l4ddl restent compatibles avec la liste blanche).
mineur aquaserveur/src/database/schema.rs:134 revue expr_matches_the_migration_script ne vérifie que des fragments de chaîne ; il ne prouve pas l'affirmation de la ligne 32 (« identique, au caractère près ») ni l'ordre numérique capteur_10 après capteur_2. Piste : test sur base comparant GENERATION_EXPRESSION (information_schema.COLUMNS) d'une table migrée et d'une table créée par datas_table_ddl, avec au moins 10 capteurs.
mineur aquaserveur/src/database/store.rs:55 revue classify_insert traite toute violation d'unicité (1062) comme un doublon, quelle que soit la clé. Sur une table existante (legacy) qui porterait une autre clé unique, une mesure réellement nouvelle serait écartée comme Duplicate, sans alerte et sans erreur. Piste : ne classer en Duplicate que si le message de l'erreur cite uq_mesure_empreinte (MESURE_EMPREINTE_INDEX), sinon InsertFailed.
mineur aquaserveur/src/database/store.rs:272 revue store_temp_alerts insère une ligne par sonde hors transaction et sort au premier échec par ? : les sondes déjà insérées restent en base, mais data_handler.rs n'appelle legionella_inserted que sur Ok, donc leur délai ne démarre pas et elles sont réinsérées à la trame suivante (doublons dans alert_boitiers, bornés par le plafond). Piste : transaction comme store_hw_alerts, ou retour de la liste des sondes effectivement insérées.
mineur aquaserveur/src/main.rs:108 revue Un échec de load_all_alert_rules au démarrage est seulement journalisé : les règles en mémoire restent vides jusqu'à une modification de fiche ou un firstco de chaque boîtier, sans nouvelle tentative. Piste : réessayer au premier passage du poller, ou arrêter le serveur comme pour PROVISIONING_OWNER_USER_ID.
mineur aquaserveur/tests/measure_migration.rs:58 revue Le test automatisé ne couvre ni l'arrêt par SIGNAL sur un epoch NULL ou négatif, ni l'ordre des colonnes au-delà de 9 capteurs ; le compte rendu indique que ces cas n'ont été vérifiés qu'à la main sur tables synthétiques. Le test exécute aussi la migration sur toutes les tables %_boitier_datas de la base de test (doublons des fixtures supprimés). Piste : ajouter une table à 10 capteurs et une table à epoch négatif dont on attend l'erreur.
mineur scripts/db/migrate-mesure-empreinte.sh:36 revue env_value prend la valeur brute après le premier = : guillemets, retour chariot (fichier CRLF) ou préfixe export ne sont pas gérés, si bien qu'un DB_PASSWORD="..." transmettrait les guillemets. Sans risque de fuite, mais source d'échec d'authentification en production. Piste : retirer \r et les guillemets englobants, ou documenter le format attendu.
mineur scripts/db/migrations/001_mesure_empreinte.check.sql:46 revue Le contrôle ne signale pas une table sans colonne epoch_date : la somme sur zéro ligne vaut 0 et la table n'est comptée nulle part, alors que la migration s'arrête dessus par SIGNAL (001_mesure_empreinte.sql:79). Le --check préalable peut donc annoncer un état sain et la migration échouer en cours de route. Piste : compter aussi les tables sans epoch_date.
mineur scripts/db/migrations/001_mesure_empreinte.sql:82 revue Le nom de table t, lu dans information_schema, est concaténé entre accents graves sans échappement dans quatre EXECUTE IMMEDIATE (lignes 82, 89, 109, 116). Une table dont le nom contiendrait un accent grave permettrait d'injecter du SQL exécuté avec les droits ALTER et DELETE du compte de migration. Exploitation peu probable (droit CREATE nécessaire), mais défense simple : refuser par SIGNAL tout nom qui ne respecte pas ^[0-9A-Za-z_]+$, ou doubler les accents graves avec REPLACE.
mineur scripts/db/migrations/001_mesure_empreinte.sql:106 revue L'empreinte n'inclut que epoch_date et les colonnes capteur_N_id/data ; toute autre colonne d'une table existante est ignorée. Si une table legacy porte d'autres colonnes (horodatage, moy, etc.), l'étape 3 supprime définitivement des lignes qui ne diffèrent que par ces colonnes. Piste : faire SIGNAL (ou au moins lister) les tables qui ont des colonnes hors id, epoch_date, capteur_N_*, mesure_empreinte avant toute suppression.
style aquaserveur/src/alert_limits.rs:22 revue La documentation du module annonce que « l'entrée la plus ancienne est retirée », alors que CooldownTable::start retire l'échéance la plus proche (min_by_key sur until, ligne 100). Équivalent tant que la période est la même pour toutes les clés ; aligner la phrase sur le code (comme le fait déjà le compte rendu).
style aquaserveur/src/main.rs:52 revue Les nouveaux messages ajoutés dans main.rs (lignes 52, 107, 108, 152 et 268) contiennent des émojis et un tiret cadratin, contraires à la règle IA-23 du projet. Le fichier en contient déjà ; ne pas en ajouter de nouveaux.

Limites

Revue publiee en COMMENT : l'auteur des PR et le relecteur sont le meme compte, Forgejo refuse APPROVE et REQUEST_CHANGES. Le blocage s'exprime par le statut de commit aquaprocess/revue-statique. Analyse statique et tests automatises seulement, sans fusion ni deploiement.

## Revue statique Vigie : PR #4 Tete `2886a1291a`, base `pr/dette-l3` (`3c8c48e78b`), 6 commit(s), 33 fichier(s) ajoutes ou modifies. **Statut `aquaprocess/revue-statique` : failure** (emoji dans le code ajoute) Constats : 0 bloquant, 8 majeur, 15 mineur, 2 style. Commentaires en ligne : 2 (mode `important`, plafond 10). ### Verifications automatiques | Verification | Resultat | Detail | |---|---|---| | cargo fmt --check | success | aucune difference | | cargo clippy -D warnings | success | aucun avertissement | | cargo test --workspace | success | 288 passes, 0 echec(s), 57 ignores | | tests ignores (base jetable) | success | 57 passes, 0 echec(s) ; pas de broker MQTT (tests sans TEST_MQTT_URL) ; dump charge en 27 s ; 1 fichier(s) de fixtures ; migrations du depot : 001_mesure_empreinte.sql | | cargo audit | attention | introduite(s) : aucune ; deja presente(s) sur la base : RUSTSEC-2026-0049 (rustls-webpki), RUSTSEC-2026-0098 (rustls-webpki), RUSTSEC-2026-0099 (rustls-webpki), RUSTSEC-2026-0104 (rustls-webpki), RUSTSEC-2026-0258 (h2), RUSTSEC-2026-0285 (rustls), 2 ignoree(s) par audit.toml | | semgrep (regles du depot) | success | 26 fichier(s) ; lignes ajoutees : 3 resultat(s) hors tests, tests : no-unwrap-in-production x73, sqlx-no-format-in-query x4 ; 196 sur lignes inchangees | | gitleaks (plage de commits) | success | 6 commit(s) analyses, aucune fuite | | format des commits | success | 6 commit(s) conformes | | emoji dans le code ajoute | failure | 5 ligne(s) ajoutee(s) avec emoji | ### Analyse qualitative Verdict : aucun bloquant. Le lot est solide sur la fenêtre d'epoch, la déduplication par empreinte et la migration idempotente ; 3 constats majeurs (dont un écart connu renvoyé à L4-bis), 13 mineurs et 2 de style, tous liés à des lignes ajoutées par la plage 3c8c48e..2886a12. ### Sécurité - Injection SQL côté Rust : les noms de table dynamiques passent par `BoitierId` (numérique canonique) ou `validate_boitier_id` ; toutes les valeurs sont liées par `bind`. Seule réserve : `datas_table_ddl` est publique et accepte une `&str` non validée (`aquaserveur/src/database/schema.rs:47`). - Migration SQL : les noms de table viennent d'`information_schema` et sont concaténés entre accents graves sans échappement dans les `EXECUTE IMMEDIATE` (`scripts/db/migrations/001_mesure_empreinte.sql:82`). Risque faible (droit CREATE nécessaire), correction triviale. - Script shell : identifiants contrôlés par liste blanche, mot de passe transmis par `MYSQL_PWD` sans valeur dans la ligne de commande, rien d'affiché. Le format de `.env` est lu de façon fragile (`scripts/db/migrate-mesure-empreinte.sh:36`). - Mémoire bornée : `CooldownTable` et `InsertBudget` plafonnés à 100 000 entrées avec purge par minute ; les clés ne viennent que de boîtiers existants et de sondes limitées aux capteurs connus (`data_handler.rs:252`). Une trame de 2 000 valeurs ne produit pas plus d'alertes que de capteurs connus. - Déni de service par trames forgées : un epoch futur ne fait plus taire les alertes (délais sur l'`Instant` de réception). En revanche, le plafond par minute est partagé avec l'alerte 15 : un flot d'accusés en erreur peut affamer les alertes légionellose (`aquaserveur/src/config_ack_handler.rs:127`). - Aucun secret ni donnée client dans le diff. ### Correction - Fenêtre d'epoch : bornes incluses, arithmétique saturante, borne absolue 2^31-1 cohérente avec `alert_boitiers.datetime` en `INT(11)`. Point de vigilance : une horloge RTC en avance de plus de 300 s rend le boîtier muet, mesures et alertes comprises (`aquaserveur/src/data_handler.rs:234`). - Déduplication : `INSERT` simple et `classify_insert`, sans `INSERT IGNORE`. Toute violation d'unicité est toutefois classée en doublon, pas seulement `uq_mesure_empreinte` (`aquaserveur/src/database/store.rs:55`). L'empreinte ignore `sw420` (`aquaserveur/src/data_handler.rs:248`). - Délai légionellose : démarré seulement après insertion réussie, mais `store_temp_alerts` n'est pas transactionnel ; un échec partiel laisse des alertes insérées sans délai (`aquaserveur/src/database/store.rs:272`). - Rechargement des règles : `replace_rules` corrige le cumul au `firstco`. Le déclenchement dépend du succès de la publication MQTT (`aquaserveur/src/config_poller.rs:84`), et un échec du chargement initial n'est pas retenté (`aquaserveur/src/main.rs:108`). - Migration : idempotente (chaque étape teste l'état), refus explicite des epochs NULL ou négatifs, suppression gardant le plus petit `id`. La suppression est irréversible et ne regarde que les colonnes `capteur_N_*` (`001_mesure_empreinte.sql:106`) ; le `--check` ignore les tables sans `epoch_date` (`001_mesure_empreinte.check.sql:46`). - Aucun `unwrap` ni `expect` ajouté dans le code de production. ### Cohérence avec le compte rendu Affirmations vérifiées à la tête `2886a12` : constantes et fonctions de fenêtre dans `aquashared/src/frame.rs:55`, `:62`, `:69`, `:77`, `:92`, `:105` avec les assertions de compilation ; `Reception`, fenêtre avant stockage, doublon sans alerte, erreur de seuils non mise en cache dans `data_handler.rs` ; `LEGIONELLA_COOLDOWN`, `hardware_cooldown`, plafond, `MAX_TRACKED_ENTRIES`, `admit_legionella`, `housekeeping` dans `alert_limits.rs` ; `StoreOutcome`, `classify_insert`, `fetch_sensor_ids`, `store_frame`, arrondi dans `store.rs` ; propriétaire (`settings.rs`, `main.rs:51`, `handler.rs`) ; règles (`alert_manager.rs:189`, `:334`, `:343`, `config_poller.rs:82`). Décomptes de tests conformes (13 dans `alert_limits.rs`, 6 dans `schema.rs`, 5 dans `measure_dedup.rs`, 6 dans `cooldown_bridge_integration.rs`, 2 dans `alert_rules_reload.rs`). Écarts : - `CooldownTable` est cité en `alert_limits.rs:93` ; la structure est en `:73`. Sans conséquence. - « L'epoch de la trame ne sert plus à aucun délai » est vrai pour le chemin de production, mais `process_data_frame` (`pipeline.rs:78`, non modifiée, utilisée par les tests seulement) passe encore `frame.epoch` comme horodatage. - « Expression identique au caractère près » (`schema.rs:32`) n'est garanti que par une comparaison de fragments (`schema.rs:134`). - `README.md` et `TESTS.md` décrivent encore `last_alert_epochs` (renvoyé à L9). ### Décisions renvoyées à L4-bis - Alerte même si l'écriture de la mesure échoue : le lot fait l'inverse (`aquaserveur/src/data_handler.rs:239`, et `:236` pour la lecture des capteurs). Écart connu, validé par l'utilisateur, traité par L4-bis. - Délai unique de 600 s : le lot introduit des délais par sonde, par type et celui de l'`AlertManager`, tous à 600 s mais définis à plusieurs endroits (`aquaserveur/src/alert_limits.rs:41`, `:32`). Écart connu, validé, traité par L4-bis. - Suggestion : trancher aussi dans L4-bis le cas de l'epoch hors fenêtre (`data_handler.rs:234`), qui coupe les alertes de la même façon. ### Tests - Bonne couverture unitaire des fonctions pures (fenêtre, limites, `classify_insert` avec une fausse erreur de base, `limit_to_known_sensors`, `config_changes`, réglages). - Les tests sur base utilisent des plages d'epoch disjointes et une horloge injectée, ce qui les rend déterministes ; la contre-épreuve « epoch futur forgé » est pertinente. - Faiblesses : migration non testée sur le `SIGNAL` ni sur plus de 9 capteurs, et exécutée sur toute la base de test (`aquaserveur/tests/measure_migration.rs:58`) ; `startup_loads_the_rules_of_every_boitier` n'affirme que « au moins un boîtier » ; aucun test de l'échec partiel de `store_temp_alerts` ni de la famine des alertes légionellose par le plafond partagé. ### Questions ouvertes - Les tables `%_boitier_datas` héritées portent-elles d'autres colonnes que `id`, `epoch_date` et `capteur_N_*` ? Si oui, la suppression des doublons est trop large. - Compatibilité MariaDB 10.6 de `BEGIN NOT ATOMIC`, `EXECUTE IMMEDIATE` et des colonnes `INVISIBLE` : hypothèse du compte rendu, à confirmer par la répétition prévue sur `mariadb:10.6`. - Les boîtiers synchronisent-ils leur RTC (NTP ou équivalent) ? La marge de 300 s en dépend. - Les alertes calculées en mémoire par l'`AlertManager` ne sont que journalisées : est-ce voulu ? ### Autres constats (non publies en ligne) | Gravite | Emplacement | Source | Constat | |---|---|---|---| | majeur | `aquaserveur/src/data_handler.rs:234` | revue | Une trame hors fenêtre (`InFuture` au-delà de 300 s, `TooOld` au-delà de 366 jours) est refusée sans mesure ni alerte, avec un seul message d'erreur. Un boîtier dont l'horloge RTC avance de plus de 5 minutes devient donc muet, y compris pour une sonde à 120 °C. Piste : évaluer quand même les alertes sur l'heure de réception (`rx.unix`) quand l'epoch est hors fenêtre, ou insérer une alerte dédiée « horloge désynchronisée » soumise au plafond, et compter ces refus ; à rapprocher de la décision L4-bis sur les alertes indépendantes du stockage. (non publie en ligne : Forgejo l'ancrerait a la ligne 197 du commit 2c0622a, decalee dans la vue de la PR) | | majeur | `aquaserveur/src/data_handler.rs:239` | revue | Écart connu, validé par l'utilisateur, traité par L4-bis : un échec de `fetch_sensor_ids` (ligne 236) ou de `insert_frame` rend `DataError::Store` avant toute évaluation d'alerte. En 3c8c48e, `store_frame` en erreur était journalisé et les alertes matériel et légionellose partaient quand même : c'est donc une régression du point de vue de l'alerte sanitaire. L4-bis doit faire partir les alertes même si l'écriture de la mesure échoue (seul `Duplicate` doit les court-circuiter). (non publie en ligne : Forgejo l'ancrerait a la ligne 136 du commit 1fb65ca, decalee dans la vue de la PR) | | majeur | `aquaserveur/src/main.rs:107` | emoji | Caractere emoji U+2705 dans une ligne ajoutee (regle IA-23). (non publie en ligne : Forgejo l'ancrerait a la ligne 94 du commit ef90407, decalee dans la vue de la PR) | | majeur | `aquaserveur/src/main.rs:108` | emoji | Caractere emoji U+274C dans une ligne ajoutee (regle IA-23). (non publie en ligne : Forgejo l'ancrerait a la ligne 95 du commit ef90407, decalee dans la vue de la PR) | | majeur | `aquaserveur/src/main.rs:152` | emoji | Caractere emoji U+274C dans une ligne ajoutee (regle IA-23). (non publie en ligne : Forgejo l'ancrerait a la ligne 139 du commit ef90407, decalee dans la vue de la PR) | | majeur | `aquaserveur/src/main.rs:268` | emoji | Caractere emoji U+274C dans une ligne ajoutee (regle IA-23). (non publie en ligne : Forgejo l'ancrerait a la ligne 255 du commit ef90407, decalee dans la vue de la PR) | | mineur | `aquaserveur/src/alert_limits.rs:41` | revue | Écart connu, validé par l'utilisateur, traité par L4-bis : le lot introduit plusieurs délais (par sonde avec `LEGIONELLA_COOLDOWN` ligne 32, par type avec `hardware_cooldown` dont les deux bras du `match` sont identiques, `AlertManager::new(600)` dans main.rs) alors que la décision retenue est un délai unique de 600 s. Valeurs aujourd'hui toutes à 600 s, mais trois sources de vérité ; L4-bis doit les ramener à une constante unique. | | mineur | `aquaserveur/src/alert_limits.rs:281` | semgrep | semgrep `no-unwrap-in-production` (WARNING) : `.unwrap()` ou `.expect()` peut paniquer en production. 1 occurrence(s) sur lignes ajoutees (281), y compris d'eventuels modules de test internes. | | mineur | `aquaserveur/src/config_poller.rs:84` | revue | `config_changes` dépend de `last_push`, qui n'est mis à jour qu'après une publication MQTT réussie. Si la première publication d'un boîtier a échoué (absent de `last_push`), une modification ultérieure de sa configuration ne déclenche aucun rechargement des règles ; à l'inverse, une publication qui échoue en boucle provoque un rechargement à chaque passage. Piste : suivre les versions vues dans une table distincte de celle des versions publiées. | | mineur | `aquaserveur/src/data_handler.rs:248` | revue | L'empreinte couvre l'epoch et les colonnes capteurs, pas le drapeau `sw420` qui n'est pas stocké dans la table de mesures. Deux trames de même epoch et mêmes valeurs qui ne diffèrent que par `sw420` donneraient un `Duplicate` et l'alerte vibration de la seconde serait perdue. Cas probablement rare (même seconde), à documenter ou à couvrir en laissant passer l'alerte matériel sur un doublon dont `sw420` vaut 1. | | mineur | `aquaserveur/src/data_handler.rs:306` | semgrep | semgrep `no-unwrap-in-production` (WARNING) : `.unwrap()` ou `.expect()` peut paniquer en production. 2 occurrence(s) sur lignes ajoutees (306, 315), y compris d'eventuels modules de test internes. | | mineur | `aquaserveur/src/database/schema.rs:47` | revue | `datas_table_ddl` est publique et interpole `boitier: &str` dans un nom de table sans validation ; la sûreté repose sur la documentation de l'appelant. Piste : prendre un `BoitierId`, ou appeler `validate_boitier_id` dans la fonction (les tests qui utilisent `l4null` et `l4ddl` restent compatibles avec la liste blanche). | | mineur | `aquaserveur/src/database/schema.rs:134` | revue | `expr_matches_the_migration_script` ne vérifie que des fragments de chaîne ; il ne prouve pas l'affirmation de la ligne 32 (« identique, au caractère près ») ni l'ordre numérique `capteur_10` après `capteur_2`. Piste : test sur base comparant `GENERATION_EXPRESSION` (information_schema.COLUMNS) d'une table migrée et d'une table créée par `datas_table_ddl`, avec au moins 10 capteurs. | | mineur | `aquaserveur/src/database/store.rs:55` | revue | `classify_insert` traite toute violation d'unicité (1062) comme un doublon, quelle que soit la clé. Sur une table existante (legacy) qui porterait une autre clé unique, une mesure réellement nouvelle serait écartée comme `Duplicate`, sans alerte et sans erreur. Piste : ne classer en `Duplicate` que si le message de l'erreur cite `uq_mesure_empreinte` (MESURE_EMPREINTE_INDEX), sinon `InsertFailed`. | | mineur | `aquaserveur/src/database/store.rs:272` | revue | `store_temp_alerts` insère une ligne par sonde hors transaction et sort au premier échec par `?` : les sondes déjà insérées restent en base, mais data_handler.rs n'appelle `legionella_inserted` que sur `Ok`, donc leur délai ne démarre pas et elles sont réinsérées à la trame suivante (doublons dans `alert_boitiers`, bornés par le plafond). Piste : transaction comme `store_hw_alerts`, ou retour de la liste des sondes effectivement insérées. | | mineur | `aquaserveur/src/main.rs:108` | revue | Un échec de `load_all_alert_rules` au démarrage est seulement journalisé : les règles en mémoire restent vides jusqu'à une modification de fiche ou un `firstco` de chaque boîtier, sans nouvelle tentative. Piste : réessayer au premier passage du poller, ou arrêter le serveur comme pour `PROVISIONING_OWNER_USER_ID`. | | mineur | `aquaserveur/tests/measure_migration.rs:58` | revue | Le test automatisé ne couvre ni l'arrêt par `SIGNAL` sur un epoch NULL ou négatif, ni l'ordre des colonnes au-delà de 9 capteurs ; le compte rendu indique que ces cas n'ont été vérifiés qu'à la main sur tables synthétiques. Le test exécute aussi la migration sur toutes les tables `%_boitier_datas` de la base de test (doublons des fixtures supprimés). Piste : ajouter une table à 10 capteurs et une table à epoch négatif dont on attend l'erreur. | | mineur | `scripts/db/migrate-mesure-empreinte.sh:36` | revue | `env_value` prend la valeur brute après le premier `=` : guillemets, retour chariot (fichier CRLF) ou préfixe `export` ne sont pas gérés, si bien qu'un `DB_PASSWORD="..."` transmettrait les guillemets. Sans risque de fuite, mais source d'échec d'authentification en production. Piste : retirer `\r` et les guillemets englobants, ou documenter le format attendu. | | mineur | `scripts/db/migrations/001_mesure_empreinte.check.sql:46` | revue | Le contrôle ne signale pas une table sans colonne `epoch_date` : la somme sur zéro ligne vaut 0 et la table n'est comptée nulle part, alors que la migration s'arrête dessus par `SIGNAL` (001_mesure_empreinte.sql:79). Le `--check` préalable peut donc annoncer un état sain et la migration échouer en cours de route. Piste : compter aussi les tables sans `epoch_date`. | | mineur | `scripts/db/migrations/001_mesure_empreinte.sql:82` | revue | Le nom de table `t`, lu dans information_schema, est concaténé entre accents graves sans échappement dans quatre `EXECUTE IMMEDIATE` (lignes 82, 89, 109, 116). Une table dont le nom contiendrait un accent grave permettrait d'injecter du SQL exécuté avec les droits ALTER et DELETE du compte de migration. Exploitation peu probable (droit CREATE nécessaire), mais défense simple : refuser par `SIGNAL` tout nom qui ne respecte pas `^[0-9A-Za-z_]+$`, ou doubler les accents graves avec `REPLACE`. | | mineur | `scripts/db/migrations/001_mesure_empreinte.sql:106` | revue | L'empreinte n'inclut que `epoch_date` et les colonnes `capteur_N_id/data` ; toute autre colonne d'une table existante est ignorée. Si une table legacy porte d'autres colonnes (horodatage, `moy`, etc.), l'étape 3 supprime définitivement des lignes qui ne diffèrent que par ces colonnes. Piste : faire `SIGNAL` (ou au moins lister) les tables qui ont des colonnes hors `id`, `epoch_date`, `capteur_N_*`, `mesure_empreinte` avant toute suppression. | | style | `aquaserveur/src/alert_limits.rs:22` | revue | La documentation du module annonce que « l'entrée la plus ancienne est retirée », alors que `CooldownTable::start` retire l'échéance la plus proche (`min_by_key` sur `until`, ligne 100). Équivalent tant que la période est la même pour toutes les clés ; aligner la phrase sur le code (comme le fait déjà le compte rendu). | | style | `aquaserveur/src/main.rs:52` | revue | Les nouveaux messages ajoutés dans main.rs (lignes 52, 107, 108, 152 et 268) contiennent des émojis et un tiret cadratin, contraires à la règle IA-23 du projet. Le fichier en contient déjà ; ne pas en ajouter de nouveaux. | ### Limites Revue publiee en `COMMENT` : l'auteur des PR et le relecteur sont le meme compte, Forgejo refuse APPROVE et REQUEST_CHANGES. Le blocage s'exprime par le statut de commit `aquaprocess/revue-statique`. Analyse statique et tests automatises seulement, sans fusion ni deploiement. <!-- vigie:review -->
@ -120,6 +124,14 @@ pub async fn handle_config_ack(
// pour que l'opérateur soit notifié via le panneau d'alertes.
let value = ack.error_detail.as_deref().unwrap_or("unknown");
if !limits.admit_insert(boitier_id, Instant::now()) {
Author
Member

[majeur] Le plafond de 10 insertions par minute est partagé entre l'alerte 15 (accusé maj_boitier_ACK en erreur), les alertes matériel et les alertes légionellose. Un flot d'accusés en erreur publiés sur le topic du boîtier consomme tout le budget de la minute, et admit_legionella (alert_limits.rs:221) refuse alors les alertes sanitaires ; comme leur délai ne démarre pas, elles sont retentées puis refusées à chaque trame tant que le flot dure. Piste : budget séparé par famille, ou part réservée (priorité) aux alertes légionellose.

Source : revue.

**[majeur]** Le plafond de 10 insertions par minute est partagé entre l'alerte 15 (accusé `maj_boitier_ACK` en erreur), les alertes matériel et les alertes légionellose. Un flot d'accusés en erreur publiés sur le topic du boîtier consomme tout le budget de la minute, et `admit_legionella` (alert_limits.rs:221) refuse alors les alertes sanitaires ; comme leur délai ne démarre pas, elles sont retentées puis refusées à chaque trame tant que le flot dure. Piste : budget séparé par famille, ou part réservée (priorité) aux alertes légionellose. _Source : revue._
@ -47,0 +49,4 @@
// Propriétaire des boîtiers provisionnés par firstco (N34) : refus de
// démarrer sur une valeur invalide plutôt qu'un rattachement silencieux.
let provisioning = ProvisioningSettings::from_env().unwrap_or_else(|e| {
eprintln!("❌ {} — arrêt.", e);
Author
Member

[majeur] Caractere emoji U+274C dans une ligne ajoutee (regle IA-23).

Source : emoji.

**[majeur]** Caractere emoji U+274C dans une ligne ajoutee (regle IA-23). _Source : emoji._
Some checks failed
aquaprocess/revue-statique echec : emoji dans le code ajoute
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin pr/dette-l4:pr/dette-l4
git switch pr/dette-l4

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch pr/dette-l3
git merge --no-ff pr/dette-l4
git switch pr/dette-l4
git rebase pr/dette-l3
git switch pr/dette-l3
git merge --ff-only pr/dette-l4
git switch pr/dette-l4
git rebase pr/dette-l3
git switch pr/dette-l3
git merge --no-ff pr/dette-l4
git switch pr/dette-l3
git merge --squash pr/dette-l4
git switch pr/dette-l3
git merge --ff-only pr/dette-l4
git switch pr/dette-l3
git merge pr/dette-l4
git push origin pr/dette-l3
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
AcadeNice/Acquarefactoring-Thomas!4
No description provided.