# Rapport d’audit de blink2video.py Date de l’audit : 13 août 2026 Fuseau : Europe/Paris Révision auditée : 58f5566 Périmètre principal : blink2video.py Périmètre connexe nécessaire : runtime.py, serve.py, merge_daily.py, watch.py, tests.py, smoketest.py, blink2video.spec Environnement d’inspection : Python 3.12.0, blinkpy 0.25.9, aiohttp 3.14.3 ## 1. Objet du rapport Ce document conserve l’analyse détaillée de blink2video.py et transforme ses conclusions en programme de travail progressif. Les objectifs sont : 1. corriger les erreurs pouvant provoquer un arrêt, une corruption silencieuse, une perte d’état ou un processus orphelin ; 2. garantir chaque correction par un test de non-régression isolé ; 3. mesurer les performances avant toute modification ; 4. vérifier après chaque étape que les coûts ont diminué ou, à défaut, qu’aucune régression mesurable n’a été introduite ; 5. ne passer à l’étape suivante qu’après satisfaction de critères de sortie explicites ; 6. consigner, pour chaque étape réellement terminée, le travail effectué, les tests exécutés, les valeurs mesurées, les écarts au plan et le verdict. Le rapport décrit un plan. Aucune correction de production n’a été appliquée pendant l’audit. ## 2. Résumé exécutif Le fichier est lisible, très commenté et possède plusieurs bonnes fondations : - téléchargement vers un fichier partiel puis renommage ; - normalisation des dates en UTC ; - sauvegarde atomique envisagée pour les fichiers JSON ; - poursuite du lot après l’échec isolé d’un clip ; - séparation des gros verbes délégués dans des modules distincts ; - volonté explicite de préserver les clips et les exclusions. La configuration recommandée, blink2video start, met cependant plusieurs processus en concurrence. Les mécanismes actuels ne garantissent pas les invariants annoncés par le code. Les cinq invariants à rétablir en premier sont : 1. une collection vide est un succès vide, jamais une erreur de type ; 2. un fichier déclaré téléchargé est réellement une vidéo complète ; 3. un événement déjà acquis reste acquis après renumérotation distante ; 4. une décision plus récente, notamment une exclusion, ne peut pas être écrasée par une copie d’état plus ancienne ; 5. une ressource verrouillée ne peut être utilisée que par un propriétaire à la fois. La prochaine release devrait être bloquée tant que les anomalies B-01 à B-06 ne sont pas corrigées et couvertes. Une évolution fonctionnelle a été ajoutée au périmètre après l’audit : - lancer blink2video sans argument doit être équivalent à blink2video start ; - le démarrage doit d’abord vérifier réellement l’authentification ; - si elle manque ou n’est plus valide, une page locale de connexion doit s’ouvrir dans le navigateur ; - le formulaire doit gérer les identifiants et la vérification en deux étapes ; - un premier contrôle read-only de type list ou mini-smoke doit confirmer que la session et le compte fonctionnent ; - les boucles permanentes ne doivent être lancées qu’après ce contrôle. ## 3. Méthode et limites ### 3.1 Travail effectué L’audit a comporté : - lecture intégrale des 1 064 lignes de blink2video.py ; - cartographie de ses appels à runtime.py, serve.py, merge_daily.py et watch.py ; - lecture de tests.py et smoketest.py ; - inspection de la version de blinkpy installée ; - analyse AST ; - analyse Ruff sans correction automatique ; - sondes ciblées en mémoire ou dans des dossiers temporaires ; - revue séparée des chemins téléchargement, CLI/processus et couverture. ### 3.2 Défauts reproduits sans compte Blink Les comportements suivants ont été reproduits : - manifeste cloud vide : TypeError lors du déballage du résultat ; - option --since 0 : requête portant sur 30 jours ; - option --loop -1 : valeur acceptée ; - port 70000 pour open : OverflowError brut ; - noms A/B et A_B : même safe_name ; - download --camera watch : watch est interprété comme un nouveau verbe ; - JSON racine de mauvais type : AttributeError ; - fusion d’un état périmé : suppression du drapeau excluded. ### 3.3 Limites L’audit n’a pas utilisé d’identifiants Blink, de caméra ni de Sync Module. Les interactions réelles avec le service devront être validées dans un test manuel optionnel, jamais dans la CI. La suite complète tests.py n’a pas été exécutée pendant cette phase de lecture seule, car elle peut envoyer une notification système et écrire des états dans le dossier applicatif réel. ## 4. Carte du fichier | Zone | Lignes approximatives | Responsabilité | |---|---:|---| | Authentification | 59–187 | session enregistrée, identifiants, 2FA | | Inventaires | 190–345 | modules, manifeste USB, cloud, filtres | | Noms et chemins | 348–378 | tailles, safe_name, target_path | | Téléchargement USB | 381–399 | préparation, fichier partiel, renommage | | Registre incrémental | 402–501 | chargement, fusion, identité, exclusion | | Arguments | 515–599 | analyseur des verbes locaux | | Traitement cloud | 602–677 | rapprochement et téléchargement | | Boucles et passage | 680–826 | connexion, USB, cloud, notifications | | Délégation | 829–885 | import des autres verbes et open | | Arrêt | 888–929 | lecture des fiches et terminaison | | Supervision | 932–1038 | composition, enfants, codes de retour | | Point d’entrée | 1041–1064 | routage global et erreurs terminales | ## 5. Échelle de priorité - B : bloquant. Crash fréquent, corruption silencieuse, perte d’une décision utilisateur ou exclusion mutuelle non garantie. - I : important. Fiabilité dégradée, comportement incohérent, sécurité ou portabilité insuffisante. - O : optimisation ou amélioration structurelle. Coût évitable, dette technique ou couverture incomplète. ## 6. Anomalies bloquantes ### B-01 — Contrat de retour cassé sur un cloud vide Emplacements : - blink2video.py:602, traiter_cloud ; - blink2video.py:613–614, retour False ; - blink2video.py:806–809, déballage de deux valeurs. Quand le cloud est vide ou que le filtre de caméra ne retient rien, traiter_cloud renvoie un booléen seul. L’appelant attend toujours erreur_cloud, nouveaux_cloud. Conséquences : - TypeError ; - arrêt de list avec la source all ; - arrêt de download --from cloud ; - mort définitive de la boucle cloud de start. Correction attendue : - un type de retour unique ; - idéalement un résultat nommé contenant téléchargés, adoptés, ignorés et échecs ; - à court terme, retourner systématiquement un tuple. Test de non-régression : T-B01. ### B-02 — Corps HTTP d’erreur enregistré comme MP4 Emplacements : - blink2video.py:260–271, CloudClip.download_to ; - blink2video.py:646–650, renommage et inscription au registre. Le statut HTTP n’est jamais contrôlé. Tout corps non vide est accepté. Une réponse 401, 404, 429 ou 500 contenant du JSON ou du HTML peut donc devenir un fichier .mp4 et être inscrite comme téléchargement réussi. Conséquences : - corruption silencieuse ; - erreur découverte seulement pendant l’assemblage ; - fichier durablement considéré comme acquis. Correction attendue : - exiger un statut de succès ; - distinguer les erreurs transitoires des erreurs définitives ; - écrire le corps par blocs ; - vérifier la taille annoncée si elle existe ; - vérifier au minimum la boîte MP4 ftyp ou effectuer un probe vidéo ; - ne renommer et ne committer le registre qu’après validation. Tests : T-B02-200, T-B02-401, T-B02-404, T-B02-429, T-B02-500, T-B02-TRUNCATED. Benchmarks : P-06 et P-07. ### B-03 — Réindexation USB suivie d’un retéléchargement Emplacements : - blink2video.py:372–378, target_path ; - blink2video.py:451–464, state_key ; - blink2video.py:485–501, is_downloaded. La clé d’état ignore clip.id afin de survivre à une réindexation, mais le nom de fichier contient clip.id. Après une réindexation : 1. la clé d’état reste la même ; 2. le chemin recalculé change ; 3. is_downloaded ne consulte pas entry.path ; 4. le nouveau chemin est absent ; 5. le même clip est téléchargé une seconde fois. Conséquences : - doublons en masse ; - espace disque gaspillé ; - ancien fichier orphelin ; - promesse du README non tenue. Correction attendue : - faire de entry.path l’autorité pour un acquis existant ; - séparer identité distante, identité d’événement et nom de fichier ; - prévoir une migration de registre versionnée. Tests : T-B03-REINDEX, T-B03-MISSING, T-B03-EXCLUDED. Benchmark : P-04. ### B-04 — Une sauvegarde périmée peut annuler une exclusion Emplacements : - blink2video.py:417–448, save_download_state et _ecrire_registre ; - merge_daily.py:455–463, écriture d’une exclusion. Le registre est verrouillé au moment de l’écriture, mais le downloader détient une copie complète chargée auparavant. _ecrire_registre relit le disque puis applique clips.update avec la copie ancienne. Scénario : 1. A charge le registre ; 2. B pose excluded=True et sauvegarde ; 3. A ajoute un autre clip ; 4. A sauvegarde sa copie ; 5. la version non exclue de A remplace la décision de B. Conséquences : - perte d’une action explicite de l’utilisateur ; - clip exclu susceptible d’être téléchargé à nouveau ; - incohérence entre Blink_Excluded et le registre. Correction attendue : - transaction unique couvrant lecture, décision et écriture ; - mise à jour par delta et non par copie complète ; - protection forte des tombstones ; - recommandation : stockage transactionnel SQLite en mode WAL. Tests : T-B04-INTERLEAVE, T-B04-TOMBSTONE, T-B04-CRASH. Benchmarks : P-02 et P-03. ### B-05 — Verrou interprocessus non atomique et volé après dix minutes Emplacements : - runtime.py:491–541, verrou ; - blink2video.py:719–723, verrou download ; - blink2video.py:431, verrou registre. Le protocole actuel lit le fichier, conclut qu’il est libre, puis l’écrit. Deux processus peuvent effectuer les deux premières opérations simultanément et entrer tous les deux. Un verrou plus vieux que stale_after est en outre considéré périmé même si son PID est vivant. Le téléchargement conserve son verrou sur un passage complet, qui peut durer plus de dix minutes. Conséquences : - deux écrivains sur le même .part ; - deux mises à jour concurrentes du registre ; - assemblages concurrents ; - suppression possible du verrou du voisin. Correction attendue : - verrou OS ou création exclusive atomique ; - jeton UUID propre à l’acquisition ; - identité du processus incluant son heure de création ; - ne jamais expirer un propriétaire vivant sur le seul critère du temps ; - heartbeat si une lease temporelle reste nécessaire. Tests : T-B05-RACE20, T-B05-LONG, T-B05-CRASH, T-B05-PID-REUSE. Benchmark : P-08. ### B-06 — Le téléchargement USB ne réserve pas le Sync Module Emplacements : - blink2video.py:34–36, hub_lock ; - blink2video.py:736–805, parcours USB ; - serve.py:833 et serve.py:1149, verrou pris par le direct et l’actualisation. Le downloader prend le verrou download, mais jamais le verrou hub partagé avec le direct. Un direct peut donc commencer pendant la fabrication du manifeste, prepare_download ou download_video. Conséquences : - réponses System is busy ; - échecs intermittents ; - retries coûteux ; - fiabilité dépendante du hasard de planification. Correction attendue : - verrou par sync_id ; - section limitée aux opérations qui utilisent réellement le module ; - aucune réservation du hub pour le cloud ; - éviter les verrous imbriqués non réentrants dans serve. Tests : T-B06-DIRECT-USB, T-B06-HUBS-DISTINCTS, T-B06-CLOUD-INDEPENDANT. Benchmark : P-09. ## 7. Anomalies importantes ### I-01 — CONFIG et OUTPUT ignorent BLINK_HOME Emplacements : - blink2video.py:26–27 ; - runtime.py:206–216 ; - blink2video.py:994 et 1008. Les chemins par défaut sont relatifs au répertoire courant. Une commande simple et la même commande composée ne travaillent donc pas nécessairement au même endroit. Risques : - refresh token déposé dans un autre projet ; - clips écrits ailleurs ; - serveur ne voyant pas la session créée en CLI ; - contradiction avec le README. ### I-02 — Sauvegardes d’authentification concurrentes Emplacements : - blink2video.py:157–164 ; - runtime.py:62–66, composition start. Toutes les sessions emploient blink_auth.tmp sans verrou. Plusieurs clients peuvent remplacer le même temporaire ou écraser un token plus récent. Le temporaire blink_auth.tmp n’est pas couvert par les motifs actuels du .gitignore. ### I-03 — Tokens rafraîchis non persistés Emplacement : blink2video.py:88–92. make_blink ne fournit pas de callback de persistance à Auth. Une session longue peut rafraîchir son token uniquement en mémoire. Après un crash, le disque peut contenir une version ancienne. ### I-04 — Déduplication asymétrique USB/cloud Emplacements : - blink2video.py:300–326, rapprocher ; - blink2video.py:620–624, emploi côté cloud ; - blink2video.py:756–764, absence d’équivalent côté USB. Si l’USB arrive avant le cloud, le cloud est rapproché. Si le cloud arrive avant l’USB, l’USB retélécharge l’événement. ### I-05 — Fichier cloud absent impossible à réparer Emplacements : blink2video.py:620–638. Toutes les entrées du registre participent au rapprochement, même si leur fichier est absent ou vide. --overwrite intervient trop tard pour changer cette décision. Une absence accidentelle doit entraîner une réparation. Seule une tombstone excluded doit interdire le téléchargement. ### I-06 — Manifeste privé, cumulatif et manifest_id potentiellement périmé Emplacements : - blink2video.py:209–231 ; - blink2video.py:391–392. Le code dépend de sync._local_storage, interface privée de blinkpy. Dans la version inspectée, le manifeste est un SortedSet conservé entre les mises à jour. Les objets déjà présents peuvent conserver leur ancien manifest_id. L’adaptateur du projet doit produire son propre snapshot frais et actualiser l’adresse de chaque item. ### I-07 — --since 0 devient 30 jours Emplacement : blink2video.py:281. since_days or 30 confond zéro et absence de valeur. Le chemin USB traite zéro comme un cutoff immédiat, le cloud comme 30 jours. ### I-08 — Pagination cloud plafonnée et coûteuse Emplacements : blink2video.py:281–284. stop=20 correspond à 19 pages dans blinkpy 0.25.9, soit approximativement 475 éléments si une page en contient 25. Les mêmes pages peuvent être relues chaque minute. Risques : - clips anciens jamais vus dans une fenêtre très active ; - centaines ou milliers de requêtes quotidiennes inutiles ; - latence proportionnelle à 30 jours d’historique plutôt qu’aux nouveautés. ### I-09 — Compte sans Sync Module refusé Emplacements : blink2video.py:690–705. main exige blink.sync avant même de terminer login ou de considérer une source cloud. Cela contredit l’indépendance annoncée du cloud et l’existence de _HubCloud. ### I-10 — JSON valide mais schéma invalide non géré Emplacements : - blink2video.py:59–77 ; - blink2video.py:402–414 ; - blink2video.py:622–676 ; - runtime.py:516–527 ; - runtime.py:288–303. Les lecteurs supposent trop tôt un dictionnaire et des champs convertibles. Une racine [], null, une entrée de clip non dictionnaire ou une date invalide peut faire échouer toute la commande. ### I-11 — Valeur d’option confondue avec un verbe Emplacement : runtime.py:652–674. Toute chaîne égale à un verbe ouvre un nouveau groupe. Les guillemets ne changent rien, car le shell les retire avant Python. Exemples problématiques : blink2video download --camera watch blink2video download --camera update blink2video watch --ignore serve Le second exemple peut aller jusqu’à lancer une mise à jour non demandée après l’échec du premier groupe. ### I-12 — Boucles non validées et classification textuelle Emplacements : - runtime.py:677–686 ; - blink2video.py:727–729 ; - blink2video.py:987. Problèmes : - valeur négative acceptée ; - zéro signifie une exécution ; - --loop=1 n’est pas détecté comme persistant ; - comportements différents entre asyncio.sleep et time.sleep ; - risque de boucle serrée contre l’API. ### I-13 — Supervision incomplète Emplacements : - blink2video.py:969–1038 ; - runtime.py:264–340. Cas non couverts : - download --loop seul non inscrit ; - enfant ponctuel non inscrit ; - exception pendant les démarrages ; - Ctrl+C avant l’entrée dans le bloc de nettoyage ; - descendants ffmpeg non arrêtés ; - absence de wait après terminate ; - fiche supprimée alors qu’un enfant vit ; - enfant qui termine avant la première itération de surveillance. ### I-14 — stop peut perdre sa cible ou viser un PID recyclé Emplacements : - blink2video.py:905–928 ; - runtime.py:233–303. La fiche est supprimée avant confirmation de la mort. L’identité du processus est limitée au PID ; l’heure enregistrée dans la fiche n’est jamais comparée à l’heure de création fournie par le système. ### I-15 — Contrôle d’intégrité limité à une taille non nulle Emplacements : - blink2video.py:383–384 ; - blink2video.py:485–501 ; - blink2video.py:638–642 ; - blink2video.py:760–762. Un fichier d’un octet est considéré complet. Le champ bytes enregistré dans le registre n’est jamais utilisé pour vérifier le fichier. ### I-16 — Collisions de noms et composants de chemin non bornés Emplacements : blink2video.py:366–378. safe_name transforme des noms distincts en une même chaîne. Les noms réservés Windows, la longueur maximale et clip.id ne sont pas normalisés. Une valeur API inattendue peut créer un chemin invalide ou sortir de l’organisation prévue. ### I-17 — Erreurs transitoires non contenues Emplacements : - blink2video.py:714–729 ; - blink2video.py:738–743 ; - blink2video.py:806–809. boucler ne capture que BusyError. Une panne HTTP, une erreur d’authentification ou un changement de schéma peut tuer définitivement une boucle de fond. Il faut distinguer : - succès vide ; - réponse partielle ; - indisponibilité transitoire ; - authentification à renouveler ; - erreur de données définitive. ### I-18 — Compteurs cloud incorrects Emplacements : blink2video.py:638–661. Un fichier déjà présent mais adopté dans le registre est inclus dans len(inedits) - echecs et peut être annoncé comme nouveau téléchargement. ### I-19 — Aide de --overwrite non conforme Emplacements : - blink2video.py:580 ; - blink2video.py:758–764 ; - blink2video.py:621–638. L’aide parle de remplacer les fichiers de taille différente. Le chemin USB retélécharge en pratique tout ce qui est sélectionné avec --overwrite. Le cloud peut au contraire ne rien remplacer si le registre contient déjà l’événement. Les notions repair, overwrite et force doivent être définies séparément. ### I-20 — Identité cloud liée arbitrairement au premier module Emplacement : blink2video.py:630–668. Le premier Sync Module sélectionné est utilisé pour former les clés de tous les clips cloud. Sur un compte multi-réseaux, clip.network_id est l’identité plus pertinente. ### I-21 — Matching temporel non bijectif Emplacements : blink2video.py:312–325. Plusieurs clips cloud peuvent être classés comme doublons du même clip local. Le rapprochement doit consommer un match et employer un identifiant stable de caméra. ### I-22 — Port open non validé et serveur non identifié Emplacements : blink2video.py:870–884. Un port hors plage produit une trace brute. Toute socket locale ouverte est considérée comme l’interface blink2video, sans endpoint de santé ni version. ## 7.1 Évolution fonctionnelle demandée ### E-01 — Démarrage sans argument et onboarding web Comportement actuel : - blink2video sans argument affiche l’aide et se termine ; - start lance directement plusieurs processus ; - en l’absence de session, plusieurs consommateurs peuvent tenter de se connecter ou rester sans authentification ; - /api/status vérifie principalement la présence du fichier, pas la validité de la session ; - serve.py possède déjà LoginFlow, /api/login, /api/2fa et une boîte de dialogue de connexion réutilisables. Comportement cible : 1. blink2video et blink2video start appellent exactement le même parcours ; 2. --help et --version conservent leur sens et ne démarrent rien ; 3. un unique processus propriétaire exécute le préflight ; 4. la session sauvegardée est testée sans interaction ; 5. si elle est valide, le parcours continue sans ouvrir le navigateur ; 6. si elle est absente ou invalide, un serveur local limité à 127.0.0.1 expose la page de connexion ; 7. le navigateur s’ouvre sur cette page ; si cela échoue, l’URL est affichée ; 8. l’e-mail, le mot de passe et le code 2FA sont traités dans ce processus unique ; 9. le mot de passe n’est ni écrit, ni journalisé, ni conservé après le flux ; 10. après succès, un mini-smoke read-only vérifie le compte ; 11. le résultat du contrôle est affiché dans la page et le terminal ; 12. les processus de start ne sont créés qu’après succès ; 13. en cas d’échec ou d’annulation, aucune boucle de fond n’est laissée. Le mini-smoke recommandé réutilise la logique métier de list mais expose un résultat structuré : - refresh/authentification réussie ; - homescreen du compte lisible ; - inventaire des réseaux, Sync Modules et caméras lisible ; - compte cloud-only accepté ; - aucun direct, armement, manifeste USB ou téléchargement déclenché. Sécurité minimale de la page : - écoute loopback uniquement ; - jeton CSRF aléatoire et à usage court ; - une seule tentative active à la fois ; - réponses no-store ; - aucun identifiant dans les logs ; - délai d’expiration et bouton d’annulation ; - limitation des tentatives ; - fermeture de la session temporaire après succès ou abandon. Cette évolution est planifiée à l’étape 5, après la stabilisation du magasin d’authentification et avant le refactor général de la CLI. ## 8. Optimisations et améliorations structurelles ### O-01 — Téléchargement cloud entièrement en mémoire Emplacement : blink2video.py:264–270. Le corps entier est lu avant Path.write_bytes. Le streaming par blocs permet : - mémoire bornée ; - suivi de progression ; - taille et hash au fil de l’eau ; - reprise ou nettoyage plus simples. ### O-02 — Réécriture complète du JSON après chaque clip Emplacements : blink2video.py:788–794 et 648–650. Chaque succès relit et sérialise la totalité du registre. Un lot de N clips peut ainsi approcher un coût quadratique. ### O-03 — Rapprochement temporel quadratique Emplacements : blink2video.py:312–325. Pour chaque clip cloud, toutes les dates de la caméra sont parcourues. Un index trié avec bisect ou un index SQL réduit ce coût. ### O-04 — Scan cloud sans watermark Emplacements : blink2video.py:274–297. Une boucle à la minute relit jusqu’à 30 jours. Un watermark durable avec recouvrement réduit les appels tout en protégeant contre les arrivées tardives. ### O-05 — Cadence qui dérive Emplacements : blink2video.py:727–729 et runtime.py:689–706. Le sommeil commence après le travail. Une opération de quatre minutes avec une cadence de cinq minutes démarre en réalité toutes les neuf minutes. Une échéance fondée sur time.monotonic évite cette dérive. ### O-06 — Bootstrap trop précoce Emplacements : blink2video.py:14–23 et runtime.py:136–198. Le bootstrap s’exécute avant de savoir si la commande est help, stop, open, update ou un verbe Blink. Une installation cassée peut rendre stop inutilisable et une simple aide peut créer un environnement ou contacter PyPI. ### O-07 — Dépendances non contraintes Emplacements : - runtime.py:127–133 ; - build.py:33 ; - workflows CI et release. Le projet dépend d’une API privée de blinkpy mais installe la dernière version disponible. Il faut une contrainte testée et une tâche séparée de mise à niveau. ### O-08 — Responsabilités trop nombreuses dans un fichier blink2video.py contient cinq sous-systèmes : 1. authentification ; 2. adaptateurs de sources ; 3. persistance ; 4. CLI ; 5. supervision. Leur séparation faciliterait les tests sans compte, les types explicites et les évolutions indépendantes. ### O-09 — Code devenu mort contextlib, HUB_LOCK et _process_alive ne sont plus utilisés dans blink2video.py. Les avertissements Ruff E402 autour des imports après runtime.bootstrap sont en revanche intentionnels tant que cette architecture existe ; ils doivent être documentés ou supprimés par le bootstrap tardif, pas contournés aveuglément. ### O-10 — Fichiers de verrou non ignorés Le .gitignore cite .blink_hub.lock mais pas les nouveaux verrous download, merge et registre. Le motif .blink_*.lock doit couvrir tous les artefacts d’exécution. ## 9. État de la couverture tests.py couvre principalement : - aide des verbes ; - assemblage vidéo ; - exclusion dans merge_daily ; - interface HTTP ; - un chemin heureux de stop. Il ne couvre directement aucune fonction d’authentification, de manifeste, de rapprochement, de téléchargement ou de registre de blink2video.py. Le commentaire de rapprocher affirmant que tests.py l’éprouve est faux. Le mode BLINK_TEST_BUNDLE est incomplet : - test_verbes lance toujours le fichier Python ; - le test HTTP lance toujours serve.py ; - test_relance inspecte runtime.py des sources ; - seuls certains chemins merge et stop utilisent le bundle. Les ports fixes 8899 et 8945 peuvent entrer en collision avec un service réel ou produire un faux positif. ## 10. Architecture cible La cible proposée reste une application locale, sans service externe ajouté. ### 10.1 Parcours de démarrage convivial Le point d’entrée suit une machine d’état explicite : invocation → préflight d’authentification → session valide ? oui → mini-smoke read-only non → serveur d’onboarding local → formulaire identifiants → formulaire 2FA si demandé → sauvegarde de session → mini-smoke read-only → préflight réussi ? oui → lancement de la composition standard non → diagnostic, aucune boucle lancée Le serveur d’onboarding peut réutiliser les composants de serve.py, mais le flux d’authentification doit vivre dans un module indépendant afin que le lanceur ne dépende pas du serveur complet. ### 10.2 Racine de données unique Tous les chemins applicatifs dérivent d’une seule racine : - BLINK_HOME si défini ; - sinon le dossier de données documenté. La racine contient authentification, médias, base d’état, verrous, journaux et fiches de processus. Le CWD ne change jamais leur emplacement. ### 10.3 Modèle de clip explicite Un clip porte séparément : - source : usb ou cloud ; - compte et réseau ; - identifiant stable de caméra ; - nom d’affichage ; - identifiant distant ; - instant UTC ; - chemin local ; - taille et éventuellement hash ; - statut ; - provenance corrélée ; - état excluded. ### 10.4 Persistance transactionnelle SQLite WAL est recommandé pour : - transactions multi-processus ; - contraintes uniques ; - index temporels ; - claims de téléchargement ; - tombstones protégées ; - curseurs de pagination ; - mises à jour partielles ; - migrations versionnées. Le JSON peut rester un format d’export et de diagnostic. ### 10.5 Adaptateurs de sources Un adaptateur USB encapsule toute dépendance aux détails privés de blinkpy. Un adaptateur cloud gère pagination, watermark et réponses HTTP. Le reste du programme manipule le même modèle de clip. ### 10.6 Superviseur unique Le superviseur : - parse toute la commande avant tout effet ; - connaît le cycle de vie de chaque verbe ; - enregistre tous les enfants ; - utilise groupes POSIX ou Job Objects Windows ; - arrête, attend puis force si nécessaire ; - conserve la fiche tant qu’un processus survit. ## 11. Règles communes à toutes les étapes Chaque étape suit la séquence suivante : 1. écrire ou sélectionner les tests qui échouent sur la version précédente ; 2. enregistrer le baseline des benchmarks concernés ; 3. effectuer uniquement les changements de l’étape ; 4. faire passer les tests nouveaux puis la suite complète ; 5. mesurer la version nouvelle avec le même protocole ; 6. produire un tableau avant/après ; 7. analyser toute régression supérieure aux seuils ; 8. ne fusionner qu’après satisfaction du critère de sortie. Une amélioration fonctionnelle ne justifie pas de supprimer un test existant. Un seuil de performance ne doit être relâché qu’avec une raison documentée dans le rapport de l’étape. ### 11.1 Compte rendu obligatoire après chaque étape Une étape n’est pas considérée réalisée parce que son code est présent. Elle est réalisée seulement quand son journal, à la section 28, contient : - statut et dates de début/fin ; - commits de départ et d’arrivée ; - périmètre prévu ; - travail effectivement effectué ; - fichiers et migrations concernés ; - décisions prises et écarts par rapport au plan ; - tests ajoutés, modifiés et exécutés ; - résultat absolu des tests, pas seulement « vert » ; - environnement exact des mesures ; - valeurs baseline et après modification ; - médiane, p95, mémoire, appels API et tailles selon le cas ; - delta absolu et relatif ; - intervalle de confiance pour les mesures ciblées ; - seuil attendu et verdict ; - régressions observées et justification éventuelle ; - anomalies restantes ; - décision : acceptée, à reprendre ou bloquée. Le journal doit conserver les nombres même lorsqu’ils sont défavorables. Une mesure remplacée reste historisée avec la raison de son invalidation. ### 11.2 Format du tableau de mesure d’une étape | Mesure | Baseline | Après | Delta | IC 95 % | Seuil | Verdict | |---|---:|---:|---:|---:|---:|---| | À remplir | — | — | — | — | — | non mesuré | ### 11.3 Format du bilan de tests d’une étape | Suite/scénario | Avant | Après | Durée | Environnement | Verdict | |---|---|---|---:|---|---| | À remplir | — | — | — | — | non exécuté | ## 12. Étape 0 — Banc de sécurité et baseline ### 12.1 Objectif Rendre toutes les étapes suivantes mesurables et reproductibles avant de modifier la production. ### 12.2 Travail 0.1. Créer une suite unitaire dédiée à blink2video. 0.2. Forcer BLINK_BOOTSTRAP=none dans les tests. 0.3. Utiliser un BLINK_HOME temporaire par test. 0.4. Placer le CWD ailleurs que la racine de données. 0.5. Fournir des faux Blink, Sync, Clip, Auth et ClientResponse. 0.6. Injecter horloge, sommeil, session HTTP, notification et navigateur. 0.7. Ajouter un petit serveur HTTP local contrôlable. 0.8. Créer des workers multi-processus pour les courses. 0.9. Créer un générateur déterministe de registres et manifests. 0.10. Créer la commande de benchmark et son format JSON de résultat. 0.11. Enregistrer matériel, OS, Python, versions et commit avec le résultat. 0.12. Corriger les tests bundle pour employer réellement le bundle partout. 0.13. Allouer les ports dynamiquement. 0.14. Fournir un faux navigateur et un serveur d’onboarding pilotable. 0.15. Capturer les appels d’authentification et vérifier l’absence de secrets dans les fichiers, journaux et exceptions. ### 12.3 Tests à écrire en premier - T-B01 : cloud vide et filtre vide ; - T-B02 : statuts HTTP et corps invalides ; - T-B03 : réindexation ; - T-B04 : interleaving exclusion/download ; - T-B05 : acquisition concurrente ; - T-B06 : direct contre USB ; - JSON racine et entrées invalides ; - BLINK_HOME avec CWD différent ; - --since 0 ; - --loop négatif, nul, séparé et forme égale ; - valeurs de caméra égales à un verbe ; - parent mort avec enfant vivant ; - échec de kill et conservation de fiche ; - sans argument contre start, avec plan de lancement observable ; - session valide, absente et expirée sans ouvrir un navigateur réel ; - succès, refus, 2FA, annulation et timeout du futur onboarding ; - mini-smoke en succès et en échec, sans worker permanent. ### 12.4 Baseline à enregistrer Exécuter P-01 à P-12 définis à la section 22 sur la révision 58f5566. Pour P-12, les branches qui n’existent pas encore dans la révision auditée sont consignées comme « non disponibles », jamais comme zéro. Le baseline doit néanmoins mesurer le comportement actuel sans argument, start avec session valide/invalide et le nombre d’appels ou processus observables. Les tests du comportement cible peuvent être conservés comme tests attendus en échec avec un identifiant E-01 explicite jusqu’à l’étape 5 ; ils ne doivent pas être masqués comme succès. ### 12.5 Critère de sortie - tous les tests historiques passent ; - les nouveaux tests reproduisent effectivement les défauts attendus ; - aucun test ne touche le compte ou les données réelles ; - deux exécutions successives des benchmarks varient de moins de 10 % sur leur médiane, hors benchmark réseau réel ; - le baseline brut est conservé. ## 13. Étape 1 — Correctifs de sûreté immédiats ### 13.1 Périmètre Cette étape corrige les défauts isolés sans changer encore le modèle de données. 1.1. Retour homogène de traiter_cloud. 1.2. Distinction entre since=None et since=0. 1.3. Validation stricte des jours, cadences et ports. 1.4. Validation de la racine JSON et des entrées. 1.5. Refus des statuts HTTP non réussis. 1.6. Compteurs downloaded, adopted, skipped et failed séparés. 1.7. Autoriser login et le cloud sans Sync Module. 1.8. Messages d’erreur typés, sans trace brute pour une erreur utilisateur. 1.9. Définition explicite de repair, overwrite et force. ### 13.2 Non-régression - cloud vide retourne zéro et code 0 ; - filtre vide retourne zéro ; - réponse non-2xx ne crée ni cible ni entrée ; - fichier partiel supprimé après tout échec ; - --since 0 n’utilise jamais 30 jours ; - port hors plage rejeté par argparse ; - cadence non positive rejetée ; - JSON invalide ou de mauvais type produit un diagnostic et une stratégie documentée ; - compte sans hub peut effectuer login et cloud. ### 13.3 Performance - comparer P-01, P-06 et P-07 ; - aucune régression de plus de 10 % sur le démarrage d’une commande simple ; - aucune régression de plus de 5 % du débit d’écriture vers un faux serveur local ; - mémoire temporairement inchangée acceptée, le streaming arrivant à l’étape 6. ### 13.4 Critère de sortie B-01 et la partie statut HTTP de B-02 sont fermés. Tous les tests de l’étape passent sur les trois OS. ## 14. Étape 2 — Identité, déduplication et réparation ### 14.1 Périmètre 2.1. Définir l’identité source stable. 2.2. Définir l’identité de corrélation inter-source. 2.3. Ne plus employer safe_name comme identité métier. 2.4. Faire de entry.path l’autorité pour un acquis existant. 2.5. Rendre le matching symétrique USB/cloud. 2.6. Rendre le matching un-à-un. 2.7. Réparer un fichier absent ou invalide. 2.8. Préserver toute tombstone excluded. 2.9. Utiliser network_id propre au clip cloud. 2.10. Produire un nom de fichier sûr, borné et stable. 2.11. Concevoir une migration idempotente des clés version 1. ### 14.2 Non-régression - réindexation 42 vers 99 : aucun nouveau téléchargement ; - USB puis cloud : un seul événement ; - cloud puis USB : un seul événement ; - deux événements légitimes proches : deux résultats ; - un match local ne consomme qu’un cloud ; - fichier absent non exclu : réparation ; - fichier absent exclu : aucun téléchargement ; - caméra renommée : pas de duplication si un identifiant stable existe ; - A/B et A_B restent distincts ; - multi-réseaux et multi-hubs sans collision ; - migration relancée deux fois sans changement supplémentaire. ### 14.3 Performance - comparer P-04 et P-05 ; - objectif initial : rapprochement au moins cinq fois plus rapide à 50 000 éléments ; - objectif initial : temps sous-linéaire par rapport au produit archive × cloud ; - aucune augmentation de plus de 5 % du nombre de fichiers produits pour un jeu sans doublon ; - zéro doublon sur les jeux corrélés. ### 14.4 Critère de sortie B-03, I-04, I-05, I-16, I-20 et I-21 sont fermés. La migration possède un test de retour arrière à partir d’une copie de sauvegarde. ## 15. Étape 3 — Persistance et verrous transactionnels ### 15.1 Périmètre 3.1. Choisir SQLite WAL, sauf contre-indication mesurée. 3.2. Définir le schéma et les migrations. 3.3. Ajouter contraintes uniques et index temporels. 3.4. Représenter séparément téléchargement et exclusion. 3.5. Implémenter un claim atomique par clip. 3.6. Remplacer les copies complètes par des mises à jour partielles. 3.7. Rendre les migrations atomiques avec sauvegarde. 3.8. Remplacer runtime.verrou par un mécanisme atomique. 3.9. Employer un jeton d’acquisition, pas seulement le PID. 3.10. Ne jamais voler le verrou d’un processus vivant sur le seul âge. 3.11. Prévoir récupération après crash. 3.12. Conserver un export JSON lisible si nécessaire. Si SQLite est refusé, les exigences minimales sont : - création atomique exclusive ; - temporaire unique ; - fsync fichier puis dossier ; - merge de deltas ; - numéro de génération ; - verrou couvrant lecture et écriture ; - tests de course identiques. ### 15.2 Non-régression - vingt processus ne peuvent acquérir le même verrou ; - mille itérations de l’interleaving exclusion/download sans perte ; - processus tué pendant un commit : base cohérente ; - processus vivant au-delà de dix minutes : verrou conservé ; - processus mort : ressource récupérable ; - PID recyclé : ancien verrou non attribué au nouveau processus ; - migration interrompue : ancien état restaurable ; - aucun clip ni tombstone perdu. ### 15.3 Performance - comparer P-02, P-03, P-04 et P-08 ; - écriture de 10 000 événements au moins cinq fois plus rapide que les sauvegardes JSON clip par clip ; - p95 d’une mise à jour unitaire inférieur ou égal au baseline ; - croissance proche de O(N) pour un lot, pas O(N²) ; - débit concurrent supérieur au débit séquentiel sans perte de correction ; - taille de stockage documentée pour 1 000, 10 000 et 100 000 événements. ### 15.4 Critère de sortie B-04 et B-05 sont fermés. La base survit aux tests de crash et de concurrence. Aucune tombstone n’est perdue sur 100 répétitions complètes. ## 16. Étape 4 — Racine de données et authentification ### 16.1 Périmètre 4.1. Dériver CONFIG et OUTPUT de la racine unique. 4.2. Documenter la résolution d’un --output relatif. 4.3. Créer le dossier de données avec permissions privées. 4.4. Écrire le fichier d’authentification en 0600 sur POSIX. 4.5. Utiliser une liste blanche de champs persistés. 4.6. Employer un temporaire unique et un replace atomique. 4.7. Coordonner charge, refresh et sauvegarde. 4.8. Persister les tokens rafraîchis par callback. 4.9. Choisir entre propriétaire unique de session et contrôle de génération. 4.10. Ajouter éventuellement keyring, DPAPI ou Keychain comme amélioration optionnelle, sans rendre le mode fichier inutilisable. ### 16.2 Non-régression - même chemin depuis tout CWD ; - BLINK_HOME respecté par login, download, serve et merge ; - programme installé dans un dossier en lecture seule ; - deux sauvegardes concurrentes sans exception ni perte du token le plus récent ; - refresh puis crash puis redémarrage réussi ; - mot de passe absent du fichier et du temporaire ; - permissions vérifiées ; - fichier JSON ne contient que les champs autorisés. ### 16.3 Performance - comparer P-01 et P-10 ; - pas plus de 10 % de régression sur connexion depuis une session valide ; - une seule écriture durable par rotation réelle de token ; - pas de polling supplémentaire au repos. ### 16.4 Critère de sortie I-01, I-02 et I-03 sont fermés. Les quatre processus du scénario start peuvent fonctionner 24 heures en test accéléré sans corruption du magasin de session. ## 17. Étape 5 — Démarrage convivial et onboarding web ### 17.1 Objectif utilisateur Une installation neuve doit pouvoir être utilisée avec une seule commande : blink2video Cette commande devient strictement équivalente à : blink2video start Le premier écran utile doit être soit l’interface prête, soit la fenêtre de connexion Blink. L’utilisateur ne doit plus devoir découvrir puis taper manuellement blink2video login avant de pouvoir démarrer. ### 17.2 Périmètre 5.1. Faire converger l’absence d’argument et le verbe start vers une fonction unique. 5.2. Préserver le comportement explicite de --help et --version. 5.3. Exécuter un préflight avant tout spawn permanent. 5.4. Tester la validité de la session, pas seulement l’existence du fichier. 5.5. Garantir un seul propriétaire du flux d’authentification. 5.6. Extraire ou réutiliser LoginFlow sans démarrer toute l’application. 5.7. Lancer un serveur d’onboarding sur 127.0.0.1 uniquement. 5.8. Ouvrir automatiquement le navigateur sur une URL contenant un jeton éphémère. 5.9. Afficher l’URL si le navigateur ne peut pas être ouvert. 5.10. Gérer identifiants, refus, timeout et 2FA dans la page. 5.11. Fermer et nettoyer le flux après succès, annulation ou expiration. 5.12. Exécuter un mini-smoke read-only après authentification. 5.13. Afficher modules, réseaux et caméras trouvés sous une forme concise. 5.14. Accepter un compte valide sans Sync Module. 5.15. Ne lancer runtime.STANDARD qu’après succès du mini-smoke. 5.16. En cas d’échec, ne laisser aucun enfant ni verrou. 5.17. Conserver blink2video login comme solution CLI explicite et fallback. 5.18. Documenter le comportement en lancement manuel, autostart et environnement sans navigateur. ### 17.3 Contrat du préflight Le préflight renvoie un résultat structuré et stable : - authenticated : session réellement utilisable ; - account_reachable : homescreen obtenu ; - networks : nombre et identifiants non sensibles ; - sync_modules : nombre et noms d’affichage ; - cameras : nombre et noms d’affichage ; - cloud_only : compte valide sans module ; - warnings : capacités absentes mais non bloquantes ; - error : cause typée et message utilisateur ; - elapsed : temps par sous-phase. Le préflight est strictement read-only : - aucun armement ; - aucun direct ; - aucun manifeste USB ; - aucun téléchargement ; - aucun assemblage ; - aucune notification système. ### 17.4 Parcours nominal Session valide : 1. invocation ; 2. chargement et refresh éventuel ; 3. mini-smoke ; 4. synthèse « compte accessible, N caméras » ; 5. lancement de start ; 6. ouverture de l’interface normale selon la politique choisie. Session absente ou invalide : 1. invocation ; 2. échec contrôlé du test non interactif ; 3. démarrage du serveur local d’onboarding ; 4. ouverture de la page ; 5. saisie e-mail et mot de passe ; 6. affichage du champ 2FA si Blink le demande ; 7. sauvegarde atomique de la session ; 8. mini-smoke ; 9. affichage du résultat ; 10. lancement de start ; 11. redirection vers l’interface normale. ### 17.5 Cas d’échec - mauvais identifiants : rester sur la page, sans écrire de session invalide ; - mauvais code 2FA : afficher les tentatives restantes ; - timeout : fermer la session temporaire et proposer de recommencer ; - navigateur indisponible : imprimer l’URL et la commande CLI de secours ; - port occupé par blink2video : réutiliser l’instance si son endpoint de santé et sa version correspondent ; - port occupé par un autre programme : prendre un port d’onboarding libre et l’annoncer ; - réseau Blink indisponible : ne pas demander inutilement de nouveaux identifiants si le problème est typé comme transitoire ; - compte valide sans caméra : préflight réussi avec avertissement ; - mini-smoke en échec : ne pas démarrer les boucles et conserver le diagnostic. ### 17.6 Sécurité et confidentialité - bind explicite sur 127.0.0.1 et ::1 seulement si correctement contrôlé ; - jeton CSRF aléatoire lié au processus et expirant ; - méthode POST et Content-Type vérifiés ; - en-têtes Cache-Control: no-store ; - aucune valeur de formulaire dans les logs ou exceptions affichées ; - écrasement des références au mot de passe dès la fin du flux ; - nombre de tentatives et cadence bornés ; - une seule authentification active ; - comparaison de l’origine et de l’hôte local ; - arrêt du serveur d’onboarding lorsqu’il n’est plus utile ; - tests garantissant que le mot de passe n’atteint aucun fichier. ### 17.7 Tests de non-régression - T-E01-DEFAULT : sans argument et start produisent le même plan ; - T-E01-HELP : --help affiche l’aide sans préflight ni réseau ; - T-E01-VERSION : --version ne démarre rien ; - T-E01-VALID : session valide, aucun navigateur, mini-smoke une fois ; - T-E01-ABSENT : session absente, page ouverte ; - T-E01-EXPIRED : fichier présent mais session invalide, page ouverte ; - T-E01-2FA : transition identifiants vers code puis succès ; - T-E01-BAD-CREDENTIALS : diagnostic, aucune session écrite ; - T-E01-BAD-2FA : refus borné et aucun spawn ; - T-E01-CANCEL : annulation et nettoyage ; - T-E01-TIMEOUT : expiration et nettoyage ; - T-E01-NO-BROWSER : URL imprimée et parcours encore utilisable ; - T-E01-PORT-FOREIGN : autre port choisi, service étranger intact ; - T-E01-PORT-BLINK : instance reconnue et réutilisée ; - T-E01-CLOUD-ONLY : authentification valide sans hub ; - T-E01-EMPTY-ACCOUNT : avertissement sans crash ; - T-E01-SMOKE-FAIL : aucun processus permanent lancé ; - T-E01-CONCURRENT : deux invocations, un seul onboarding propriétaire ; - T-E01-NO-SECRET : mot de passe absent des fichiers, logs et erreurs ; - T-E01-AUTOSTART : comportement documenté sans console interactive ; - T-E01-BUNDLE : parcours identique depuis le bundle. ### 17.8 Mesures de performance et d’expérience Ajouter le benchmark P-12 Onboarding : - temps invocation → décision session valide/invalide ; - temps invocation → page locale prête ; - temps succès auth → résultat mini-smoke ; - temps résultat mini-smoke → processus standard lancés ; - temps total invocation → interface prête ; - nombre de créations de ClientSession ; - nombre d’appels Blink ; - nombre d’ouvertures du navigateur ; - RSS du processus d’onboarding ; - CPU au repos pendant l’attente du formulaire ; - nombre d’enfants et de verrous avant/après échec. Budgets initiaux : - coût local du préflight hors réseau inférieur à 200 ms en médiane ; - page d’onboarding prête en moins de 500 ms en p95 sur la machine de référence ; - exactement un test d’authentification par lancement ; - exactement un mini-smoke après authentification ; - aucun appel Blink dupliqué entre préflight et lancement lorsque le résultat peut être réutilisé ; - CPU d’attente inférieur à 1 % d’un cœur ; - RSS stable pendant dix minutes d’attente ; - sur session valide, surcoût de blink2video sans argument inférieur à 5 % par rapport à start avec le même préflight ; - zéro enfant, verrou ou serveur d’onboarding survivant après échec ; - les temps réseau sont rapportés séparément des temps locaux. ### 17.9 Compte rendu attendu À la clôture, le journal de l’étape 5 doit notamment indiquer : - choix de réutilisation ou extraction de LoginFlow ; - URL et stratégie de port retenues ; - politique autostart/headless ; - nature exacte du mini-smoke ; - appels API effectués ; - captures des quatre parcours : valide, login simple, login 2FA, échec ; - temps avant/après et time-to-ready ; - preuve de l’absence de secret persistant ; - preuve qu’aucun worker n’est lancé avant le succès. ### 17.10 Critère de sortie E-01 est livré lorsque le parcours sans argument fonctionne de bout en bout en source et en bundle, que le mini-smoke précède chaque lancement permanent et que tous les tests de sécurité, nettoyage et performance ci-dessus passent. ## 18. Étape 6 — Adaptateurs USB/cloud et performance des entrées-sorties ### 18.1 Périmètre 6.1. Encapsuler _local_storage dans un adaptateur. 6.2. Construire un snapshot USB frais. 6.3. Actualiser manifest_id pour chaque téléchargement. 6.4. Réserver seulement le hub concerné. 6.5. Laisser cloud et hubs distincts avancer en parallèle. 6.6. Paginer le cloud jusqu’à page vide ou cutoff. 6.7. Ajouter watermark durable et recouvrement. 6.8. Ne committer le watermark qu’après scan complet. 6.9. Respecter Retry-After et ajouter backoff avec jitter. 6.10. Reconnecter une session longue après erreur d’authentification typée. 6.11. Streamer les réponses par blocs vers .part. 6.12. Vérifier taille, signature et éventuellement hash/probe. 6.13. Publier une progression fondée sur les octets lorsque disponibles. 6.14. Contraindre la version de blinkpy et créer une tâche de mise à niveau. ### 18.2 Non-régression - ancien manifest_id remplacé ; - clip supprimé du manifeste absent du snapshot courant ; - direct et USB du même hub exclusifs ; - deux hubs distincts parallèles ; - cloud parallèle à l’USB ; - 600 clips cloud tous visités ; - panne à la page N : watermark inchangé ; - nouvel événement légèrement antérieur au watermark récupéré grâce au recouvrement ; - 429 respecte Retry-After ; - flux interrompu laisse uniquement un .part nettoyable ; - vidéo d’un gigaoctet traitée sans chargement complet en mémoire. ### 18.3 Performance - comparer P-06, P-07, P-09 et P-10 ; - mémoire supplémentaire maximale inférieure à 32 Mio pour un flux de 1 Gio, hors buffers du système ; - réduction d’au moins 90 % des appels cloud en régime stable sans nouveauté ; - débit local à moins de 5 % du débit brut du faux serveur ; - hubs distincts : gain parallèle mesurable sans appels supplémentaires ; - aucune boucle serrée après 429 ou erreur réseau. ### 18.4 Critère de sortie B-02, B-06, I-06, I-08, I-15 et I-17 sont fermés. Les mesures démontrent une mémoire bornée et une baisse nette du nombre d’appels API. ## 19. Étape 7 — Grammaire CLI et supervision ### 19.1 Périmètre 7.1. Définir une grammaire qui connaît l’arité des options. 7.2. Prévalider toute la composition avant tout effet. 7.3. Ajouter aux verbes les métadonnées composable et persistent. 7.4. Classer la persistance après parsing, jamais par recherche de chaîne. 7.5. Interdire ou définir clairement les compositions de stop, update, autostart, login et open. 7.6. Inscrire toute instance persistante, y compris un verbe local seul. 7.7. Enregistrer les enfants ponctuels. 7.8. Installer le nettoyage avant le premier spawn. 7.9. Employer groupes POSIX et Job Objects Windows. 7.10. Arrêter coopérativement puis forcer après délai. 7.11. Appeler wait pour chaque enfant. 7.12. Identifier PID, heure de création et exécutable. 7.13. Écrire les fiches atomiquement avec états starting, running et stopping. 7.14. Ne supprimer une fiche qu’après confirmation. 7.15. Collecter exactement une fois tous les codes de sortie. 7.16. Ajouter un endpoint de santé signé pour open. ### 19.2 Non-régression - caméra appelée watch, serve ou update ; - --loop 1 et --loop=1 équivalents ; - cadence nulle ou négative refusée ; - échec du second spawn nettoie le premier ; - Ctrl+C pendant une étape ponctuelle ; - enfant persistant mort immédiatement ; - parent tué, enfant encore vivant ; - petit-enfant ffmpeg ; - kill refusé : fiche conservée ; - PID réutilisé : aucun processus étranger tué ; - deux stop simultanés ; - open sur un autre service local : refus ; - bundle : chaque verbe réellement lancé depuis le bundle. ### 19.3 Performance - comparer P-01 et P-11 ; - parsing de 10 000 compositions synthétiques sans croissance anormale ; - stop p95 inférieur à 5 secondes pour des enfants coopératifs ; - stop forcé borné par le délai documenté ; - zéro processus survivant ; - surveillance des processus sous 1 % d’un cœur au repos ; - démarrage d’une composition sans régression supérieure à 10 % hors bootstrap retiré à l’étape suivante. ### 19.4 Critère de sortie I-11, I-12, I-13, I-14 et I-22 sont fermés. Les tests de processus passent sur Windows, Linux et macOS sans survivant. ## 20. Étape 8 — Découpage, bootstrap et maintenabilité ### 20.1 Périmètre 8.1. Extraire auth. 8.2. Extraire les modèles et adaptateurs de sources. 8.3. Extraire le registre. 8.4. Extraire le moteur de téléchargement. 8.5. Extraire la CLI et le superviseur. 8.6. Garder blink2video.py comme lanceur léger. 8.7. Retarder le bootstrap jusqu’au besoin réel du verbe. 8.8. Garantir help, version, open, stop et update sans dépendances Blink. 8.9. Verrouiller la création du venv. 8.10. Vérifier les dépendances depuis l’interpréteur cible. 8.11. Contraindre les versions testées. 8.12. Retirer code mort et règles d’ignore obsolètes. 8.13. Remplacer progressivement print par une journalisation structurée. ### 20.2 Non-régression - import de chaque module sans bootstrap implicite ; - stop fonctionnel avec aiohttp et blinkpy absents ; - help et version hors ligne ; - venv créé simultanément par deux processus sans corruption ; - bundle complet ; - documentation générée cohérente avec les parseurs. ### 20.3 Performance - comparer P-01 et P-10 ; - help, version, open et stop au moins 30 % plus rapides en mode source, ou justification basée sur le baseline ; - aucun import de ffmpeg/aiohttp/blinkpy pour les commandes qui n’en ont pas besoin ; - mémoire de démarrage réduite et mesurée. ### 20.4 Critère de sortie O-06 à O-10 sont fermés. Le point d’entrée ne contient plus de logique métier et chaque sous-système possède sa suite unitaire. ## 21. Étape 9 — Qualification finale et release ### 21.1 Matrice 9.1. Python 3.11 et 3.12. 9.2. Windows 10/11 x86-64. 9.3. Linux glibc pris en charge. 9.4. macOS Apple Silicon. 9.5. Sources et bundle PyInstaller. 9.6. CWD extérieur. 9.7. BLINK_HOME absent puis personnalisé. 9.8. Dossier programme en lecture seule. 9.9. 0, 1, 10 000 et 100 000 événements. 9.10. 600 clips cloud. 9.11. Backlog USB supérieur à dix minutes. 9.12. Multi-hubs, caméras renommées et noms collisionnants. 9.13. Crashs injectés à chaque frontière commit/rename. 9.14. Test réel opt-in avec secrets locaux, exclu de la CI. 9.15. Installation neuve sans session, puis onboarding simple et 2FA. 9.16. Session expirée, navigateur indisponible, annulation et timeout. 9.17. Démarrage interactif, headless et autostart sans ouverture répétée du navigateur. 9.18. Sans argument et start en source et dans le bundle, avec le même plan de lancement. ### 21.2 Critère de release - zéro anomalie B ouverte ; - zéro anomalie I sans décision explicite ; - suite complète verte sur la matrice ; - aucune donnée réelle touchée par la CI ; - zéro processus survivant ; - migrations testées sur une copie réaliste ; - rapport de performance avant/après publié ; - aucun benchmark critique au-delà du budget de régression ; - onboarding validé sans secret dans les fichiers ou journaux ; - aucun worker permanent lancé avant validation du mini-smoke ; - README français et anglais mis à jour. ## 22. Protocole de performance ### 22.1 Règles de mesure Les résultats avant et après doivent être pris : - sur la même machine et le même mode d’alimentation ; - avec les mêmes versions de Python et dépendances ; - avec les mêmes jeux de données générés et leur graine ; - sans réseau Internet, sauf test explicitement réel ; - avec la graine de référence 20260813 ; - après trois passages de chauffe ; - sur vingt passages pour les mesures courtes ; - sur au moins cinq passages pour les scénarios lourds ; - en conservant médiane, p95, minimum, maximum, écart-type et coefficient de variation ; - avec un intervalle de confiance bootstrap à 95 % pour toute amélioration revendiquée ; - avec time.perf_counter_ns pour le temps ; - avec mémoire RSS maximale de tout l’arbre de processus et, si utile, tracemalloc ; - avec nombre d’appels HTTP, octets lus/écrits et taille du stockage ; - en JSON brut plus un résumé Markdown. Une comparaison est invalide si l’environnement ou le jeu de données diffère. Elle est également invalide si le résultat fonctionnel est incorrect. Une campagne est rejouée si le coefficient de variation dépasse 5 % sans cause expliquée. Pour une zone non ciblée par l’étape, la médiane ne doit pas régresser de plus de 5 %, et le p95 ou la RSS de plus de 10 %. Une amélioration n’est déclarée que si son intervalle de confiance confirme le seuil annoncé. ### 22.2 Jeux de données | Jeu | Événements registre | Clips cloud | Taille médias | Usage | |---|---:|---:|---:|---| | S | 100 | 25 | 1–10 Mio | tests rapides | | M | 10 000 | 500 | 10–100 Mio | benchmark courant | | L | 100 000 | 5 000 | métadonnées seules | index et migration | | STREAM | 1 | 1 | 1 Gio synthétique | mémoire et débit | | RACE | 10 000 | — | — | 20 processus | Scénarios transversaux : | ID | Contenu | |---|---| | J0 | cloud vide, un clip, aucun hub, JSON null, tableau et entrées invalides | | J1 | HTTP 200/206/401/404/429/500, HTML, JSON, vide et MP4 tronqué | | J2 | deux sources, ordre permuté et événements aux bornes de tolérance | | J3 | 100 000 entrées, fichiers absents, exclusions et entrées héritées | | J4 | 20 processus, 1 000 mutations chacun et barrières de course | | J5 | 2 501 clips paginés puis 0, 1 et 100 nouveautés | | J6 | 32 clips de 32 Mio et flux de 512 Mio coupé à 25, 50 et 99 % | | J7 | arbre de processus profondeur 3 et erreurs injectées à chaque spawn | | J8 | échelles 1 000, 10 000, 50 000 et 100 000 pour la complexité | Les données doivent inclure : - deux sources ; - plusieurs hubs ; - noms Unicode et collisionnants ; - événements à ±2 secondes ; - IDs renumérotés ; - exclusions ; - fichiers absents, vides et tronqués ; - dates invalides isolées ; - plus de 20 pages cloud. ### 22.3 Benchmarks #### P-01 — Démarrage CLI Commandes : - sans argument ; - --help ; - --version ; - open sur port fermé ; - stop sans instance. Mesures : temps total, RSS maximale, modules lourds importés. #### P-02 — Écriture d’un lot Insérer 100, 1 000, 10 000 puis 100 000 événements. Mesures : temps total, p95 par commit, octets écrits, croissance du stockage. #### P-03 — Mise à jour unitaire Mettre à jour un clip et une tombstone dans un registre de taille croissante. Mesures : latence p50/p95 et octets écrits. #### P-04 — Chargement et vérification de l’acquis Charger le registre puis vérifier 1 000 clips, dont réindexés, absents et exclus. Mesures : temps, lectures disque et erreurs. #### P-05 — Rapprochement USB/cloud Rapprocher 5 000 clips cloud avec 100 000 événements locaux. Mesures : temps, RSS, doublons vrais, faux positifs, faux négatifs. #### P-06 — Scan cloud Simuler 600 clips la première fois, puis zéro, un et dix nouveaux clips. Mesures : appels HTTP, pages, octets de métadonnées et temps. #### P-07 — Streaming média Télécharger depuis un serveur local des médias de 10 Mio, 100 Mio et 1 Gio. Mesures : débit, RSS maximale, temps, taille/hash, nettoyage après coupure. #### P-08 — Concurrence registre/verrou Vingt processus effectuent des claims, commits et exclusions. Mesures : débit, attente p95, conflits attendus, pertes et erreurs. #### P-09 — Parallélisme des sources Comparer : - même hub USB contre direct ; - deux hubs USB distincts ; - USB contre cloud. Mesures : durée murale, chevauchements autorisés, erreurs System is busy. #### P-10 — Authentification enregistrée Simuler charge, refresh et sauvegarde par plusieurs clients. Mesures : latence, écritures disque, version finale et erreurs. #### P-11 — Supervision et arrêt Lancer parent, enfants et petits-enfants coopératifs ou récalcitrants. Mesures : temps d’arrêt p50/p95, appels système, CPU au repos, survivants. #### P-12 — Onboarding et time-to-ready Scénarios : - session valide ; - session absente ; - session expirée ; - login simple ; - login avec 2FA ; - mauvais identifiants ; - navigateur indisponible ; - mini-smoke en échec. Mesures : - temps local avant la décision d’authentification ; - temps avant disponibilité de la page ; - temps distant d’authentification, rapporté séparément ; - temps du mini-smoke ; - temps avant lancement des workers ; - temps total avant interface prête ; - nombre de sessions et d’appels Blink ; - ouvertures du navigateur ; - RSS et CPU pendant l’attente ; - enfants, serveurs et verrous survivants après chaque sortie. Le temps actuel de blink2video sans argument, qui affiche seulement l’aide, n’est pas comparé directement au nouveau time-to-ready. La comparaison pertinente est : - blink2video contre start avec le nouveau préflight ; - start avant/après hors temps réseau ; - commandes --help et --version avant/après, qui doivent rester légères. ### 22.4 Budgets d’acceptation initiaux Les seuils relatifs priment sur les seuils absolus tant que le baseline n’est pas enregistré. | Indicateur | Budget | |---|---| | Tests de correction | zéro échec | | Tombstones perdues | zéro | | Doublons sur jeux corrélés | zéro | | Faux MP4 acceptés | zéro | | Processus survivants après stop | zéro | | Course de verrou gagnée par plusieurs processus | zéro | | Régression d’une opération non ciblée | moins de 10 % | | Débit de streaming local | au moins 95 % du baseline brut | | RSS supplémentaire pour flux 1 Gio | moins de 32 Mio | | Appels cloud sans nouveauté | réduction d’au moins 90 % | | Écriture de 10 000 événements | gain d’au moins 5× | | Rapprochement à grande échelle | gain d’au moins 5× | | CPU des boucles au repos | moins de 1 % d’un cœur | | Arrêt coopératif p95 | moins de 5 secondes | | Page d’onboarding locale prête | moins de 500 ms en p95 | | Tests auth par lancement | exactement un | | Mini-smoke par lancement réussi | exactement un | | Ressources survivantes après onboarding en échec | zéro | Après l’étape 0, ces budgets seront complétés par les nombres absolus observés. ### 22.5 Fréquence des campagnes - à chaque commit fonctionnel : J0 à J2 et benchmarks courts touchés ; - avant la clôture d’une étape : tous les tests et benchmarks annoncés dans cette étape, sur la révision de départ et la révision candidate ; - sur chaque pull request : J0 à J6, avec comparaison automatique au baseline ; - chaque nuit : J3 à J8 et scénarios multi-processus lourds ; - avant release : matrice de l’étape 9, endurance d’au moins douze heures et campagne complète multi-OS ; - après toute variation supérieure aux seuils : répétition sur machine au repos et conservation des deux campagnes avec l’explication retenue. Le rapport d’une étape doit distinguer un gain mesuré, une absence de régression et une mesure non concluante. « Plus rapide » ne peut être écrit sans valeurs avant/après, unité, taille d’échantillon et intervalle de confiance. ## 23. Matrice de traçabilité | Anomalie | Étape | Tests principaux | Benchmarks | |---|---:|---|---| | B-01 | 1 | T-B01 | P-01 | | B-02 | 1 fermé, streaming/mémoire (O-01) toujours ouvert, voir 28.11 | T-B02-* | P-06, P-07 | | B-03 | 2 | T-B03-* | P-04, P-05 | | B-04 | 3 (fermé le 17/08, option légère, voir 28.8) | T-B04-* | P-02, P-03, P-08 | | B-05 | 3 (fermé le 17/08 hors T-B05-PID-REUSE, voir 28.8) | T-B05-* | P-08 | | B-06 | 6 (fermé le 17/08, voir 28.11) | T-B06-* | P-09 | | I-01 à I-03 | 4 (fermé le 17/08, voir 28.9) | auth/data-root | P-01, P-10 | | I-04, I-05 | 2 | ordre sources/réparation | P-04, P-05 | | I-06 à I-08 | 6 | manifests/pagination | P-06, P-09 | | I-09, I-10 | 1 | cloud-only/JSON | P-01 | | I-11, I-12 | I-12 fermé étape 1, I-11 fermé le 17/08 (voir 28.12) | grammaire/boucles | P-01 | | I-13, I-14 | fermés le 17/08 (voir 28.12) | supervision/stop | P-11 | | I-15 | 6 (fermé le 17/08 pour la validation ftyp, voir 28.11 ; probe complet non retenu) | intégrité/stream | P-07 | | I-16, I-20, I-21 | 2 | identité/matching | P-05 | | I-17 | 6 (fermé hors séquence le 17/08, voir 28.6) | erreurs transitoires | P-06, P-09 | | I-18, I-19 | 1 | compteurs/options | P-04 | | I-22 | 1 fermé (validation de port) ; endpoint de santé signé non retenu, voir 28.12 | port/health | P-01 | | E-01 | 5 (livré en version resserrée le 17/08, voir 28.10) | T-E01-* | P-01, P-10, P-12 (non mesurés) | | O-01 à O-05 | 3 et 6 (O-05 fermé hors séquence le 17/08, voir 28.6) | performance | P-02 à P-09 | | O-06 à O-10 | O-06/O-09 fermés le 17/08 (voir 28.13) ; O-07 (pin blinkpy), O-10 (gitignore verrous) non traités | imports/bundle | P-01 | ## 24. Décisions à prendre avant les étapes concernées ### D-01 — SQLite ou JSON renforcé Recommandation : SQLite WAL. La combinaison multi-processus, exclusions, watermarks, claims et index temporels justifie un stockage transactionnel. Décision requise avant l’étape 3. ### D-02 — Racine de données Définir si un --output relatif est résolu : - sous BLINK_HOME ; - ou sous le CWD comme exception explicite. Recommandation : sous la racine de données, avec possibilité d’un chemin absolu. Décision requise avant l’étape 4. ### D-03 — Identifiant stable de caméra Vérifier quels identifiants device_id/network_id sont disponibles dans les deux sources. Définir le fallback lorsque l’API ne fournit pas le même identifiant. Décision requise pendant l’étape 2. ### D-04 — Propriétaire des tokens Choisir entre : - un processus propriétaire de la session ; - plusieurs clients coordonnés par génération ; - un petit broker local. La solution la plus simple compatible avec blinkpy doit être éprouvée par un test de rotation de refresh token. Décision requise avant l’étape 4. ### D-05 — Validation vidéo Choisir le niveau : - statut + taille + signature MP4 ; - ou probe ffmpeg systématique. Recommandation : validation légère au téléchargement, probe complet avant inscription définitive si le coût P-07 reste acceptable. Décision requise avant l’étape 6. ### D-06 — Propriétaire de l’onboarding et politique headless Décider comment le parcours se comporte pour un lancement manuel, un bundle, un terminal sans navigateur et l’autostart du système. Recommandation : - faire converger sans argument et start vers un coordinateur unique ; - n’autoriser qu’un propriétaire du préflight et de l’onboarding ; - ouvrir le navigateur une seule fois pour un lancement interactif ; - en autostart/headless, servir la page sur loopback et publier une URL par le canal disponible, sans boucle d’ouvertures de navigateur ; - réutiliser une instance existante uniquement après vérification signée de son endpoint de santé et de sa version ; sinon choisir un port éphémère libre ; - implémenter le mini-smoke comme service de domaine partagé avec list, et non comme sous-processus CLI qui rouvrirait une session et fausserait les mesures. Décision requise avant l’étape 5. ## 25. Stratégie de migration et retour arrière Toute migration d’état doit : 1. refuser un schéma source inconnu ; 2. créer une sauvegarde nommée et datée ; 3. écrire la cible dans un nouveau fichier ; 4. valider le nombre d’entrées, tombstones et chemins ; 5. remplacer atomiquement seulement après validation ; 6. être idempotente ; 7. conserver la sauvegarde jusqu’à une release suivante ; 8. fournir un outil de diagnostic ou d’export ; 9. ne jamais déplacer ni supprimer un média pendant la migration du registre. Le rollback de code doit être documenté si l’ancien programme ne sait pas lire le nouveau format. Un export JSON version 1 en lecture seule peut servir de pont, sans redevenir la source de vérité. ## 26. Définition globale de terminé Le chantier est terminé lorsque : - chaque anomalie de ce rapport possède un test ou une décision documentée ; - tous les tests sont isolés des données réelles ; - la CI couvre sources et bundle sur les plateformes prises en charge ; - les migrations ont été testées sur des copies réalistes ; - les processus ne survivent jamais à stop ; - les fichiers invalides ne sont jamais inscrits comme acquis ; - les exclusions sont préservées sous concurrence et après crash ; - les appels cloud et la mémoire de téléchargement ont diminué selon les budgets ; - les benchmarks avant/après sont archivés et reproductibles ; - blink2video sans argument et blink2video start exécutent le même parcours ; - une session absente ou invalide peut être établie depuis la page locale ; - le mini-smoke read-only réussit avant tout lancement permanent ; - l’onboarding ne laisse ni secret, ni serveur, ni processus après sa sortie ; - les deux README reflètent le comportement final ; - un test réel opt-in confirme auth, USB et cloud sans publier de secret. ## 27. Ordre de démarrage recommandé Le prochain travail doit commencer strictement ainsi : 1. ouvrir l’étape 0 uniquement ; 2. écrire T-B01 à T-B06, T-E01 et les tests JSON/CLI/processus ; 3. enregistrer P-01 à P-12 sur la révision auditée ; 4. publier le premier tableau baseline ; 5. seulement ensuite commencer l’étape 1 ; 6. ne pas mélanger refactor structurel et correction P0 dans le même changement ; 7. fermer chaque étape avec ses résultats de tests et de performance. ## 28. Journal d’exécution des étapes Ce journal est la source de vérité de l’avancement. Une étape ne peut passer à « terminée » que si un compte rendu détaillé, conforme aux sections 11.1 à 11.3, est ajouté sous son entrée. Un résultat défavorable ou non concluant doit être enregistré ; il ne doit jamais être remplacé par une appréciation sans mesure. État au 13 août 2026 : l’audit et le plan ont été rédigés, mais aucune étape de correction n’a commencé. Aucun benchmark de référence complet n’a donc été enregistré et aucune amélioration de performance n’est revendiquée. | Étape | Intitulé | Statut initial | Tests à consigner | Mesures à consigner | |---:|---|---|---|---| | 0 | Banc de sécurité et baseline | en cours — filet installé, baseline non qualifiée | historique, T-B01 à T-B06, T-E01, JSON/CLI/processus | P-01 à P-12 baseline | | 1 | Correctifs de sûreté immédiats | implémentée localement — CI multi-OS à confirmer | section 13.2 | P-01, P-06, P-07 | | 2 | Identité, déduplication et réparation | implémentée localement — CI multi-OS à confirmer | 24 tests dédiés, section 28.5 | P-04 et P-05 rapides | | 3 | Persistance transactionnelle et verrous | implémentée localement (option légère JSON+verrou atomique, D-01 non suivie, voir 28.8) — CI multi-OS à confirmer | section 15.2 | P-02, P-03, P-08, P-09 (non mesurés formellement) | | 4 | Racine de données et authentification | implémentée localement (voir 28.9) — CI multi-OS à confirmer | section 16.2 | P-01, P-10 (non mesurés formellement) | | 5 | Démarrage convivial et onboarding web | implémentée en version resserrée (voir 28.10) — CI multi-OS à confirmer | T-E01-* | P-01, P-10, P-12 (non mesurés) | | 6 | Adaptateurs USB/cloud et entrées-sorties | partielle : B-06/I-15 fermés en périmètre resserré (voir 28.11), reste du périmètre (18.1) non commencé | section 18.2 | P-06, P-07, P-09, P-10 (non mesurés) | | 7 | Grammaire CLI et supervision | partielle : I-11/I-13/I-14 fermés en périmètre resserré (voir 28.12) | section 19.2 | P-01, P-08, P-11 (non mesurés) | | 8 | Refactor modulaire et optimisation du démarrage | implémentée en périmètre complet (8.1-8.9, 8.12), 8.10/8.11/8.13 différés (voir 28.13) — CI multi-OS à confirmer | section 20.2 | P-01, P-10 (non mesurés) | | 9 | Qualification finale et release | partielle : volumétrie 100 000 événements et README vérifiés localement, matrice CI déclenchée (voir 28.14) — reste hors de portée d'une session locale | matrice 21.1 | P-02 à P-05 mesurés à 100k | ### 28.1 Travail préparatoire P-00 — Sauvegarde et checkpoint Statut : **terminé le 13 août 2026**. Ce travail prépare l’ouverture de l’étape 0 ; il ne constitue ni sa baseline de performance ni sa clôture. Révision source au départ : `58f5566`, tag `v0.5.5`, branche `main` alignée sur `origin/main`. Le seul fichier de travail initial était le présent rapport, encore non suivi. #### Travail effectué 1. lecture de la fiche d’instance et comparaison des PID avec les processus OS ; 2. vérification que le parent 5080 et ses cinq enfants Python dataient tous de 13:35, conformément à la fiche ; 3. arrêt applicatif obtenu avec un code de sortie nul ; 4. copie locale des originaux, exclusions et états vers `C:\Users\Nico\Documents\blink-backups\2026-08-13_pre-remediation_v0.5.5` ; 5. détection par empreinte d’une modification concurrente de `.blink_passages.json` : une nouvelle composition avait démarré à 16:01, avec le parent 7424 et cinq enfants ; 6. inventaire puis retrait temporaire des deux raccourcis de démarrage `blink2video-start.lnk` et `blink2video-serve.lnk` ; 7. arrêt de la seconde composition, resynchronisation de deux fichiers pour 14,20 Mio, puis nouvelle vérification intégrale ; 8. restauration à l’identique des deux raccourcis d’autostart ; leur commande a été contrôlée et ils reprendront effet à la prochaine ouverture de session ; 9. création de la branche `audit-remediation` depuis `58f5566` ; 10. préparation du checkpoint Git `pre-remediation-2026-08-13`, destiné à pointer le commit contenant ce rapport, puis publication de la branche et du tag sur `origin` ; 11. maintien de l’application à l’arrêt après la sauvegarde afin de commencer l’étape 0 depuis un état maîtrisé. Le secret `blink_auth.json` n’a été copié ni dans le snapshot non chiffré ni dans Git. Les répertoires `Blink_Normalized`, `Blink_Daily`, `Blink_Weekly` et `Blink_Monthly`, qui représentaient 3 059 528 467 octets, ont été omis car ils sont dérivés et régénérables. Les originaux et médias exclus ont été conservés. #### Vérifications et mesures | Vérification | Résultat | Verdict | |---|---:|---| | Processus correspondant à la première fiche | 6/6 | identité confirmée avant arrêt | | Code de sortie du premier arrêt | 0 | réussi | | Relance concurrente détectée | 1 composition, 6 processus | snapshot initial invalidé puis repris | | Raccourcis d’autostart avant/après | 2/2 | configuration restaurée | | Fichiers sauvegardés hors présent README | 90 | conforme à l’inventaire | | Volume logique sauvegardé | 582 356 434 octets | conforme source/copie | | Empreintes `Blink_Clips` | 67/67 identiques | réussi | | Empreintes `Blink_Excluded` | 19/19 identiques | réussi | | Empreintes des états et du journal | 4/4 identiques | réussi | | Empreintes totales | 90/90 identiques | réussi | | Copies non chiffrées de `blink_auth.json` créées | 0 | réussi | | Fiches d’instance après préparation | 0 | application arrêtée | | Espace libre restant sur `C:` après copie | 3 888 934 912 octets | suffisant pour le chantier, à surveiller | Le premier contrôle des états avait donné 3 empreintes identiques sur 4. Ce résultat n’a pas été masqué : il a permis de repérer la relance, d’invalider la première photographie et de refaire le contrôle dans de bonnes conditions. #### Limite de la sauvegarde Le snapshot se trouve sur le même volume physique `C:`. Il protège d’une régression logicielle ou d’une migration fautive, mais pas d’une panne du disque. Une copie supplémentaire sur un support distinct et chiffré reste recommandée. Le fichier `BACKUP-README.md` placé dans le snapshot documente son contenu, ses exclusions et les précautions de restauration. #### Verdict Préparation acceptée : le code de production reste à la révision auditée, les données irremplaçables disposent d’une copie vérifiée, les secrets restent hors Git et l’étape 0 peut commencer. Aucun gain de performance n’est revendiqué à ce stade. ### 28.2 Étape 0 — Ouverture du banc de sécurité et de la baseline Statut : **en cours, à reprendre avant l’étape 1**. Début : 13 août 2026. Révision de départ du chantier : `4f23039`. Révision de production mesurée : `58f5566` ; aucun fichier de production n’a été modifié pendant cette ouverture. #### Travail effectué - ajout de `test_blink2video_audit.py`, suite `unittest` isolée portant les IDs du rapport ; - usage systématique de `BLINK_BOOTSTRAP=none`, d’un `BLINK_HOME` temporaire et d’un CWD différent ; - faux Blink, Sync, Clip, réponse HTTP, session, horloge/sommeil et appels système ; - courses multi-processus déterministes pour le verrou et tests de processus sans viser une instance réelle ; - ajout d’une garde prouvant qu’un `unexpected success` rend la CI rouge, et de quatre spécifications E-01 ignorées explicitement tant que le serveur d’onboarding de l’étape 5 n’existe pas ; - durcissement de `tests.py` : sandbox posée avant les imports, commande unique source/bundle, environnement hermétique pour tous les enfants, ports dynamiques, vérification de la version et du BLINK_HOME servis, contrôle de tous les enfants avant/après stop ; - remplacement de la notification réelle de la CI par une simulation ; le contrôle visuel reste uniquement dans `smoketest.py` manuel ; - ajout dans `.github/workflows/ci.yml` d’une matrice de caractérisation Python 3.11/3.12 sur Windows, Linux et macOS ; - ajout de `benchmarks/stage0_baseline.py`, de sa documentation et de sorties JSON/Markdown conservant environnement, révision, empreintes des fichiers de production, échantillons, médiane, p95, coefficient de variation et IC 95 % ; - ajout des profils `quick`, `full` et `safe`, du filtrage `--only` et d’une commande de comparaison ; - distinction entre révision documentaire et HEAD/empreintes réellement exécutés ; noms de campagne horodatés, écriture sans écrasement et refus des comparaisons dont profil, protocole, environnement, scénarios ou validité diffèrent ; - ajout d’un délai hors chronométrage entre les 120 créations de processus de P-01 `quick`, avec décompte explicite dans le résultat ; - serveur média HTTP loopback pour P-07, générateurs déterministes de registres et manifests, charge multi-processus P-08, fausses E/S P-09, faux tokens P-10 et arbre de processus local P-11 ; - conservation explicite des scénarios P-12 non disponibles, sans leur donner artificiellement une durée nulle. #### Bilan des tests | Suite/scénario | Résultat | Durée observée | Verdict | |---|---:|---:|---| | Compilation de `tests.py`, `test_blink2video_audit.py` et du benchmark | 3/3 | 0,5 s | réussi | | `docs.py --check` | 2 README à jour | 4,9 s | réussi | | Suite dédiée finale | 33 tests : 7 réussis, 22 échecs attendus, 4 ignorés E-01 | 1,717 s | réussi comme caractérisation | | Verbes, notification simulée et relance de la suite historique | 20 assertions | mesure ciblée agent | réussi | | Composition, endpoint, enfants et stop | 9 assertions | 9,3 s | réussi | | `tests.py` complet, bac à sable et chaîne ffmpeg | 70 contrôles observés | 25,4 s | réussi, zéro processus/fiche survivant | | Validation YAML du workflow CI | 1 fichier | < 1 s | réussi | | Ruff sur les trois fichiers Python de l’étape 0 | 0 erreur | 0,3 s | réussi | | `git diff --check` | 0 erreur | < 1 s | réussi | | Bundle réel sur les trois OS | non exécuté localement | — | confié à la CI, restant | Les 22 échecs attendus reproduisent B-01 à B-06 et les défauts I-01, I-05, I-07, I-09 à I-14 et I-16 sélectionnés. `unittest` considère tout succès inattendu comme un échec : lorsqu’un défaut sera corrigé, son marqueur devra être retiré dans le même changement. Les quatre tests E-01 ignorés décrivent login web simple, 2FA, annulation/timeout/navigateur absent et ordre mini-smoke/workers. #### Incident de mesure et décision de sécurité La campagne `quick-run-a` a exécuté le protocole 3 chauffes, 20 mesures courtes et 5 lourdes. Pendant P-01, les 115 créations de processus ont amené `MsMpEng` à 100 % selon le constat de l’utilisateur. Le processus de benchmark avait terminé lorsque son arrêt a été vérifié. L’antivirus n’a pas été arrêté ni désactivé par le chantier. L’utilisateur a ensuite indiqué avoir désactivé la protection en temps réel ; aucune campagne de performance n’a été lancée dans cet état, car ses résultats ne représenteraient pas le fonctionnement normal. La réactivation immédiate de la protection a été recommandée. Les résultats bruts sont conservés dans `benchmarks/results/stage0-baseline-20260813-quick-run-a.*`, avec le statut `invalid`. Tous ses temps et mesures de ressources sont interdits pour une comparaison avant/après. Ses constats fonctionnels restent utiles : - P-04 : 100 erreurs sur 1 000 vérifications, toutes liées aux 100 clips renumérotés ; - P-08 : 8 gagnants sur 8 lors de chacun des 5 essais de verrou ; - P-09 : absence de chevauchement des deux hubs et du cloud simulés ; - P-10 : erreurs concurrentes par essai `[0, 1, 0, 1, 7]` ; - P-11 : aucun survivant sur les cinq arbres locaux coopératifs ; - P-12 : sans argument observé comme `help_and_exit`, onboarding indisponible. Pour poursuivre sans créer de rafale de processus, trois campagnes internes A, B et C ont mesuré P-02 à P-07, P-09 et P-10. Le rapport détaillé est dans `benchmarks/results/stage0-stability-20260813.md`. | Mesure | Campagne B | Campagne C | Delta | Seuil | Verdict | |---|---:|---:|---:|---:|---| | P-02, 100 entrées | 2,677 ms | 2,857 ms | +6,72 % | < 10 % | stable | | P-02, 1 000 entrées | 15,233 ms | 29,975 ms | +96,78 % | < 10 % | instable | | P-02, 5 000 entrées | 73,521 ms | 94,454 ms | +28,47 % | < 10 % | instable | | P-03, mise à jour sur 5 000 | 78,877 ms | 112,758 ms | +42,95 % | < 10 % | instable | | P-04, 1 000 vérifications | 85,466 ms | 94,525 ms | +10,60 % | < 10 % | instable | | P-05, 5 000 × 500 | 48,035 ms | 55,529 ms | +15,60 % | < 10 % | instable | | P-06, 600 clips | 1,424 ms | 1,865 ms | +30,99 % | < 10 % | instable | | P-07, 1 Mio | 6,996 ms | 5,907 ms | -15,56 % | < 10 % | instable | | P-07, 8 Mio | 27,136 ms | 24,382 ms | -10,15 % | < 10 % | instable | | P-09, deux hubs et cloud | 93,850 ms | 93,937 ms | +0,09 % | < 10 % | stable | | P-10, huit écrivains | 5,729 ms | 5,315 ms | -7,22 % | < 10 % | stable | | P-10, sauvegarde série | 0,837 ms | 0,764 ms | -8,69 % | < 10 % | stable | Seuls 4 scénarios sur 12 sont stables entre B et C. Aucune valeur n’est donc promue au rang de baseline qualifiante et aucune amélioration de performance n’est revendiquée. #### Écarts au plan et reste à faire - P-07 a été borné à 8 Mio au lieu de 1 Gio par prudence lors de la conception initiale ; le profil complet garde le scénario 1 Gio ; - P-01, P-08, P-11 et la partie mesurable de P-12 doivent être rejoués sans saturation antivirus, mais sans désactiver la protection ; - le faux navigateur et le serveur d’onboarding pilotable restent différés, l’API cible n’existant pas avant l’étape 5 ; - la preuve systématique d’absence de secrets dans tous les journaux du futur onboarding reste à écrire à l’étape 5 ; - les bundles et la matrice multi-OS doivent passer ; - deux campagnes successives qualifiantes doivent rester sous 10 % d’écart. #### Verdict **À reprendre, étape 0 maintenue en cours.** Le filet de caractérisation et l’outil de mesure sont installés, mais les critères de sortie ne sont pas tous satisfaits. À la demande de l’utilisateur, qui a donné priorité aux correctifs avant épuisement du quota hebdomadaire, l’étape 1 a néanmoins été ouverte avec des points avant/après P-01, P-06 et P-07 strictement ciblés. Cette décision ne qualifie pas la baseline globale et n’autorise aucune revendication de gain sur les scénarios non rejoués. ### 28.3 Bloc à ajouter lors de la réalisation de chaque étape Chaque étape reçoit un sous-titre daté et conserve au minimum : 1. statut, dates, révisions avant/après et environnement ; 2. travail effectivement réalisé, fichier par fichier ; 3. décisions prises et écarts au plan ; 4. tests ajoutés, résultats avant/après, durée et éventuels échecs ; 5. mesures brutes et synthèse baseline/après/delta/IC 95 % ; 6. comparaison aux budgets et analyse des régressions ; 7. anomalies fermées, risques restants et rollback disponible ; 8. verdict explicite : acceptée, à reprendre ou bloquée. Le statut du tableau ci-dessus et le compte rendu détaillé doivent être modifiés dans le même changement que la clôture de l’étape. Les artefacts JSON bruts de tests et benchmarks sont référencés depuis le compte rendu au lieu d’être recopiés ou résumés sans preuve. ### 28.4 Étape 1 — Correctifs de sûreté immédiats Statut : **implémentée et validée localement le 13 août 2026 ; qualification CI sur trois OS encore requise pour la clôture formelle**. Révision de départ : `6c283d8`. Révision portant le code et les tests : `38269ca`. Protection en temps réel Microsoft Defender : active pendant les mesures. Checkpoint Git du journal : `4f67817`. #### Travail effectué - `blink2video.py` : - `traiter_cloud` retourne toujours un `CloudResult` homogène ; - les compteurs `downloaded`, `adopted`, `skipped` et `failed` sont séparés ; - un cloud vide ou un filtre sans résultat est un succès à quatre compteurs nuls ; - seuls les statuts HTTP 2xx sont acceptés comme média ; un corps HTML/JSON de 404, 429 ou 500 n’est plus enregistré comme MP4 ; - le corps d’erreur est consommé pour permettre la réutilisation de la connexion, puis tout fichier partiel est supprimé ; - `--since 0` désigne maintenant le jour courant ; `None` conserve explicitement trente jours ; - les racines et entrées JSON cloud/registre/session sont validées avant tout accès ; une entrée corrompue est isolée sans abattre le passage ; - `login` et la source cloud fonctionnent sans Sync Module ; la source USB explique au contraire qu’un module est requis ; - les erreurs par clip sont catégorisées (`HTTP`, `réseau`, `données`), sans afficher de trace ni de contenu distant ; - `--overwrite` est défini comme un retéléchargement forcé de tous les clips visibles, y compris ceux déjà acquis ; - `open --port` emploie le validateur partagé. - `runtime.py` : ajout de validateurs argparse communs pour une cadence strictement positive, un nombre de jours positif ou nul et un port compris entre 1 et 65535. - `serve.py` et `watch.py` : validation partagée du port ; toutes les boucles utilisant `runtime.ajouter_boucle` refusent désormais zéro et les valeurs négatives. - `test_blink2video_audit.py` : scénarios détaillés HTTP 404/429/500, JSON `[]`/`null`/entrées malformées, fenêtres temporelles, cloud vide, cloud sans hub, compteurs et ports. Dix-huit marqueurs `expectedFailure` ont été retirés seulement après succès réel des correctifs correspondants. - `test_runtime_validation.py` : neuf tests unitaires des bornes et parseurs. - `.github/workflows/ci.yml` : les deux suites isolées passent dans la matrice Python 3.11/3.12 sur Windows, Linux et macOS. - `benchmarks/stage0_baseline.py` : correction du nom des artefacts ; le point présent dans l’horodatage UTC ne tronque plus profil et libellé. #### Décisions et écarts au plan - aucun nouveau drapeau `--force` n’est ajouté : `--overwrite` porte cette sémantique afin de ne pas créer deux synonymes ; - la réparation automatique d’un média cloud absent du disque reste I-05 et appartient à l’étape 2 ; elle n’est pas présentée comme corrigée ici ; - la pagination au-delà de `stop=20`, le streaming par blocs et la validation MP4 approfondie restent à l’étape 6 ; - la baseline globale de l’étape 0 reste non qualifiée. Les mesures ci-dessous sont des campagnes partielles comparables, destinées uniquement à contrôler ce lot. #### Tests et vérifications | Contrôle | Résultat | Durée | Verdict | |---|---:|---:|---| | Suites isolées combinées | 54 tests : 38 réussis, 12 défauts futurs attendus, 4 E-01 ignorés | 1,690 s | réussi | | Chaîne historique `tests.py` | 70 contrôles observés | 23,5 s | réussi | | Compilation des 7 fichiers Python touchés/outillage | 7/7 | 0,5 s | réussi | | Ruff, règles d’erreur hors dettes préexistantes E402/F401 | 0 erreur | 0,3 s | réussi | | Cohérence des README | 2/2 | 0,4 s | réussi | | Validation YAML CI | 1 workflow | 0,4 s | réussi | | Nettoyage après tests | 0 processus Python, 0 fiche `.blink_run` | < 1 s | réussi | Tous ces tests sont isolés par `BLINK_BOOTSTRAP=none`, `BLINK_HOME` temporaire, faux Blink/réponses HTTP et loopback local. Aucun compte, identifiant, navigateur ou accès Internet réel n’a été utilisé. #### Mesures P-01 — démarrage CLI prudent Profil `safe`, deux mesures par scénario, aucun échauffement, un contrôle sémantique et une pause de 500 ms entre créations : 15 processus par campagne. | Scénario | Avant | Après | Delta | Budget | Verdict | |---|---:|---:|---:|---:|---| | sans argument | 624,285 ms | 669,744 ms | +7,28 % | < +10 % | conforme | | `--help` | 613,738 ms | 663,346 ms | +8,08 % | < +10 % | conforme | | `--version` | 626,055 ms | 658,677 ms | +5,21 % | < +10 % | conforme | | `open` sur port fermé | 2 618,987 ms | 2 679,289 ms | +2,30 % | < +10 % | conforme | | `stop` sans instance | 706,102 ms | 684,048 ms | -3,12 % | < +10 % | conforme | La campagne prudente prouve l’absence de régression au seuil de l’étape 1 ; son faible effectif ne permet pas d’attribuer les variations à une optimisation. #### Mesures P-06 — parsing du manifeste cloud simulé Profil `quick`, cinq mesures par scénario après trois échauffements, sans réseau. | Scénario | Avant | Après | Delta | Verdict | |---|---:|---:|---:|---| | première passe, 600 entrées | 9,928 ms | 5,472 ms | -44,89 % | gain observé, non attribuable | | seconde passe, 0 nouvelle | 5,488 ms | 5,590 ms | +1,87 % | absence de régression | | seconde passe, 1 nouvelle | 9,670 ms | 5,470 ms | -43,43 % | gain observé, non attribuable | | seconde passe, 10 nouvelles | 7,096 ms | 5,784 ms | -18,49 % | gain observé, non attribuable | Le faux fournisseur rend l’inventaire complet en un appel. Il mesure le parseur de production mais pas la pagination du fournisseur ; `stop=20` reste donc un écart connu et P-06 n’est pas promu comme baseline qualifiante. #### Mesures P-07 — téléchargement HTTP loopback Profil `quick`, cinq mesures après trois échauffements. | Taille | Avant | Après | Delta temps | Débit avant | Débit après | Mémoire Python après | |---:|---:|---:|---:|---:|---:|---:| | 1 Mio | 6,991 ms | 5,799 ms | -17,05 % | 143,0 Mio/s | 172,4 Mio/s | 2,09 Mio | | 8 Mio | 29,117 ms | 24,693 ms | -15,20 % | 274,8 Mio/s | 324,0 Mio/s | 16,10 Mio | Le débit observé augmente et la mémoire reste pratiquement inchangée (+8 260 octets sur le scénario 8 Mio). Comme l’étape 1 ne change pas encore le modèle d’entrée-sortie, ces valeurs satisfont le budget mais ne sont pas revendiquées comme un gain structurel ; le streaming arrive à l’étape 6. Artefacts bruts : - avant P-06/P-07 : `stage0-baseline-20260813T155541.{json,md}` ; - avant P-01 : `stage0-baseline-20260813T155617.{json,md}` ; - après P-06/P-07 : `stage0-baseline-20260813T160748.055435Z-quick-stage1-after.{json,md}` ; - après P-01 : `stage0-baseline-20260813T160757.129162Z-safe-stage1-after.{json,md}`. #### Verdict et retour arrière B-01 et la partie statut HTTP de B-02 sont fermés. I-07, I-09, I-10, I-12, I-18 et la validation de port I-22 sont également fermés dans le périmètre de cette étape. Tous les budgets locaux sont respectés et aucune régression de la suite historique n’est observée. **Verdict local : acceptée. Verdict formel : en attente de la matrice CI sur les trois OS.** Le retour arrière du code consiste à revenir de `38269ca` à `6c283d8` ; les artefacts avant/après permettent alors de reproduire la décision. ### 28.5 Étape 2 — Identité, déduplication et réparation Statut : **implémentée et validée localement le 13 août 2026 ; qualification CI sur trois OS encore requise pour la clôture formelle**. Révision de départ : `4f67817`. Protection en temps réel Microsoft Defender : active pendant les tests et mesures. Aucun compte Blink ni accès réseau réel. Révision portant le code, les tests et les mesures : `fc254c0`. #### Travail effectué - le registre migre de v1 vers v2 de façon additive et idempotente ; anciennes clés et archives restent lisibles, et le JSON v1 d'origine est conservé dans `.blink_download_state.v1.backup.json` avant la première écriture v2 ; - identité source et corrélation inter-source sont séparées : l'USB ignore l'ID renuméroté du manifeste, le cloud conserve son ID distant, et réseau/module/ caméra stable empêchent les collisions multi-réseaux et multi-hubs ; - le nom API brut sert au repli d'identité ; `safe_name` ne sert plus qu'aux chemins. Les composants Windows réservés, séparateurs et longueurs sont neutralisés, puis une empreinte SHA-256 courte distingue les valeurs qui deviendraient identiques après assainissement ; - `entry.path` est l'autorité pour un acquis : chemin relatif obligatoire, contenu sous `output`, fichier ordinaire, taille non nulle et taille enregistrée cohérente. Un média absent/tronqué est réparé ; une tombstone reste prioritaire, y compris avec `--overwrite` ; - la fusion JSON force une version monotone et empêche une copie downloader périmée d'annuler une exclusion concurrente. La transactionnalité complète et le remplacement du verrou restent volontairement à l'étape 3 ; - le rapprochement USB/cloud est symétrique, bijectif et de cardinalité maximale. Il emploie un index temporel `bisect` et Hopcroft-Karp itératif ; le passage USB apparie désormais le lot entier au registre et consomme chaque match une fois ; - un index éphémère du registre et de ses chemins accélère les vérifications. Il n'est jamais sérialisé et est invalidé par les helpers d'écriture ; - le cloud utilise le `network_id` propre à chaque clip et le filtre caméra compare le nom original, de sorte que `A/B` ne sélectionne pas `A_B` ; - `.github/workflows/ci.yml` exécute désormais `test_stage2_identity.py` dans la matrice Windows/Linux/macOS × Python 3.11/3.12. Limite documentée : blinkpy 0.25 n'expose pas toujours `device_id` ni `network_id` sur les objets USB. Dans ce cas, la corrélation se replie sur le nom API original ; un renommage USB ne peut donc pas être reconnu avec certitude. #### Tests et vérifications | Contrôle | Résultat | Durée | Verdict | |---|---:|---:|---| | Suite dédiée étape 2 | 27/27 | 0,198 s | réussi | | Suites isolées combinées | 81 tests : 68 réussis, 9 défauts futurs attendus, 4 E-01 ignorés | 1,926 s | réussi | | Chaîne historique `tests.py` | 70 contrôles observés | 23,5 s | réussi | | Compilation Python et `git diff --check` | 0 erreur | < 1 s | réussi | Les 27 contrats couvrent réindexation, ordre USB/cloud, cardinalité maximale, deux événements proches, caméra renommée avec ID stable, noms collisionnants, multi-réseaux/hubs, médias absents ou tronqués, exclusions avec overwrite, chemins hostiles, migration/restauration, version monotone, filtres et limites Windows. Tous emploient `BLINK_HOME` temporaire et de faux objets Blink. #### Mesures P-04 — vérification d'un acquis Profil `quick`, 1 000 vérifications dont 100 réindexations et 100 exclusions, vingt mesures après trois chauffes. | Mesure | Avant | Après | Delta | Verdict | |---|---:|---:|---:|---| | médiane | 87,894 ms | 82,810 ms | -5,78 % | gain observé | | p95 | 128,844 ms | 87,242 ms | -32,29 % | gain observé | | erreurs | 100/1 000 | 0/1 000 | -100 % | corrigé | La médiane est 1,06× plus rapide tout en réparant les 100 faux négatifs de réindexation. Le CV final de 2,83 % rend cette campagne locale stable. #### Mesures P-05 — rapprochement USB/cloud Profil `quick`, 5 000 événements locaux × 500 cloud, cinq mesures après trois chauffes. | Mesure | Avant | Après | Delta | Verdict | |---|---:|---:|---:|---| | médiane | 43,489 ms | 5,774 ms | -86,72 % | 7,53× plus rapide | | doublons / inédits | 250 / 250 | 250 / 250 | identique | exact | | pic heap Python | 48 096 o | 591 276 o | +543 180 o | coût accepté de l'index | Le gain dépasse l'objectif initial de cinq fois sur le volume `quick` et la complexité devient `O((N+M) log N + E)` hors appariement, au lieu du produit complet `N×M`. La dispersion finale est notable (CV 22,60 %), mais l'intervalle reste très inférieur à la référence ; cette campagne partielle n'est pas extrapolée au profil 100 000 × 5 000 et ne remplace pas la qualification finale. Artefacts bruts conservés : - avant P-04/P-05 : `stage0-baseline-20260813T161544.863841Z-quick-stage2-before.{json,md}` ; - après P-04/P-05 sur le code final : `stage0-baseline-20260813T170234.187443Z-quick-stage2-final-reviewed-indexed.{json,md}`. #### Verdict et retour arrière B-03, I-04, I-05, I-16, I-20 et I-21 sont fermés dans le périmètre local de l'étape. B-04 n'est fermé que pour la conservation d'une tombstone lors de la fusion ; ses courses interprocessus et B-05 restent à traiter à l'étape 3. **Verdict local : acceptée. Verdict formel : en attente de la matrice CI sur les trois OS.** Le retour arrière s'effectue vers `4f67817`; le registre v1 original reste restaurable depuis sa sauvegarde avant toute écriture v2. ### 28.6 Correctifs hors séquence — retours d'usage réel du 17 août 2026 Statut : **implémenté et validé localement le 17 août 2026 ; qualification CI sur trois OS encore requise pour la clôture formelle**, comme pour les étapes 1 et 2. Contexte : cette intervention n'ouvre pas l'étape 3 prévue par l'ordre de démarrage (section 27). Elle répond à une remontée directe de l'utilisateur sur l'installation réellement en service (six processus actifs, composition `start`), avant que le chantier ne reprenne l'étape 3. Huit symptômes ont été rapportés et diagnostiqués : 1. horodatage incrusté absent sur des clips récents — rattaché à I-17 (retard ou arrêt silencieux du worker de normalisation) ; 2. synchronisation cloud pas vraiment à la minute — rattaché à O-05 (dérive de cadence) et à O-04 (pas de watermark, non traité ici) ; 3. ouverture de page lente — cause distincte identifiée, `ffmpeg -i` relancé en synchrone à chaque requête pour tout clip écarté sans durée connue, **non corrigée dans ce lot**, à traiter séparément (mise en cache du sondage) ; 4. bouton « Réessayer » du direct rendu inopérant par le message d'erreur superposé — bug CSS, sans anomalie correspondante dans le rapport initial ; 5. le direct déclenche un nouvel enregistrement côté caméra — comportement du firmware/cloud Blink, hors du périmètre de ce projet, aucune action possible ; 6. direct plus fluide sur téléphone que sur la page web — conséquence structurelle du relais MJPEG côté serveur, non corrigée ; 7. absence de suppression de clip via l'API — choix de conception déjà documenté (section 2), pas une anomalie ; 8. le verbe `start` reste actif en permanence — comportement voulu du superviseur (section 10.6), pas une anomalie. Seuls les points 1, 2 (partiellement) et 4 ont donné lieu à un correctif dans ce lot, par choix explicite de l'utilisateur de traiter I-17 et O-05 en priorité. Les points 3 et 6 restent des anomalies ouvertes, non numérotées dans le rapport initial ; ils devront recevoir un identifiant lors de la prochaine révision du rapport. #### Travail effectué - `runtime.py`, fonction `repeter()` (utilisée par `merge_daily.py --loop` et `watch.py --loop`) : - toute exception de `travail()` autre que `KeyboardInterrupt` est désormais interceptée, journalisée, et le tour suivant a lieu normalement au lieu de tuer le worker (I-17) ; - l'échéance du prochain tour se calcule dès le début du tour courant via `time.monotonic()`, au lieu de dormir la pleine durée après la fin du travail : un tour plus lent que la cadence ne décale plus tous les suivants d'autant (O-05). - `blink2video.py`, fonction `boucler()` (boucle `download --loop`, seule boucle de fond qui n'utilise pas `runtime.repeter` car asynchrone) : - même durcissement : capture de toute erreur de tour hors `BusyError` (I-17), échéance calculée depuis `time.monotonic()` avant le tour plutôt qu'un `asyncio.sleep(args.loop * 60)` fixe après (O-05). - `watch.py` n'a pas eu besoin de ce traitement : `_controler()` encadre déjà son appel réseau (`read_state`) d'un `except Exception` propre. - `serve.py`, CSS `.live .hint.overlay` : la règle n'avait aucun décalage (`position:absolute` sans `top/left/bottom`), donc se recentrait exactement sur le seul enfant en flux du conteneur flex — le bouton « Réessayer » — et interceptait son clic. Correctif : décalage explicite au-dessus du bouton et `pointer-events:none`, pour que le message ne capte plus jamais de clic, y compris pendant l'attente initiale du direct où il n'y a pas de bug visible aujourd'hui mais où le même risque existait. - `test_runtime_validation.py` : nouvelle classe `RepeterTests`, deux tests (erreur de tour non fatale, échéance calculée depuis le début du tour à l'aide d'une horloge et d'un sommeil simulés). - `test_blink2video_audit.py` : deux tests ajoutés à `TestsDefautsAsynchrones` (`test_I17_boucler_survit_a_une_erreur_de_tour`, `test_O05_boucler_calcule_l_echeance_depuis_le_debut_du_tour`), même méthode de simulation côté `boucler()`. #### Décisions et écarts au plan - ce lot n'ouvre pas formellement l'étape 3 : aucune modification du format de registre ni du verrou interprocessus n'a été faite ici, seulement les deux boucles de fond et un bug d'interface découvert en passant ; - la mise en cache du sondage `ffmpeg -i` des clips écartés (point 3) et le relais MJPEG (point 6) restent des anomalies ouvertes, à qualifier et numéroter avant traitement ; - O-04 (watermark cloud) n'a pas été traité : il aurait changé la sémantique de pagination de `read_cloud_manifest`, hors du périmètre resserré demandé pour ce lot. #### Tests et vérifications | Contrôle | Résultat | Durée | Verdict | |---|---:|---:|---| | Suites isolées combinées (`test_runtime_validation`, `test_blink2video_audit`, `test_stage2_identity`) | 85 tests : 72 réussis, 9 défauts futurs attendus, 4 E-01 ignorés | 1,843 s | réussi | | Nouveaux tests I-17/O-05 (4) | 4/4 | inclus ci-dessus | réussi | | Chaîne historique `tests.py` | 70 contrôles observés, dont la suite « Interface web » | 25,6 s | réussi, « Tout est vert » | | Compilation des 5 fichiers Python touchés | 5/5 | < 1 s | réussi | | Ruff sur les fichiers touchés | 9 erreurs, toutes E402 ou pré-existantes hors du diff (confirmé par `git diff --stat`) | 0,3 s | réussi, aucune régression | Aucune mesure de performance formelle (P-xx) n'a été enregistrée pour ce lot : il ne s'agit pas d'une étape numérotée du plan, et la correction porte sur la robustesse d'une boucle de fond, pas sur son débit. Un futur passage par l'étape 6 (P-06, P-09) restera l'occasion de mesurer l'effet cumulé d'I-17 et O-05 avec le protocole complet. #### Verdict et retour arrière I-17 et O-05 sont fermés pour les trois boucles de fond (`download`, `merge`, `watch`), hors séquence par rapport à l'ordre de démarrage recommandé mais à la demande explicite de l'utilisateur, suite à un usage réel révélant leurs symptômes avant que l'étape 3 ne soit rouverte. Un bug d'interface non répertorié (bouton « Réessayer » du direct) est également fermé. **Verdict local : acceptée. Verdict formel : en attente de la matrice CI sur les trois OS**, comme pour les étapes 1 et 2. Retour arrière : `git revert` ciblé sur les trois fichiers de production concernés (`runtime.py`, `blink2video.py`, `serve.py`) ; aucune migration de données n'est impliquée. Reste à faire, en dehors de la séquence normale du plan : qualifier et numéroter les deux anomalies découvertes aux points 3 et 6, avant de décider si elles rejoignent une étape existante ou en ouvrent une nouvelle. ### 28.7 Correctif hors séquence — plafonnement de /api/clips et cache de durée Statut : **implémenté et validé localement le 17 août 2026 ; qualification CI sur trois OS encore requise pour la clôture formelle**. Contexte : ferme le point 3 laissé ouvert en 28.6 (« ouverture de page lente ») et répond à une question de suivi de l'utilisateur, posée avant de partir sur l'étape 3 : le nombre de clips connus grossit chaque jour, sans plafond, et `/api/clips` renvoyait déjà tout l'historique à chaque requête. Vérifié sur le registre réel de l'installation avant toute décision : 147 entrées, 82,2 Ko — un `json.loads` sur ce volume ne coûte rien de mesurable, SQLite n'aurait apporté aucun gain ici. Le vrai coût, confirmé sur les mêmes données : 38 clips écartés sur 38 n'avaient aucune durée en cache, donc `ffmpeg -i` était relancé 38 fois, en synchrone, à chaque ouverture de page. Décision pour l'étape 3 elle-même (verrou/registre) : option légère retenue (verrou atomique + JSON), pas de migration SQLite, faute de preuve que le format JSON soit en cause où que ce soit dans le projet actuel. #### Travail effectué - `serve.py`, `collect()` : - nouveau paramètre `depuis` (date) : les entrées plus anciennes sont ignorées avant même d'être sondées, donc sans coût ffmpeg supplémentaire pour l'historique masqué ; - la réponse porte désormais `window_days` (fenêtre appliquée, ou `null` si aucune) et `total_known` (nombre total d'entrées valides du registre, fenêtre ou non), pour que la page puisse annoncer « 30 derniers jours, X sur Y clips connus » ; - `probe_duration_cached()` : la durée d'un clip écarté est mise en cache dans `Blink_Thumbs/excluded_durations.json`, clé = identité, valeur = durée + empreinte (taille, mtime) du fichier source. Un média remplacé (réparation d'un fichier absent, par exemple) change l'empreinte et est re-sondé ; sinon la valeur en cache est réutilisée sans relancer ffmpeg. - `/api/clips` accepte `?all=1` pour lever explicitement la fenêtre ; sans ce paramètre, la fenêtre par défaut est `DEFAULT_WINDOW_DAYS = 30` jours, cohérente avec la fenêtre déjà utilisée ailleurs dans le projet (cloud `since` par défaut). - Page web : la liste affiche un bandeau « N derniers jours affichés (X sur Y clip(s) connus) · afficher tout l'historique » quand la fenêtre est active, avec un lien qui recharge une fois avec `?all=1` (mémorisé le temps de la session, perdu au rechargement de la page). - `tests.py` : le test « l'inventaire liste tous les clips, écartés compris » attendait explicitement l'historique complet (les clips synthétiques du test d'horodatage couvrent plusieurs mois) ; sa requête est passée à `/api/clips?all=1` pour préserver son intention plutôt que de contourner la nouvelle fenêtre. - `test_serve_clips_window.py` (nouveau) : quatre tests — clip récent visible et clip ancien masqué par défaut, `depuis=None` renvoie tout, une durée de clip écarté n'est sondée qu'une fois, un média remplacé est re-sondé. #### Décisions et écarts au plan - pas de pagination ni de chargement par jour à la demande : un simple bandeau « afficher tout » suffit à la volumétrie actuelle et évite d'inventer une mécanique de pagination non standard pour un usage encore modeste ; - le compteur `total_known` traverse tout le registre même en mode fenêtré, ce qui reste bon marché (itération pure Python sur 147 entrées) : seul le sondage ffmpeg, coûteux, est réellement évité par la fenêtre et le cache ; - `provenances(entries)` (panneau « sources ») continue de porter sur l'intégralité du registre, fenêtre ou non : c'est un agrégat par caméra, pas une liste de vignettes, son coût ne grossit pas avec le nombre de clips. #### Tests et vérifications | Contrôle | Résultat | Durée | Verdict | |---|---:|---:|---| | `test_serve_clips_window.py` (nouveau) | 4/4 | 0,021 s | réussi | | Suites isolées combinées (avec `test_serve_clips_window`) | 89 tests : 76 réussis, 9 défauts futurs attendus, 4 E-01 ignorés | 1,794 s | réussi | | Chaîne historique `tests.py`, dont « Interface web » corrigée | 70 contrôles observés | ~25 s | réussi, « Tout est vert » | | Compilation `serve.py` | 1/1 | < 1 s | réussi | | Ruff sur `serve.py`, `tests.py`, `test_serve_clips_window.py` | 5 erreurs, toutes pré-existantes dans `serve.py` (E402 déjà documentées en O-09), 0 dans les fichiers nouveaux/modifiés de tests | 0,3 s | réussi, aucune régression | Mesure ponctuelle sur les données réelles (hors protocole P-xx formel, à l'appui de la décision JSON/SQLite) : | Mesure | Valeur | Source | |---|---:|---| | Entrées du registre principal | 147 | `Blink_Clips/.blink_download_state.json`, 82,2 Ko | | Clips écartés sans durée en cache avant correctif | 38 / 38 | `Blink_Normalized/.blink_normalized.json` | | Appels `ffmpeg -i` par ouverture de page avant correctif | 38 | déduit du point précédent | | Appels `ffmpeg -i` par ouverture de page après correctif (cache froid puis chaud) | 38 puis 0 | `test_serve_clips_window.py` | #### Verdict et retour arrière Point 3 de la section 28.6 (« ouverture de page lente ») est fermé. Aucune anomalie numérotée du rapport initial n'était directement concernée : ce correctif, comme celui du point 4 en 28.6, est un défaut trouvé en creusant un retour d'usage réel plutôt qu'une anomalie déjà répertoriée à l'audit. **Verdict local : acceptée. Verdict formel : en attente de la matrice CI sur les trois OS.** Retour arrière : `git revert` ciblé sur `serve.py` et `tests.py` ; `excluded_durations.json` est un cache pur, sa suppression ne perd aucune donnée et le pire cas est un retour au comportement précédent (re-sondage systématique). ## Étape 3 — Persistance et verrous transactionnels Statut : **implémentée et validée localement le 17 août 2026 ; qualification CI sur trois OS encore requise pour la clôture formelle**, comme pour les étapes précédentes. Révision de départ : `e7ede05`. ### 28.8 Décision D-01 et travail effectué #### Décision D-01, tranchée hors recommandation du rapport La section 24 recommandait SQLite en mode WAL pour l'étape 3. Avant de trancher, le coût réel du JSON a été mesuré sur le registre de l'installation en service plutôt que supposé : **147 entrées, 82,2 Ko**. Un `json.loads` sur ce volume est négligeable, quel que soit le moteur de stockage ; le vrai coût mesuré à la même occasion (voir 28.7) était un problème différent, un sondage ffmpeg synchrone, déjà corrigé et sans lien avec le format d'état. Décision retenue, à la demande explicite de l'utilisateur : **option légère — verrou atomique + registre JSON existant**, plutôt que la migration SQLite. Justification : les deux anomalies bloquantes de cette étape (B-04, B-05) viennent de l'absence d'atomicité dans le protocole de verrouillage, pas du format de stockage ; la fusion par delta et la protection des tombstones du registre JSON existaient déjà depuis l'étape 2 (section 28.5). Une seconde migration de format, juste après celle de l'étape 2 (v1→v2), aurait ajouté une dette (schéma versionné, migration, dépendance `sqlite3` de fait) sans preuve que le JSON soit en cause. SQLite reste une évolution valable si le volume ou les besoins de requête grossissent un jour ; ce n'est pas le cas aujourd'hui. #### Travail effectué - `runtime.py`, fonction `verrou()`, réécrite intégralement : - acquisition par création exclusive du fichier de verrou (`os.open(..., os.O_CREAT | os.O_EXCL | os.O_WRONLY)`), garantie atomique au niveau de l'OS (`CreateFileW` avec `CREATE_NEW` sous Windows, `open` avec `O_EXCL` sous POSIX) : deux processus qui tentent la création en même temps ne peuvent plus réussir tous les deux. L'ancien protocole lisait le fichier, concluait qu'il était libre, puis l'écrivait en deux opérations séparées, ce qui laissait une fenêtre de course (B-05) ; - jeton d'acquisition (`uuid.uuid4().hex`) écrit avec owner/pid : la libération en sortie ne retire le fichier que si son contenu porte encore notre propre jeton, ce qui évite de retirer la marque d'un autre propriétaire dans un cas limite ; - un propriétaire dont le PID n'existe plus est retiré puis la création est retentée, sans jamais voler un propriétaire vivant sur le seul critère de l'âge : `stale_after` ne déclenche plus de vol temporel (B-05, moitié « volé après dix minutes ») ; - une lecture illisible (fichier disparu entre l'échec de la création et la lecture, ou création concurrente pas encore écrite) retente immédiatement plutôt que de trancher à tort. - `_ecrire_registre()` (blink2video.py) et son verrou `"registre"` n'ont pas été modifiés : la fusion par delta et la protection des tombstones déjà en place depuis l'étape 2 suffisaient une fois le verrou lui-même rendu atomique — les deux processus qui l'utilisaient en concurrence ne peuvent plus désormais entrer en même temps dans `_ecrire_registre`. - `test_blink2video_audit.py` : - retrait de `@unittest.expectedFailure` sur `test_B05_un_verrou_vivant_ne_perime_pas_sur_le_seul_ttl` et `test_B05_acquisition_concurrente_n_a_qu_un_gagnant`, devenus des succès réels ; - `test_B04_fusion_perimee_preserve_une_exclusion_plus_recente` corrigé et dé-marqué : la fixture omettait `created_at` sur l'entrée disque, ce qui la faisait écarter par `load_download_state()` avant même la fusion — échec pour une raison étrangère à B-04, indépendante de cette étape. - `test_runtime_validation.py` : nouvelle classe `VerrouTests`, trois tests (verrou d'un processus mort récupéré, libération qui ne retire que son propre jeton, cycle acquisition/libération normal). #### Décisions et écarts au plan - **T-B05-PID-REUSE non couvert**, en connaissance de cause : un PID recyclé par un processus non lié, actif pendant que le vrai propriétaire est mort, ne peut pas être distingué sans comparer l'heure de création du processus, non disponible dans la bibliothèque standard de façon portable. L'ajouter proprement supposerait soit une dépendance supplémentaire (`psutil`, déjà présente en usage optionnel dans `benchmarks/`, jamais en production), soit un code par plateforme fragile (WMI/PowerShell sous Windows, `/proc` ou `ps` ailleurs) — décision non prise unilatéralement, à soumettre si ce risque devient réel (poste qui redémarre beaucoup, PID recyclé vite). Le même gisement de risque était déjà documenté sans solution en I-14 ; - pas de test de charge à 1000 itérations (section 15.2) ni de scénario à 20 processus (T-B05-RACE20) : le test à 2 processus prouve l'exclusion mutuelle au niveau du verrou lui-même, propriété qui ne dépend pas du nombre de concurrents une fois l'opération atomique — un test à plus grande échelle resterait un exercice de performance (P-08), pas de correction supplémentaire, et n'a pas été jugé nécessaire pour clore B-05 ; - aucun test d'intégration multi-processus dédié à B-04 (T-B04-INTERLEAVE) n'a été ajouté au-delà de l'existant : la combinaison du verrou désormais atomique (prouvé par le test de course B-05) et de la fusion par delta déjà prouvée (test B-04 corrigé) couvre le même chemin par composition, sans dupliquer un test déjà écrit à chaque couche. #### Tests et vérifications | Contrôle | Résultat | Durée | Verdict | |---|---:|---:|---| | Suites isolées combinées (avec `VerrouTests`) | 92 tests : 78 réussis, 6 défauts futurs attendus (contre 9 avant cette étape), 4 E-01 ignorés, 4 skips | 2,617 s | réussi | | Dont T-B05 (2 tests) | passés de « échec attendu » à réussis | inclus ci-dessus | corrigé | | Dont T-B04 (1 test) | fixture corrigée, passé de « échec attendu » à réussi | inclus ci-dessus | corrigé | | Nouveaux `VerrouTests` (3) | 3/3 | inclus ci-dessus | réussi | | Chaîne historique `tests.py`, cycle complet (login, download, merge, serve, stop) | 70 contrôles observés | ~24 s | réussi, « Tout est vert » | | Compilation `runtime.py` | 1/1 | < 1 s | réussi | | Ruff sur `runtime.py`, `test_runtime_validation.py`, `test_blink2video_audit.py` | 0 erreur | 0,3 s | réussi | #### Mesures Aucune mesure formelle P-02/P-03/P-08/P-09 n'a été enregistrée pour cette étape : le format de stockage n'a pas changé (donc P-02/P-03, coût d'écriture du registre, ne peuvent pas bouger significativement), et P-08 (course sur le verrou) n'a pas de protocole de mesure de débit prévu au-delà du constat correction/incorrection déjà couvert par le test de course. Écart documenté au lieu d'un chiffre inventé, conformément à la règle de la section 11.1. #### Verdict et retour arrière B-04 et B-05 sont fermés, hors T-B05-PID-REUSE explicitement laissé ouvert et documenté plutôt que masqué. La décision D-01 est tranchée en faveur de l'option légère, à rebours de la recommandation par défaut du rapport, avec la mesure qui la justifie. **Verdict local : acceptée. Verdict formel : en attente de la matrice CI sur les trois OS.** Retour arrière : `git revert` ciblé sur `runtime.py` et les deux fichiers de tests modifiés ; aucune migration de données n'est impliquée, le format du registre n'a pas changé. ## Étape 4 — Racine de données et authentification Statut : **implémentée et validée localement le 17 août 2026 ; qualification CI sur trois OS encore requise pour la clôture formelle**, comme pour les étapes précédentes. Révision de départ : le commit fermant l'étape 3. ### 28.9 Travail effectué #### I-01 — CONFIG et OUTPUT ignoraient BLINK_HOME Cause identifiée par comparaison des jumeaux (les quatre points d'entrée du projet) : `merge_daily.py`, `serve.py` et `watch.py` dérivent tous leurs chemins par défaut de `BASE_DIR = runtime.app_dir()`. `blink2video.py` était le seul des quatre à définir `CONFIG`/`OUTPUT` comme des `Path` relatifs bruts, résolus au hasard du répertoire courant de lancement. Un drift accidentel pur, pas une différence voulue. - `blink2video.py` : `CONFIG` et `OUTPUT` dérivent désormais de `BASE_DIR = runtime.app_dir()`, exactement comme les trois autres fichiers. - Décision D-02 (résolution d'un `--output` relatif explicite) : **non traitée**, restée hors périmètre. Le comportement existant (résolution contre le CWD pour une valeur explicite, inchangé dans les quatre programmes) n'est pas un défaut observé comme l'était le seul défaut de *valeur par défaut* de blink2video.py ; le changer toucherait les quatre programmes à la fois pour un besoin encore hypothétique. Décision affichée ici plutôt que tranchée en silence, comme le demande la section 24. #### I-02 — Sauvegardes d'authentification concurrentes - `save_session()` : liste blanche explicite de champs persistés (`AUTH_FIELDS`) au lieu de copier `blink.auth.login_attributes` en bloc — le mot de passe, présent dans ce dict source, n'est plus jamais écrit, ni vide ni en clair, et un champ que blinkpy ajouterait un jour ne fuiterait pas automatiquement sur disque (4.5). - Temporaire propre à chaque appel (`blink_auth...tmp`) au lieu du `blink_auth.tmp` partagé par tous les processus : deux sauvegardes concurrentes n'écrivent plus jamais dans le même fichier intermédiaire (4.6). - Horodatage interne `updated_at` : une sauvegarde plus ancienne (callback de rafraîchissement arrivé en retard, par exemple) ne peut plus écraser une version plus récente déjà sur disque — même principe de version monotone que la fusion du registre de téléchargement depuis l'étape 2, appliqué ici au fichier de session (4.7, en partie de D-04 : contrôle de génération sans broker). - Écriture en 0600 sur POSIX (`os.chmod`, ignoré sous Windows où le modèle de permissions diffère) avant le remplacement atomique (4.3, 4.4). - `.gitignore` : `blink_auth.tmp` n'était couvert par aucun motif existant (`blink_auth.json.*` ne couvre pas un remplacement d'extension). Motif `blink_auth.*.tmp` ajouté pour couvrir la nouvelle forme du temporaire. #### I-03 — Tokens rafraîchis non persistés - Confirmé dans le code de blinkpy installé (0.25.9) : `Auth.query()` appelle `self.callback()` juste après un rafraîchissement transparent de jeton, mais `make_blink()` ne fournissait aucun callback — un rafraîchissement en cours de boucle ne touchait jamais le disque. - `make_blink()` fournit désormais `callback=lambda: save_session(blink)` à la construction de `Auth`. Comme `save_session` s'appuie sur la liste blanche et l'horodatage d'I-02, chaque rafraîchissement automatique est maintenant écrit, sans jamais pouvoir régresser une sauvegarde plus récente. #### Décisions et écarts au plan (4.9, 4.10) - **4.9 (propriétaire unique vs génération)** : le contrôle de génération (`updated_at`) a été retenu plutôt qu'un unique processus propriétaire de la session. Un broker local (D-04, troisième option) n'a pas été implémenté : jugé disproportionné tant que rien ne prouve que le contrôle de génération est insuffisant en pratique ; - **4.10 (keyring/DPAPI/Keychain)** différé sans être retiré du plan : le mode fichier reste requis dans tous les cas (portabilité, absence de trousseau sur certaines installations), et rien dans les anomalies observées ne justifie de l'ajouter maintenant. ### Tests et vérifications | Contrôle | Résultat | Durée | Verdict | |---|---:|---:|---| | Suites isolées combinées | 96 tests : 87 réussis, 5 défauts futurs attendus (contre 6 avant), 4 E-01 ignorés (inclus dans les skips) | 2,379 s | réussi | | Dont T-I01 (déjà écrit à l'étape 0, spawn un vrai sous-processus) | passé de « échec attendu » à réussi | inclus ci-dessus | corrigé | | Nouveaux tests I-02 (3) et I-03 (1) | 4/4 | inclus ci-dessus | réussi | | Chaîne historique `tests.py`, cycle complet | 70 contrôles observés | ~24 s | réussi, « Tout est vert » | | Compilation `blink2video.py` | 1/1 | < 1 s | réussi | | Ruff sur `blink2video.py`, `test_blink2video_audit.py` | 3 erreurs, toutes E402 pré-existantes (imports après `runtime.bootstrap()`, documentées en O-09) | 0,3 s | réussi, aucune régression | ### Mesures Aucune mesure formelle P-01/P-10 enregistrée : ce lot ne change pas le volume ni la fréquence des écritures (une sauvegarde de session reste un événement rare — login initial et rafraîchissements occasionnels de jeton), et la baseline globale de l'étape 0 reste elle-même non qualifiée (section 28.2). Écart documenté plutôt que chiffre inventé. ### Verdict et retour arrière I-01, I-02 et I-03 sont fermés. D-02 (résolution d'un `--output` relatif explicite) reste une décision ouverte, affichée plutôt que tranchée en silence : elle changerait un comportement identique aujourd'hui dans les quatre programmes, pour un cas non observé comme problème. **Verdict local : acceptée. Verdict formel : en attente de la matrice CI sur les trois OS.** Retour arrière : `git revert` ciblé sur `blink2video.py`, `.gitignore` et `test_blink2video_audit.py` ; aucune migration n'est impliquée, un `blink_auth.json` existant reste lisible tel quel (mêmes clés, sous-ensemble de celles déjà écrites par l'ancien code). ## Étape 5 — Démarrage convivial et onboarding web Statut : **livrée en version resserrée le 17 août 2026 ; qualification CI sur trois OS encore requise pour la clôture formelle**. ### 28.10 Décision de périmètre et travail effectué #### Décision de périmètre, prise avec l'utilisateur avant le codage La section 17 du rapport décrit un périmètre complet (jeton CSRF à usage court, limitation de tentatives, réutilisation de port via endpoint de santé signé, verrou de propriétaire unique du flux, 21 tests `T-E01-*`, benchmark P-12 complet). Avant de commencer, l'utilisateur a choisi explicitement la **version resserrée** proposée en alternative : réutiliser `LoginFlow` déjà présent dans `serve.py`, faire converger `blink2video` sans argument et `blink2video start`, ajouter un préflight et un mini-smoke read-only avant tout worker permanent — sans CSRF ni rotation de port ni benchmark P-12 formel. Ce choix est documenté ici pour que la comparaison au périmètre complet du rapport reste traçable plutôt qu'silencieuse. #### Travail effectué - `blink2video.py` : - `route(argv: list) -> int`, dispatch pur extrait de `if __name__ == "__main__"` (5.1/5.2) : une liste vide emprunte le même chemin que `start` (préflight compris) ; `--help`/`--version` gardent leur sens inchangé. Extrait en fonction ordinaire précisément pour rester testable sans sous-processus, ce que le bloc `if __name__` interdisait. - `preflight() -> dict` (async, 5.3/5.4) : test réellement la session via `connect_saved()` (pas seulement la présence du fichier), strictement read-only (aucun armement, direct, manifeste, téléchargement, assemblage, notification — 17.3). Renvoie `authenticated`, `networks`, `sync_modules`, `cameras`, `cloud_only`, `error`, conforme au contrat de la section 17.3 hors `warnings`/`elapsed` (non retenus dans la version resserrée). - `accueillir(etat, supplement, delai=600.0) -> int` (5.6 à 5.16) : lance un `serve` temporaire (réutilise `LoginFlow` sans modification), attend que son port réponde, ouvre le navigateur sur `?login=1`, interroge `preflight()` toutes les 2 s jusqu'à authentification ou délai. Le serveur temporaire est systématiquement arrêté en sortie, succès compris (5.16) : `start` relance ensuite la composition complète plutôt que de faire rejoindre ce processus à la supervision normale — un bref redémarrage de l'interface, coût assumé de la simplicité. - `_port_ouvert(port) -> bool` : extrait pour rester mockable sans toucher au module `socket` global, dont la boucle asyncio de Windows se sert aussi pour elle-même. - `executer()`, branche `"start"` : appelle `preflight()` avant tout spawn ; si non authentifié, `accueillir()` doit réussir avant que la composition `runtime.STANDARD` soit lancée (5.15). - Deux variables d'environnement réservées aux tests, non documentées côté utilisateur : `BLINK_ONBOARDING_TIMEOUT` (raccourcit le délai) et `BLINK_NO_BROWSER` (empêche l'ouverture réelle d'un navigateur). - `serve.py` : la page ouvre automatiquement la boîte de dialogue de connexion existante quand chargée avec `?login=1` (5.10), sans nouvelle route ni CSRF, en réutilisant `authenticate()` déjà présent. - `tests.py`, `test_verbes()` : l'assertion « sans argument, l'aide s'affiche » ne pouvait plus tenir (E-01 change ce comportement délibérément). Remplacée par une vérification que l'onboarding démarre bien et respecte le délai (`BLINK_ONBOARDING_TIMEOUT`/`BLINK_NO_BROWSER` ajoutés à `environnement_test`), et une assertion séparée que `--help` continue d'afficher l'aide sans déclencher d'onboarding. - `test_blink2video_audit.py` : - `test_E01_sans_argument_selectionne_start` réécrit : la version d'origine (étape 0) attendait que `parse_args().command` vaille `"start"`, une hypothèse d'implémentation antérieure au choix réel (dispatch via `route()`, pas via le positionnel `command`). Corrigé pour vérifier le mécanisme effectivement construit ; - nouveaux tests `preflight()` (4, dans `TestsDefautsAsynchrones`) et `accueillir()`/`route()`/`executer()` (6, nouvelle classe `TestsE01Onboarding`, remplace le stub `TestsOnboardingFutur` retiré) ; - **incident évité pendant l'écriture des tests** : le premier jet de `test_E01_preflight_sans_session_ne_touche_pas_le_reseau` n'avait pas patché `b2v.CONFIG`. Comme `BASE_DIR`/`CONFIG` se calculent une seule fois à l'import du module (§28.9), et que ce fichier de test n'impose pas `BLINK_HOME` avant son propre `import blink2video`, le test lisait le `blink_auth.json` réel de la machine et tentait de le rafraîchir contre l'API Blink réelle — exactement ce que la section 12.5 interdit. Repéré à l'exécution (durée anormale, ~2,3 s pour un test censé être local) avant tout commit, puis corrigé par un patch explicite de `CONFIG`. Consigné ici plutôt que passé sous silence, conformément à la règle du journal. #### Ce qui n'a pas été fait (périmètre resserré, assumé) - pas de jeton CSRF ni de vérification d'origine : l'interface reste liée à 127.0.0.1 uniquement, contre-mesure déjà en place et jugée suffisante pour ce périmètre ; - pas de limitation de tentatives ni de politique de cadence dédiées à l'onboarding : `LoginFlow` existant n'en a pas non plus, inchangé ; - pas de réutilisation d'une instance existante via endpoint de santé signé ni de rotation de port automatique : un port déjà occupé par une vraie instance ou un programme tiers fait simplement échouer `accueillir()` avec un message clair, sans tentative de port alternatif (5.10 partiellement couvert : le cas « port étranger » n'est pas distingué du cas « port blink2video déjà actif ») ; - pas de synthèse structurée affichée dans la page elle-même (modules, réseaux, caméras) : le résumé de succès (« compte accessible, N caméra(s) ») s'affiche dans le terminal, pas dans l'interface web ; - pas de benchmark P-12 ni de mesure formelle des budgets de la section 17.8 ; - transition onboarding → composition complète non transparente pour un onglet déjà ouvert : le serveur temporaire redémarre (assumé en 5.6/5.16 ci-dessus), un onglet resterait sur une erreur de connexion quelques instants avant de pouvoir être rechargé. #### Tests et vérifications | Contrôle | Résultat | Durée | Verdict | |---|---:|---:|---| | Suites isolées combinées (avec `TestsE01Onboarding`) | 103 tests : 99 réussis, 4 défauts futurs attendus (contre 5 avant), 0 skip (le stub `TestsOnboardingFutur` a été remplacé par de vrais tests) | 5,0 s | réussi | | Nouveaux tests E-01 (`preflight` ×4, `TestsE01Onboarding` ×6, dispatch ×2) | 12/12 | inclus ci-dessus | réussi | | Chaîne historique `tests.py`, dont `test_verbes` réécrit | 70 contrôles observés | ~30 s (onboarding borné à 3 s ajouté) | réussi, « Tout est vert » | | Compilation `blink2video.py`, `serve.py` | 2/2 | < 1 s | réussi | | Ruff sur les 4 fichiers touchés | 8 erreurs, toutes E402 pré-existantes (imports après `runtime.bootstrap()`, O-09) | 0,3 s | réussi, aucune régression | #### Mesures Aucune mesure P-01/P-10/P-12 formelle : le benchmark P-12 lui-même n'existe pas encore dans `benchmarks/stage0_baseline.py` (jamais implémenté depuis l'étape 0, où il était déjà noté « non disponible »). L'ajouter appartient à une reprise future de l'étape 5 vers son périmètre complet, pas à cette version resserrée. Écart documenté plutôt que chiffre inventé. #### Verdict et retour arrière E-01 est livré en version resserrée : le parcours nominal (sans argument → préflight → onboarding si nécessaire → composition complète) fonctionne de bout en bout et est testé sans toucher de compte ni de navigateur réels. CSRF, rotation de port, endpoint de santé signé et P-12 restent hors périmètre et devront faire l'objet d'une décision explicite s'ils sont un jour requis (aucune décision D-xx correspondante n'a été ajoutée : le choix a été fait directement avec l'utilisateur pour cette étape). **Verdict local : acceptée en périmètre resserré. Verdict formel : en attente de la matrice CI sur les trois OS.** Retour arrière : `git revert` ciblé sur `blink2video.py`, `serve.py`, `tests.py` et `test_blink2video_audit.py` ; aucune migration de données. ## Étape 6 — Adaptateurs USB/cloud et performance des entrées-sorties Statut : **partielle, périmètre resserré, implémentée et validée localement le 17 août 2026 ; qualification CI sur trois OS encore requise**. ### 28.11 Décision de périmètre et travail effectué #### Décision de périmètre Les 14 points de la section 18.1 couvrent à la fois de la fiabilité (B-06, I-15) et de la performance/robustesse pure (watermark durable, streaming par blocs borné à 32 Mio, backoff+jitter sur 429, pin de blinkpy). Choix pris avec l'utilisateur avant le codage : traiter d'abord les deux anomalies actives (B-06, I-15), laisser le reste en périmètre différé et documenté. L'adaptateur `_local_storage` (6.1, dépendance privée blinkpy déjà documentée en I-06), le watermark cloud (6.7/6.8, O-04), le streaming par blocs (6.11/6.12, O-01) et le pin de version (6.14, O-07/D-01 implicite ne s'applique pas ici) restent donc ouverts. #### Travail effectué - `blink2video.py`, `un_passage()` (B-06) : le bloc par module (lecture du manifeste USB puis téléchargement) est désormais entièrement couvert par `hub_lock(name)`, la même primitive que `serve.py` utilise pour le direct et l'actualisation. Un module déjà réservé lève `BusyError`, rattrapée localement : le tour continue avec les autres modules plutôt que d'abandonner tout le passage (composition avec le correctif I-17 déjà en place). Limite assumée, identique à celle qui existait déjà pour direct/actualisation : le verrou reste global à tous les hubs (une seule ressource nommée `"hub"`), pas par `sync_id` comme le demande 6.4/6.5 dans le périmètre complet — deux Sync Modules distincts se bloquent donc encore l'un l'autre inutilement. Non traité dans ce lot, documenté ici plutôt que silencieusement laissé de côté. - `download_clip()` (USB) et `CloudClip.download_to()` (cloud), pour I-15 : les deux ne vérifiaient qu'une taille non nulle après écriture, ce qui laissait passer un fichier d'un octet ou une page d'erreur HTML servie avec un statut 2xx. Les deux appellent désormais `merge_daily.valid_mp4()` (import ajouté : `merge_daily as md`), qui relit la boîte `ftyp` en début de fichier — la même fonction que `merge_daily.py` utilise déjà, pas une seconde copie qui aurait dérivé. Import vérifié sans cycle : `merge_daily` n'importe jamais `blink2video`. - Validation de portée : seule la boîte `ftyp` est vérifiée, pas un probe ffmpeg complet (durée, résolution, decodabilité). C'est la moitié « légère » de la décision D-05 (section 24), pas encore la validation complète — la probe systématique reste à trancher si la légère se révèle insuffisante en usage réel. #### Tests et vérifications | Contrôle | Résultat | Durée | Verdict | |---|---:|---:|---| | Suites isolées combinées | 109 tests : 106 réussis, 3 défauts futurs attendus (contre 4 avant cette étape) | 6,0 s | réussi | | Dont T-B06 (3, dont 2 nouveaux : occupation par le direct, indépendance du cloud) | 3/3 | inclus ci-dessus | réussi | | Dont I-15 (4 nouveaux : cloud/USB × invalide/valide) | 4/4 | inclus ci-dessus | réussi | | Chaîne historique `tests.py`, dont assemblage réel de clips ffmpeg | 70 contrôles observés | ~24 s | réussi, « Tout est vert » | | Compilation `blink2video.py` | 1/1 | < 1 s | réussi | | Ruff sur `blink2video.py`, `test_blink2video_audit.py` | 4 erreurs, toutes E402 (imports après `runtime.bootstrap()`, dont le nouvel `import merge_daily as md`, même catégorie déjà documentée en O-09) | 0,3 s | réussi, aucune régression | #### Mesures Aucune mesure P-06/P-07/P-09/P-10 : ce lot ne touche ni le débit ni la mémoire (la validation `ftyp` lit 64 octets d'un fichier déjà entièrement écrit sur disque, coût négligeable et non mesuré séparément), et les vrais sujets de performance de cette étape (streaming, watermark) restent hors périmètre. Écart documenté plutôt que chiffre inventé. #### Verdict et retour arrière B-06 et I-15 sont fermés en périmètre resserré. I-17 était déjà fermé hors séquence (28.6) et compte pour cette étape sans travail supplémentaire. Restent ouverts dans cette étape : I-06 (adaptateur `_local_storage`), I-08 (pagination cloud plafonnée à `stop=20`), O-01 (streaming par blocs), O-04 (watermark durable). Le critère de sortie formel de la section 18.4 n'est donc pas atteint dans son intégralité. **Verdict local : acceptée en périmètre resserré. Verdict formel : en attente de la matrice CI sur les trois OS, et du reste du périmètre de l'étape 6 si retenu plus tard.** Retour arrière : `git revert` ciblé sur `blink2video.py` et `test_blink2video_audit.py` ; aucune migration de données. ## Étape 7 — Grammaire CLI et supervision Statut : **partielle, périmètre resserré, implémentée et validée localement le 17 août 2026 ; qualification CI sur trois OS encore requise**. ### 28.12 Décision de périmètre et travail effectué #### Décision de périmètre La section 19.1 couvre 16 points, dont une grammaire d'arité générique (7.1/7.2/7.3/7.4), une politique explicite de composition pour chaque verbe délégué (7.5), des groupes POSIX et Job Objects Windows pour la supervision (7.9), des fiches à états `starting`/`running`/`stopping` (7.13), et un endpoint de santé signé pour `open` (7.16). Poursuite du même choix que les étapes 5 et 6 : fermer d'abord les anomalies actives et déjà testées (I-11, I-13, I-14 — les trois seuls défauts encore marqués `expectedFailure` dans la suite isolée après l'étape 6), laisser le reste en périmètre différé. #### Travail effectué - `runtime.py`, `decouper_verbes()` (I-11) : le découpage ne considérait que la présence littérale d'un nom de verbe pour ouvrir un nouveau groupe, sans savoir qu'une option attend une valeur. `download --camera watch` ouvrait donc deux groupes, le second (`watch`) démarrant une surveillance non demandée. Corrigé par une table explicite des options à valeur unique (`--camera`, `--hub`, `--output`, `--port`...), une table des options à valeurs multiples qui consomment jusqu'au prochain mot commençant par `-` (`--exclude`, `--ignore`...), et un traitement spécifique de `--loop` (`nargs="?"` : sa valeur n'est prise que si le mot suivant est un nombre, sans quoi le mot suivant reste un verbe à part entière). Cette table est tenue à la main dans `runtime.py` plutôt que déduite des analyseurs de chaque programme (blink2video.py, merge_daily.py, serve.py, watch.py) : ceux-ci vivent dans des fichiers que ce découpage précède et ne peut pas encore interroger. C'est la version resserrée de 7.1 (grammaire d'arité), pas la table générique construite depuis les vrais analyseurs que demande le périmètre complet. - `runtime.py`, `lire_instances()` (I-13) : une fiche n'était conservée que si le PID du parent restait vivant, même si un enfant persistant lui survivait — cas réel sous POSIX, où le parent ne partage pas le groupe de processus de ses enfants. Corrigé : la fiche reste tant qu'au moins un membre, parent ou enfant, répond vivant. - `blink2video.py`, `arreter()` (I-14) : la fiche d'instance était effacée inconditionnellement après la tentative d'arrêt, avant même de savoir si elle avait réussi. Un survivant devenait alors introuvable au `stop` suivant, sa seule piste ayant disparu avec la fiche. Corrigé : la fiche n'est retirée que si tous ses membres sont confirmés morts ; sinon elle reste, et ses survivants sont comptés dans le message final. - `test_blink2video_audit.py` : les trois tests déjà écrits à l'étape 0 (`test_I11_valeur_de_camera_egale_a_un_verbe_reste_une_valeur`, `test_I13_parent_mort_ne_fait_pas_oublier_un_enfant_vivant`, `test_I14_echec_arret_conserve_la_fiche`) sont démarqués. Cinq nouveaux tests I-11 ajoutés : `--camera update` (le cas le plus dangereux, car il aurait pu déclencher une mise à jour non demandée), `--ignore` à valeurs multiples avalant un nom de verbe, arrêt correct de cette consommation au prochain vrai groupe, et les deux formes de `--loop` (seul, puis suivi d'un nombre). #### Ce qui n'a pas été fait (périmètre resserré, assumé) - pas de grammaire d'arité générique interrogeant les vrais analyseurs (7.1/7.2) : la table de `_OPTIONS_VALEUR_UNIQUE`/`_OPTIONS_VALEURS_MULTIPLES` est tenue à la main et devra être mise à jour si une option change d'arité dans un des quatre programmes, sans garde-fou automatique ; - pas de métadonnées `composable`/`persistent` sur les verbes (7.3/7.4), ni de politique explicite pour les compositions de `stop`/`update`/`autostart`/ `login`/`open` (7.5) : ces verbes restent réglés au cas par cas dans `executer()`, comme avant ; - pas de groupes POSIX/Job Objects Windows dédiés (7.9), pas de `wait()` systématique par enfant (7.11), pas d'identification par heure de création de processus (7.12, même limite déjà documentée en 28.8 pour B-05/PID-reuse) ; - pas de fiches à états `starting`/`running`/`stopping` (7.13) ; - pas d'endpoint de santé signé pour `open` (7.16, cette absence était déjà notée dans le compte rendu de l'étape 5, 28.10). #### Tests et vérifications | Contrôle | Résultat | Durée | Verdict | |---|---:|---:|---| | Suites isolées combinées | **114 tests : 114 réussis, 0 défaut futur attendu** (contre 3 avant cette étape) | 4,8 s | réussi | | Dont I-11 (6, dont 5 nouveaux) | 6/6 | inclus ci-dessus | réussi | | Dont I-13, I-14 (2) | 2/2 | inclus ci-dessus | réussi | | Chaîne historique `tests.py`, dont « Arrêt d'une instance » (fiche réelle) | 70 contrôles observés | ~24 s | réussi, « Tout est vert » | | Compilation `runtime.py`, `blink2video.py` | 2/2 | < 1 s | réussi | | Ruff sur les 3 fichiers touchés | 4 erreurs, toutes E402 pré-existantes (O-09), 0 nouvelle dans `runtime.py` | 0,3 s | réussi, aucune régression | Tous les défauts caractérisés depuis l'étape 0 (T-B01 à T-B06, JSON/CLI/ processus, I-01 à I-22 hors ceux explicitement différés) sont désormais fermés dans la suite isolée. Les défauts restants du rapport concernent des points d'optimisation ou de refactor (section 8) et les périmètres complets volontairement différés des étapes 5, 6 et 7, tous documentés par leur propre section 28.x. #### Mesures Aucune mesure P-01/P-08/P-11 : ce lot corrige un découpage de chaîne et deux conditions de garde sur des structures déjà en mémoire, sans changer le volume ni la fréquence d'aucune opération mesurée par le protocole. Écart documenté plutôt que chiffre inventé. #### Verdict et retour arrière I-11, I-13 et I-14 sont fermés en périmètre resserré. I-12 et la validation de port d'I-22 étaient déjà fermés depuis l'étape 1 ; seul le volet « endpoint de santé signé » d'I-22 reste ouvert. Le critère de sortie formel de la section 19.4 est donc atteint pour les anomalies numérotées qu'il cite (I-11 à I-14, I-22 partiellement), mais pas pour l'ensemble du périmètre structurel de la section 19.1. **Verdict local : acceptée en périmètre resserré. Verdict formel : en attente de la matrice CI sur les trois OS.** Retour arrière : `git revert` ciblé sur `runtime.py`, `blink2video.py` et `test_blink2video_audit.py` ; aucune migration de données. ## Étape 8 — Découpage, bootstrap et maintenabilité Statut : **implémentée en périmètre complet pour 8.1 à 8.9 et 8.12, avec 8.10/8.11/8.13 différés, le 17 août 2026 ; qualification CI sur trois OS encore requise**. Seule étape parmi 3 à 8 où l'utilisateur a choisi le périmètre complet plutôt qu'une version resserrée, malgré le risque de régression signalé comme le plus élevé du plan (gros déplacement de code dans le fichier le plus central du projet). ### 28.13 Décision, travail effectué et incidents de refactor #### Travail effectué (8.1 à 8.6) : découpage en cinq modules `blink2video.py` (2 034 lignes) est éclaté en : - `blink_auth.py` (8.1) : session, identifiants, 2FA, `preflight()` ; - `blink_models.py` (8.2) : adaptateurs USB/cloud, identité, chemins ; - `blink_registre.py` (8.3) : registre incrémental, corrélation, réparation ; - `blink_engine.py` (8.4) : un passage de téléchargement, en boucle ou une fois ; - `blink_cli.py` (8.5) : grammaire, verbes délégués, supervision, onboarding. `blink2video.py` (8.6) ne contient plus que `import runtime`, `from blink_cli import route`, et le bloc `if __name__`. Graphe de dépendances volontairement acyclique : `blink_models` ← `blink_registre` ← `blink_engine` ← `blink_cli` ; `blink_auth` est une feuille, consommée uniquement par `blink_cli` et `serve.py`/`watch.py`. #### Travail effectué (8.7/8.8) : bootstrap différé, O-06 fermé `runtime.bootstrap()` n'est plus appelé sans condition en tête de fichier. Il est maintenant appelé juste avant chaque import de `blink_auth`/ `blink_models`/`blink_engine` — tous localisés à l'intérieur des fonctions qui en ont réellement besoin (`main()`, la branche « start » d'`executer()`, `accueillir()`), sur le même principe que `deleguer()` appliquait déjà pour merge_daily/serve/watch/maj/autostart. Vérifié directement, pas supposé : `stop`, `--help`, `open` et `update --help` exécutés avec `sys.modules['aiohttp'] = None` et `sys.modules['blinkpy'] = None` (aiohttp et blinkpy rendus réellement inimportables) fonctionnent sans erreur ; `login` dans les mêmes conditions échoue proprement via le message `runtime.bootstrap()` existant (« Dépendances absentes »), sans traceback. #### Travail effectué (8.12) : code mort retiré, O-09 fermé `HUB_LOCK` (variable jamais lue) et `_process_alive` (fonction jamais appelée, doublon de `runtime.processus_vivant`) n'ont pas été reportés dans les nouveaux modules : ils disparaissent simplement en n'étant recopiés nulle part, confirmé par une recherche vide sur tout le dépôt après coup. #### Incident de refactor majeur, corrigé avant tout commit Le premier découpage utilisait `from blink_models import (nom1, nom2, ...)` dans `blink_engine.py`, et `from blink_engine import boucler` / `from blink_models import select_sync_modules` localement dans `blink_cli.main()`. Ce style casse le mocking par test : `nom` importé ainsi devient une liaison locale au module importeur, résolue dans SON espace de noms à l'appel — patcher `blink_models.nom` ne change alors rien à ce que `blink_engine` appelle réellement. Découvert par une cascade d'échecs `AttributeError: does not have the attribute ...` puis, après une première correction incomplète, par des appels réels non mockés qui atteignaient un faux objet HTTP dépourvu de la bonne méthode. Corrigé en généralisant `import module` + `module.nom(...)` (accès par attribut, toujours patchable) à toutes les frontières entre les cinq nouveaux fichiers ; seul `deleguer()` et les imports véritablement day-one-lazy (`from aiohttp import ClientSession` dans `main()`) restent en forme locale, documentés comme tels. Un second incident, mineur : un test `mock.patch("aiohttp.ClientSession", ...)` devait remplacer un `mock.patch.object(b2v, "ClientSession", ...)` devenu invalide pour la même raison — `main()` importe `ClientSession` localement par nom, donc seul un patch à la source (`aiohttp.ClientSession`) l'atteint, puisque l'import s'exécute à nouveau, et voit le correctif, à chaque appel. #### Migration des tests : ampleur réelle, pas un détail `test_blink2video_audit.py` et `test_stage2_identity.py` contenaient des dizaines de références `b2v.NOM` (accès direct et `mock.patch.object(b2v, "NOM", ...)`), dont la cible réelle a changé de fichier avec le découpage. Traité par script Python ponctuel (regex ciblées par nom, deux passes pour couvrir les appels étalés sur plusieurs lignes), puis les échecs restants corrigés à la main un par un jusqu'à connaissance complète des transferts manquants. `time`, `datetime`, `asyncio` et `runtime` restent accessibles via `b2v.time`/`b2v.dt`/`b2v.asyncio`/`b2v.runtime` sans aucune correction : modules stdlib/projet partagés (mis en cache une seule fois par Python), un patch sur l'un affecte le même objet quel que soit le nom sous lequel on l'atteint — seuls les noms définis dans ce projet (fonctions, classes, constantes) posent le problème d'espace de noms ci-dessus. `serve.py` (4 usages : `bk.BusyError`, `bk.connect_saved`, `bk.hub_lock`, `bk.login`) et `watch.py` (1 usage : `bk.connect_saved`) important `blink2video as bk` pour des noms désormais absents de ce module ont été corrigés pour importer `blink_auth`/`blink_engine` directement ; `import blink2video as bk` devenu inutile a été retiré de `serve.py`. #### Vérification par bundle réel, pas seulement statique `build.py` a été exécuté intégralement (PyInstaller 6.20.0) sans aucune modification de `blink2video.spec` : l'analyse statique de PyInstaller suit les `import` situés à l'intérieur des fonctions aussi bien qu'en tête de fichier, donc `blink_auth`/`blink_models`/`blink_engine` sont découverts sans figurer dans `hiddenimports` (seuls les verbes délégués par `importlib.import_module` avec une chaîne dynamique en ont besoin, logique inchangée). L'exécutable produit (109 Mo) a ensuite été exercé réellement, pas seulement compilé : `stop`, `--help`, `start --help`, `watch --help`, `merge --help`, `download --help`, `login --help`, `open --port 1` — tous corrects, contre le vrai bundle plutôt que les sources. #### Ce qui n'a pas été fait (périmètre différé, assumé) - 8.9 (verrouillage de la création du venv), 8.10 (vérification des dépendances depuis l'interpréteur cible), 8.11 (contrainte de version testée pour blinkpy, O-07) : non traités, le venv de construction et d'exécution restent gérés comme avant ce lot ; - 8.13 (remplacement progressif de `print` par une journalisation structurée) : explicitement décliné. Pour un outil CLI local dont `print` *est* l'interface utilisateur (chaque message est lu par une personne dans un terminal, jamais agrégé ni interrogé), une bascule vers `logging` n'apporterait rien à l'usage réel et représenterait une réécriture mécanique de très large surface pour un gain non démontré — jugé hors de proportion, contrairement au reste du périmètre complet choisi ; - O-10 (motif `.gitignore` pour les nouveaux verrous `download`/`merge`/ `registre`) : non traité, resté hors du périmètre de cette étape. #### Tests et vérifications | Contrôle | Résultat | Durée | Verdict | |---|---:|---:|---| | Suites isolées combinées | 119 tests : 119 réussis (contre 114 avant cette étape), 0 défaut futur attendu | 7,8 s | réussi | | Dont O-06 (5 nouveaux, sous-processus avec aiohttp/blinkpy réellement bloqués) | 5/5 | inclus ci-dessus | réussi | | Chaîne historique `tests.py`, cycle complet réel (login, download, merge, serve, stop, direct simulé) | 70 contrôles observés | ~24 s | réussi, « Tout est vert » | | Compilation des 10 fichiers touchés/créés | 10/10 | < 1 s | réussi | | Ruff sur les fichiers nouveaux/modifiés du cœur (6) | 0 erreur | 0,3 s | réussi | | Ruff sur `serve.py`/`watch.py` (fichiers appelants modifiés) | erreurs pré-existantes uniquement (E402/F401 déjà présentes sur HEAD, vérifié par comparaison directe) | 0,3 s | réussi, aucune régression | | Bundle PyInstaller réel, 8 commandes exercées sur l'exécutable produit | 8/8 | ~30 s de build + secondes d'exécution | réussi | #### Mesures Aucune mesure P-01/P-10 formelle (temps de démarrage, mémoire). Écart documenté : le protocole de benchmark (`benchmarks/stage0_baseline.py`) mesure `blink2video.py` en tant que script, pas les nouveaux modules pris séparément, et sa baseline globale reste elle-même non qualifiée depuis l'étape 0 (28.2). Une future reprise formelle de ce protocole donnerait un chiffre réel au gain de démarrage que 8.7/8.8 rendent possible pour stop/open/--help (déjà démontré fonctionnel, pas encore chronométré). #### Verdict et retour arrière O-06 et O-09 sont fermés. Le point d'entrée ne contient plus de logique métier (8.6) et chaque sous-système a désormais son propre fichier, condition de sortie de la section 20.4 — mais O-07, O-10 et une partie de 8.9-8.11 restent ouverts, donc le critère de sortie formel n'est pas intégralement atteint. **Verdict local : acceptée. Verdict formel : en attente de la matrice CI sur les trois OS.** Retour arrière : plus délicat que les étapes précédentes vu l'ampleur du déplacement de code ; `git revert` du commit de cette étape reste mécaniquement possible (aucune migration de données, aucun format de fichier changé), mais toucherait dix fichiers d'un coup. Alternative plus sûre en cas de régression isolée : ne revenir que sur le fichier concerné (par exemple `blink_engine.py`) en gardant le reste du découpage, les cinq modules étant faiblement couplés par construction (graphe acyclique déjà documenté ci-dessus). ## Étape 9 — Qualification finale et release Statut : **partielle le 17 août 2026 ; la matrice complète (section 21.1) dépasse ce qu'une session locale peut couvrir**. ### 28.14 Ce qui a été fait localement, ce qui ne pouvait pas l'être #### Décision de périmètre La quasi-totalité de la matrice 21.1 (Python 3.11/3.12 × Windows/Linux/ macOS, Apple Silicon, bundle sur trois OS, test réel avec compte et 2FA, crash injecté à chaque frontière commit/rename, jeu de 600 clips cloud réel) exige une infrastructure multi-OS que cette session, sur une seule machine Windows, ne peut pas produire elle-même. Décision prise avec l'utilisateur : faire ce qui est réellement local, puis déclencher la matrice CI déjà configurée dans `.github/workflows/ci.yml` par un push — seule vraie voie vers une qualification multi-OS depuis cette position. #### Travail effectué - **9.9, volumétrie** (`benchmarks/stage0_baseline.py --profile full --only P-02,P-03,P-04,P-05`) : mesuré à 100 000 événements avec exactitude vérifiée à chaque palier : | Mesure | 100 | 1 000 | 10 000 | 100 000 | |---|---:|---:|---:|---:| | P-02 (lecture/écriture registre JSON) | 3,4 ms | 25,6 ms | 237 ms | 2 744 ms | | P-03 (mise à jour d'état) | — | — | — | 2 741 ms | | P-04 (vérification d'acquisition) | — | — | — | 7 848 ms | | P-05 (rapprochement 100 000 local × 5 000 cloud) | — | — | — | 117,6 ms | Aucune dérive quadratique observée ; P-05 confirme à grande échelle le gain déjà mesuré à l'étape 2 (index temporel + appariement Hopcroft-Karp, section 28.5). - **Incident découvert en y touchant** : `benchmarks/stage0_baseline.py` référençait encore `b2v.STATE_FILENAME`, `b2v.target_path`, `b2v._ecrire_registre`, `b2v.rapprocher`, `b2v.state_key`, `b2v.remember_download`, `b2v.is_downloaded`, `b2v.read_cloud_manifest`, `b2v.CloudClip`, `b2v.save_session`, `b2v.load_saved_session` — tous déplacés par l'étape 8. Le fichier était donc cassé pour toute mesure volumétrique depuis ce commit, sans qu'aucun test ne l'ait couvert (les benchmarks ne tournent pas dans la suite `unittest`). Corrigé avec la même méthode qu'à l'étape 8 (script de correspondance par nom), vérifié en relançant réellement la mesure plutôt qu'en se fiant à la compilation seule. - **README FR/EN** (`docs.py --check`) : les deux à jour, aucune régénération nécessaire — la table des verbes et les chemins documentés dérivent tous de `runtime.VERBES`/`runtime.app_dir()`, inchangés dans leur forme publique malgré le découpage interne. - **Passe de régression finale** : 119 tests isolés (119 réussis), suite historique `tests.py` complète verte, immédiatement avant le push. - **9.18, sans argument = start, sources et bundle** : déjà couvert en sources par les tests E-01 (étape 5) et par la suite historique ; côté bundle, vérifié manuellement à l'étape 8 sur l'exécutable PyInstaller réel (`blink2video.exe stop`, `--help`, `start --help`, etc. — section 28.13). - **Déclenchement de la matrice CI** (exécution `32044634431`, déclenchée manuellement via `workflow_dispatch` : le déclencheur `push` du workflow ne vise que `main`, pas cette branche) : premier passage réellement exécuté de ce workflow depuis l'ouverture de l'audit — chaque étape précédente avait laissé la qualification CI « à confirmer », jamais vérifiée pour de vrai. Deux défauts génériques révélés, aucun spécifique à une seule plateforme : 1. **`tzdata` absent du job « caractérisation »** (`aiohttp blinkpy` installés, jamais `tzdata`, contrairement aux jobs « chaîne vidéo » et « bundle »). Sans effet tant que `runtime.bootstrap()` n'était appelé qu'une fois, au tout début de `blink2video.py` ; l'étape 8 l'a rendu paresseux (appelé depuis `accueillir()`/`executer()`/`main()`, et par les imports déjà existants de `serve.py`/`watch.py`), ce qui a suffi à exposer ce trou dès la première exécution réelle — 9 tests en échec sur trois OS, tous avec la même trace. Corrigé dans `ci.yml`. 2. **Assertion trop stricte sur le délai d'onboarding** dans `tests.py` (`test_verbes`, ajoutée au 28.10). Touché sur `bundle windows-latest` et `chaîne vidéo macos-latest`. Deux corrections successives : - d'abord acceptée l'un ou l'autre des deux messages de fin de délai (port qui ne répond pas, ou authentification jamais confirmée) plutôt qu'un seul texte exact ; - insuffisant : un second déclenchement a montré `bundle windows-latest` échouer encore, avec dans le journal CI un message visiblement corrompu (« D�lai de connexion d�pass� »). Le comportement réel du programme n'était pas en cause — seul le décodage, par ce test, de la sortie accentuée de l'exécutable PyInstaller figé l'était. Corrigé en cherchant « Abandon », mot ASCII commun aux deux messages, plutôt qu'un texte accentué dont l'encodage n'est pas fiable d'une forme d'exécution à l'autre (source vs bundle). 3. **Sans rapport avec ce chantier, signalé sans être corrigé** : `bundle ubuntu-22.04` échoue à la récupération de secours de ffmpeg dans `build.py` (`urllib.error.HTTPError: HTTP Error 404`) — l'URL vise le tag `latest` du dépôt `BtbN/FFmpeg-Builds`, connu pour se déplacer. Pré-existant, sans lien avec les étapes 3 à 9 ; laissé pour une décision séparée sur la source de secours à utiliser. **Corrigé après coup** (demande explicite de l'utilisateur, hors du périmètre initial de l'étape 9) : le nom de fichier embarquait le numéro de version ffmpeg (`n7.1`), remplacé côté BtbN par une compilation plus récente (`n9.0`) sous le même tag `latest`, d'où le 404. `master-latest`, variante sans numéro que BtbN publie justement pour ne pas dépendre d'un nom qui se déplace, la remplace. Structure interne de l'archive (`/bin/ffmpeg`) vérifiée identique avant de committer, pas seulement supposée. Quatre déclenchements complets : | Exécution | tzdata | délai onboarding | ffmpeg secours (ubuntu-22.04) | |---|---|---|---| | `32044634431` | rouge (9 jobs) | rouge (windows, macos) | rouge | | `32051601394` | vert | rouge (windows seul, encodage bundle) | rouge | | `32052057435` | vert | vert | rouge | | `32052496444` | vert | vert | **vert** | **14 jobs sur 14 verts** à la quatrième exécution : caractérisation sur les trois OS × deux versions de Python, chaîne vidéo complète sur les trois OS, bundle réel sur les trois OS, artefacts publiés (`blink2video-windows-x86_64`, `blink2video-linux-x86_64`, `blink2video-macos-arm64`). Première exécution intégralement verte de toute la durée de cet audit. #### Ce qui n'a pas été fait, et pourquoi ce n'était pas possible ici - 9.10 (600 clips cloud réels), 9.14 (test réel opt-in avec secrets locaux) : demandent un compte Blink réel, explicitement exclu de toute automatisation par la section 12.5 et par la posture de tout ce chantier depuis l'étape 0 ; - 9.11 (backlog USB > 10 minutes), 9.13 (crash injecté à chaque frontière commit/rename) : demandent du matériel Blink réel ou une instrumentation de fautes plus lourde qu'un test unitaire, non construite dans ce plan ; - 9.4 (macOS Apple Silicon) : couvert par le runner `macos-latest` de la CI déclenchée (Apple Silicon côté GitHub Actions), pas testé sur le Mac mini M1 disponible par ailleurs (Scaleway) — resterait une option de test manuel si la CI échoue spécifiquement sur cette plateforme et qu'une investigation directe devient nécessaire ; - 9.15/9.16/9.17 (onboarding complet : 2FA, session expirée, navigateur indisponible, headless, autostart) : les scénarios *simulés* sont déjà couverts par les tests E-01 de l'étape 5 (section 28.10) ; un parcours réel avec un compte Blink reste hors CI par construction, comme 9.10/9.14. #### Verdict et retour arrière La partie réalisable sans infrastructure multi-OS est faite et documentée : volumétrie à 100 000 événements sans dérive quadratique, README cohérents, régression complète verte, bundle réel exercé sur trois OS via la CI. La matrice CI elle-même, déclenchée et suivie jusqu'au bout (quatre exécutions, quatre défauts génériques trouvés et corrigés en cours de route, tous antérieurs à cette étape mais jamais révélés faute d'exécution réelle), finit à **14 jobs verts sur 14** : caractérisation × 3 OS × 2 versions de Python, chaîne vidéo complète × 3 OS, bundle réel × 3 OS avec artefacts publiés. Le critère de release de la section 21.2 reste néanmoins **non atteint dans son ensemble** : les tests avec un compte Blink réel ainsi que les scénarios matériel/crash de 9.10-9.14 restent hors CI par construction, et rien ne remplace un usage réel prolongé. **Verdict : qualification locale acceptée, qualification CI multi-OS atteinte à 14/14, qualification de release complète non atteinte** (compte réel et scénarios matériel non couvrables par CI). Retour arrière possible sans risque sur les correctifs de ce lot (`benchmarks/stage0_baseline.py`, `.github/workflows/ci.yml`, `tests.py`, `build.py`) : aucun changement de comportement de production, uniquement des références cassées et des défauts de configuration/test/build. ### 28.15 Hors séquence — comparatif des mécanismes de direct (MJPEG / MSE / WebRTC) #### Décision de périmètre Demande explicite de l'utilisateur, hors plan initial : accélérer/moderniser le visionnage du direct, projet destiné à une publication GitHub/Reddit. Trois approches comparées sur la caméra réelle « Salon » (Blink Mini, intérieur, wifi stable — choisie plutôt que « jardin », plus éloignée, pour isoler la mécanique de relais du bruit wifi côté caméra) : MJPEG réglé (déjà en production), MSE/fMP4 (remux sans réencodage), WebRTC (aiortc). #### Mesures réelles | | MJPEG réglé (prod) | MSE/fMP4 | WebRTC (aiortc) | |---|---|---|---| | 1ère image (Salon) | 3,4 s | non chronométré en continu (sessions Blink trop courtes ce jour-là) | 16,3 s | | CPU en régime | quasi nul (0-5 %, sous-processus ffmpeg) | quasi nul par construction (remux, pas de réencodage) | quasi nul (0-1,5 %, mesuré sur tout le process Python) | | Mémoire | ~78 Mo (ffmpeg seul) | non isolée (même process) | ~92 Mo (process entier : aiohttp+blinkpy+av+aiortc) | | Nouvelle dépendance | aucune | PyAV (`av`) | aiortc + cryptography + aioice + pylibsrtp (venv isolé nécessaire) | #### Travail effectué - **MSE/fMP4** (`send_live_mse`, route `/live-mse/`) : remux direct via PyAV (`av.open` en lecture, remux vers un conteneur MP4 fragmenté en écriture, aucun sous-processus ffmpeg). Chaîne de codec MSE (`avc1.PPCCLL`) lue directement dans le SPS du flux réel plutôt que devinée — nécessaire : la caméra Salon encode en H.264 High profile (`avc1.640028`), pas Baseline comme l'hypothèse initiale le supposait, confirmant que la détection dynamique était le bon choix. - **WebRTC** (`send_webrtc_offer`/`send_webrtc_stop`, aiortc) : connexion réelle établie et confirmée avec la caméra Salon (état `connected`, vidéo décodée côté navigateur). Bug réel trouvé et corrigé en cours de route : `MediaPlayer()` d'aiortc ouvre et sonde le flux de façon synchrone et bloquante ; appelé tel quel dans la coroutine partagée avec le reste de l'appli, un flux qui traîne gèle la boucle asyncio unique dont dépendent *aussi* le direct MJPEG, MSE et l'actualisation du système — pas que cette requête. Corrigé en isolant l'ouverture dans un thread d'exécuteur borné par un délai (`loop.run_in_executor` + `asyncio.wait_for`). - **Décision utilisateur, avec les chiffres en main** : MSE retenu. WebRTC écarté malgré la préférence initiale de l'utilisateur pour ce protocole : son avantage (latence, franchissement NAT) sert un usage multi-pairs par Internet, pas un spectateur unique sur une page servie en local — l' avantage ne s'exprime pas ici, le coût (dépendance lourde, négociation plus lente à l'usage que le MJPEG, piège de blocage découvert en direct) reste entier. Code WebRTC entièrement retiré (serveur, routes, JS, venv `.venv-webrtc`) après la décision, pour ne pas laisser de code mort. - **Bouton principal « Voir en direct »** basculé de `watch()` (MJPEG) à `watchMse()`. - **Deux bugs réels trouvés et corrigés via des essais sur la caméra réelle avec l'utilisateur** : 1. CSS : `.live img { width:100%; height:100%; ... }` ne visait que ``, pas `