En testant la nouvelle vue Activité de la 4.82, on est tombé sur quelque chose d’inattendu — et ça a donné trois PRs qui se complètent. Merci énorme à @Will_71 pour ses tests sur sa propre base !
Épisode 1 — le bug invisible depuis 2 ans (PR #2650)
En voulant purger les états d’un capteur bavard (décocher « Conserver l’historique »), la tâche répondait instantanément : 0 states to delete… alors que la vue Activité affichait des milliers d’états pour cette feature.
Verdict après archéologie git :
Question
Réponse
Cassé depuis
v4.45.0 (août 2024)
Cause
La migration DuckDB (#2104) a déplacé les états, mais 2 requêtes SQLite ont été oubliées
Endroit 1
purgeStatesByFeatureId (le toggle « Conserver l’historique ») comptait et supprimait dans SQLite, désormais vide → no-op total
Endroit 2
Le garde-fou de device.destroy (« too much states ») comptait aussi SQLite → protection anti-blocage morte
Pourquoi invisible
Rien n’affichait l’historique par feature… jusqu’à la vue Activité
La #2650 migre les deux vers DuckDB (même requête que celle que destroy utilisait déjà) et garde le nettoyage des restes SQLite pour les installations pas encore migrées.
Conséquence du bug : chaque device/feature supprimé depuis 2 ans a pu laisser ses états « orphelins » dans DuckDB — invisibles, mais scannés par chaque requête et occupant du disque.
Sur proposition de @pierre-gilles : une tâche automatique one-shot au démarrage (pattern de la migration DuckDB : Variable système posée uniquement en fin de run → si Gladys redémarre en plein milieu, elle repart seule au boot suivant — testé en vrai en coupant le container au milieu ).
Et « la plus lente possible » : pas de comptage préalable (qui tenait la connexion lecture 15-20 min sur ma base), parcours en tranches hebdomadaires avec une pause de 5× la durée de chaque tranche — la purge ne consomme jamais qu’une fraction des ressources et s’adapte toute seule à la machine.
Test terrain
Base
Orphelins trouvés
Durée
Impact ressenti
Moi (serveur partagé avec HA + 2ᵉ Gladys)
448 M d’états
20
1 h 49 (cache froid) / 29 min (chaud)
Vue Activité : 125 ms pendant la purge (contre 44 s avec la 1ʳᵉ version non bridée)
Encore un grand merci à @Will_71 pour les tests complets des 2 PR
Épisode 3 — la page Tâches qui aurait détecté le bug en un jour (PR #2652)
Si la page Tâches avait affiché « 0 état trouvé » face à une Activité pleine, le bug de purge n’aurait pas survécu 2 ans. C’est maintenant le cas — les tâches portent des données structurées (traduites côté front, donc dans toutes les langues) :
Cible : Détecteur de présence cuisine › Détection mouvement
Étapes en direct : En attente de la base de données… / Calcul du nombre d’états… / Suppression des états… / Suppression des agrégats… (deux purges simultanées affichent honnêtement l’une qui travaille et l’autre qui attend)
Comptages : États à supprimer : N DuckDB + N SQLite — N agrégats, puis États supprimés : … en compte-rendu persistant
Compteur live pour la purge d’orphelins, % par tranches, durée qui tique pour les tâches en cours
Conception validée par les échanges sur la #2528 : aucune modification du wrapper de jobs — les tâches restent lancées par le pattern événement existant, et chaque job attache lui-même ses données via un paramètre optionnel d’updateProgress.
Pour la review
@pierre-gilles les 3 PRs sont prêtes (tests + coverage 100 % sur les lignes modifiées, CI verte, validées en réel sur deux installations). Ordre de merge : #2650 → #2651 → #2652 (la dernière est empilée sur les deux autres, son diff rétrécira après leurs merges).
CodeRabbit avait raison sur le point critique : la dernière tranche non bornée évaluait les états écrits pendant la purge contre un instantané figé des features — un device appairé pendant les ~30 min de purge pouvait perdre ses premiers états. Corrigé avec un cutoff capturé avant l’instantané, appliqué à toutes les tranches, + test de régression. Le test manquant de la branche « zéro feature » est ajouté aussi ; le warning SQL injection est un faux positif (placeholders uniquement), répondu dans la PR.
Mea culpa partagé avec Fable 5 sur ce coup : deux décisions raisonnables isolément (tranche finale ouverte pour couvrir les états écrits en cours de run + snapshot des features en début) devenues dangereuses combinées — et je n’avais pas relancé de passe de review dédiée en fin de développement auprès de Fable. Bonne illustration que la review croisée bot + agent + humain attrape des angles morts différents