# Rapport d’analyse de code — 26 septembre 2026 Base examinée : **blink2video 0.14.2**, commit **`f20cc34`**. Le dépôt était propre au début de l’analyse. Aucun correctif applicatif n’a été apporté. L’analyse retient **8 anomalies**, dont **2 prioritaires pour l’intégrité des archives**. Les priorités décrivent l’impact lorsque les conditions indiquées sont réunies, pas une fréquence mesurée chez les utilisateurs. | Référence | Priorité | Anomalie | Validation | | --- | --- | --- | --- | | B01 | P1 | Deux clips cloud distincts proches peuvent devenir un seul téléchargement | Reproduction Python isolée avec doubles locaux | | B02 | P1 | Une fusion filtrée peut remplacer un agrégat complet par un agrégat incomplet | Lecture du chemin complet et des tests existants | | B03 | P2 | Le compteur de nouveaux clips produit `NaN` et empêche l’actualisation associée | Fonction JavaScript réelle exécutée avec une API simulée | | B04 | P2 | La première configuration peut rouvrir son dialogue obligatoire après validation | Lecture croisée serveur/interface et simulation JavaScript partielle | | B05 | P2 | L’icône ouvre le mauvais port après une surcharge de `--port` | Lecture croisée CLI, icône et tests | | B06 | P2 | Une exception pendant une étape ponctuelle échappe au nettoyage des processus persistants | Lecture du cycle de vie | | B07 | P2 | Le démarrage automatique Linux casse les chemins contenant des espaces | Lecture du générateur et documentation systemd | | B08 | P2 | Le démarrage automatique macOS produit un plist invalide avec certains chemins | Lecture du générateur XML | **P1** : risque de sauvegarde incomplète ou de perte d’un contenu déjà assemblé ; à traiter en premier. **P2** : défaut fonctionnel à corriger, avec un déclencheur ou un périmètre plus limité. ## B01 — Un second clip cloud distinct est ignoré comme doublon **Localisation :** `blink_engine.py:510–518`, `blink_registre.py:426–467`. L’inventaire initial est construit dans `blink_engine.py:404–428`. L’inventaire utilise un rapprochement global, mais l’exécution refait une recherche individuelle avec `_trouver_entree()`. Cette recherche accepte une entrée de la même caméra à ±2 secondes, sans imposer que deux identifiants cloud distincts restent distincts. Elle ne reçoit pas non plus d’ensemble `consumed` empêchant de réutiliser une association déjà consommée. Après le téléchargement du premier clip, celui-ci devient donc une entrée susceptible de faire ignorer le second, alors que le plan initial prévoyait bien deux acquisitions. **Reproduction effectuée :** registre vide, deux clips synthétiques de même caméra/réseau, identifiants `1` et `2`, horodatages `12:00:01` et `12:00:02` UTC, `overwrite=False`. Le manifeste et les transferts sont des doubles locaux ; la validation MP4 est simulée et la suppression automatique désactivée. L’appel à `traiter_cloud()` annonce deux clips à rapatrier puis renvoie : ```text CloudResult(downloaded=1, adopted=0, skipped=1, failed=0) Appels download_to par clip : [1, 0] Identifiants cloud conservés dans le registre : ['1'] ``` **Impact :** sauvegarde silencieusement incomplète, sans échec signalé. Cette reproduction démontre le défaut de déduplication ; elle ne mesure pas la fréquence à laquelle le service Blink fournit des événements aussi proches et ne démontre pas une suppression distante du clip ignoré. **Correction proposée :** conserver les associations décidées par le plan ; pour des entrées cloud munies d’identifiants distants, distinguer les identifiants différents. Réserver la tolérance temporelle aux rapprochements entre sources ou aux anciennes entrées ambiguës, en conservant une consommation unique des associations. **Test à ajouter :** exécuter le parcours cloud complet avec deux identifiants distincts à une seconde d’écart, puis vérifier deux transferts et deux entrées. `test_stage2_identity.py:176–209` protège déjà la distinction d’événements proches au niveau des fonctions d’identité, sans couvrir cette revalidation pendant le transfert. ## B02 — La protection des agrégats incomplets disparaît avec un filtre **Localisation :** `merge_daily.py:1993–1998`, `2135–2152`, puis `1653–1726` dans `build_periods()`. La protection des agrégats ne se déclenche que si une journée indisponible figure dans les journées sélectionnées : ```python if indisponibles & selected.keys(): return 1 ``` Or `build_periods()` collecte ensuite toutes les vidéos journalières du dossier, indépendamment de `--date` et `--camera`. Le filtre réduit donc la portée de la protection, sans réduire celle de la reconstruction hebdomadaire/mensuelle. **Scénario déclencheur :** une semaine existante contient les journées A et B. Les clips bruts et la vidéo journalière de A deviennent indisponibles, mais l’agrégat complet existe encore. Une fusion sélectionne uniquement B, avec les agrégats activés. A n’appartient pas à `selected`, donc la garde laisse passer. La semaine est reconstruite à partir de B et remplace l’ancienne version complète. Si une période ne conserve aucune journalière, le nettoyage des périodes obsolètes peut également supprimer son agrégat. **Impact :** perte du contenu de A dans un fichier déjà assemblé. Le risque est particulièrement important si cet agrégat est devenu la seule copie disponible. Aucun fichier réel n’a été supprimé ni remplacé pendant cet audit : ce constat est établi statiquement. **Correction proposée :** protéger chaque période/caméra touchée par l’ensemble des indisponibilités, y compris hors du filtre journalier. À défaut, reporter tous les agrégats dès qu’une indisponibilité est connue. Le nettoyage des périodes obsolètes doit respecter la même protection. **Test à ajouter :** deux jours d’une même période, ancien agrégat complet, sources et journalière du premier jour manquantes, filtre sur le second ; vérifier que l’agrégat et son état restent inchangés. Les tests de `test_merge_daily_source_partielle.py` couvrent la protection sans filtre, pas ce cas. ## B03 — Les nouveaux clips ne déclenchent plus le compteur ni son actualisation **Localisation :** `serve_app.js:798–815`, contrat serveur dans `serve.py:947–951`. Le serveur renvoie maintenant : ```json {"total_known": {"clip": 10, "direct": 0}} ``` Mais `heuresDePassage()` soustrait encore l’objet entier au nombre de clips de `/api/passages` : ```javascript Math.max(0, (etat.clips || 0) - data.total_known) ``` Le résultat est `NaN`, qui est faux dans les conditions JavaScript suivantes. Le message de nouveaux clips reste vide et le rechargement automatique déclenché par leur arrivée n’est pas demandé. **Reproduction effectuée :** extraction de la fonction réelle dans un contexte JavaScript isolé, sans navigateur ni réseau. Avec 11 clips côté passages, 10 côté galerie, une date de passage connue et l’actualisation cochée : **0 appel** à `rechargerEnArrierePlan()` avec l’objet actuel, contre **1 appel** avec l’ancien total numérique. **Impact :** l’utilisateur ne voit pas l’annonce des nouveaux clips et la galerie peut rester périmée. Les autres mécanismes de rafraîchissement ne sont pas tous concernés. **Correction proposée :** comparer avec `data.total_known.clip`, en traitant explicitement l’absence initiale de ce compteur. **Test à ajouter :** fournir à `heuresDePassage()` le schéma exact de `/api/clips`, avec un clip supplémentaire, puis vérifier le message et le rechargement selon l’état de la case automatique. ## B04 — La transition de première configuration ne gère pas le nouveau jeton **Localisation :** `serve_app.js:2979–2998`, `41–46`, `2710–2727`, `3140–3145` ; `serve.py:3444–3446`, `4863–4868` ; remplacement du serveur dans `blink_cli.py:779–845`. Après validation de la première configuration sans changement de port, le parent remplace le serveur temporaire. Le nouveau processus crée un nouveau jeton. L’ancienne page continue pourtant de sonder `/api/status` avec son ancien `X-Blink-Token` et reçoit une réponse HTML 403. `attendreFinConfigurationInitiale()` tente directement `.json()`, ignore l’erreur et n’atteint pas la navigation vers `location.pathname` qui devait retirer `?setup=1`. En parallèle, les sondages utilisant `lireJSON()` provoquent un `location.reload()`, lequel conserve ce paramètre. La page rechargée appelle alors `ouvrirReglages(true)` : ce `true` reste prioritaire même lorsque le serveur indique que la configuration est terminée. Fermer et Échap sont désactivés dans ce dialogue initial. **Validation :** le chemin serveur/interface a été relu ; une simulation des fonctions JavaScript avec une réponse HTML 403 confirme l’absence de navigation dans le sondage initial, puis l’appel à `reload()` par `lireJSON()` sans retrait de `setup=1`. Le parcours complet avec deux vrais serveurs et un navigateur n’a pas été exécuté. **Impact :** retour au dialogue obligatoire après une configuration déjà validée ; de nouvelles validations peuvent entretenir des relances inutiles. Le cas avec changement de port suit une autre branche et n’est pas inclus dans ce constat. **Correction proposée :** gérer explicitement la rotation du jeton pendant cette transition et obtenir une page fraîche sans le paramètre de configuration. Vérifier aussi l’état serveur avant de forcer à nouveau le dialogue initial ; conserver la protection par jeton. **Test à ajouter :** remplacement du serveur par un processus à jeton différent sur le même port ; vérifier la navigation vers `/` et l’absence de réouverture obligatoire. Le test actuel simule directement un JSON `initial_setup: false`, sans la réponse 403 intermédiaire. ## B05 — Le port transmis à l’icône contredit celui du serveur **Localisation :** `blink_cli.py:939–948`, `1103–1106`, `1160` ; utilisation dans `tray.py:101–105`. `start` place les options enregistrées avant les options explicites. Le serveur retient la dernière occurrence de `--port`, mais le superviseur utilise `arguments.index("--port")`, donc la première. La forme `--port=9555` n’est pas reconnue par cette extraction. **Exemple :** port enregistré 8765, lancement `start --port 9555`. Le serveur écoute sur 9555, mais le menu Ouvrir de l’icône reçoit 8765 et ouvre une adresse incorrecte. **Impact :** navigation impossible depuis l’icône, ou ouverture d’un autre service si le port enregistré est occupé. Concerne la branche supervisée avec une icône disponible. **Correction proposée :** réutiliser `_port_demande(arguments)`, qui gère déjà les deux syntaxes et la priorité de la dernière occurrence, puis appliquer le repli configuré si nécessaire. **Test à ajouter :** intercepter le port passé à `tray.executer()` après une surcharge, avec et sans `=`. Le test de surcharge à l’accueil ne vérifie pas cette transmission à l’icône. ## B06 — Une interruption avant la surveillance laisse les processus persistants actifs **Localisation :** `blink_cli.py:1014–1049`, puis `1149–1172`. Les processus persistants démarrent avant les étapes ponctuelles. Ces dernières passent par `runtime.lancer()` en dehors du `try/finally` qui appelle `nettoyer_lances()`. **Scénario :** composition `serve download`, puis `KeyboardInterrupt` ou `OSError` pendant le lancement/l’exécution de l’étape ponctuelle. L’exception quitte cette branche avant d’atteindre le nettoyage. Sous POSIX, le serveur enfant possède explicitement sa propre session ; le signal du terminal ne suffit donc pas à l’arrêter avec le parent. **Impact :** le superviseur peut disparaître alors que le serveur continue à tourner. Les fiches des survivants restent normalement exploitables par `stop` : il ne s’agit pas nécessairement de processus devenus introuvables. Un simple code de sortie non nul du téléchargement ne déclenche pas ce défaut. **Correction proposée :** couvrir toute la durée de vie des enfants par un `try/finally`, y compris les étapes ponctuelles et les préparatifs précédant la surveillance. **Test à ajouter :** enfants factices, puis `runtime.lancer()` levant successivement une `OSError` et une `KeyboardInterrupt` ; vérifier le nettoyage dans les deux cas. Aucun signal ni processus réel n’a été utilisé pour ce constat. ## B07 — Le service Linux ne préserve pas les arguments contenant des espaces **Localisation :** `autostart.py:368–386`. La ligne `ExecStart` est produite par une simple concaténation : ```python f"ExecStart={' '.join(commande(quoi))}\n" ``` Un exécutable `/home/user/Blink Videos/blink2video` devient deux éléments, et un chemin de script ou une option contenant des espaces subit le même problème. La syntaxe des commandes systemd nécessite de préserver explicitement les frontières des arguments ; elle n’est pas celle d’un shell généraliste. Voir la [documentation officielle systemd, « Command lines »](https://github.com/systemd/systemd/blob/main/man/systemd.service.xml#L1350). **Impact :** le service ne démarre pas correctement depuis un emplacement pourtant valide. Les retours de `systemctl` sont en outre ignorés avant l’annonce d’installation réussie. **Correction proposée :** sérialiser les arguments selon les règles systemd, y compris leurs échappements, et vérifier le résultat du chargement/démarrage. **Test à ajouter :** exécutable, chemin de script et option contenant un espace ; vérifier leur conservation comme arguments distincts, puis compléter par un test d’unité sur Linux. Aucun service systemd n’a été installé pendant l’audit. ## B08 — Les chemins macOS ne sont pas échappés dans le plist **Localisation :** `autostart.py:305–326`. Les éléments de `ProgramArguments` et `WorkingDirectory` sont insérés directement dans du XML. Un dossier valide comme `/Users/user/Blink & Videos` produit un `&` brut dans ``, ce qui rend le plist invalide. **Impact :** le démarrage automatique échoue pour certains chemins ou arguments. Le résultat de `launchctl load` est ignoré, puis la fonction annonce tout de même l’installation. **Correction proposée :** générer le plist avec `plistlib`, puis contrôler le résultat de `launchctl` avant d’annoncer un succès. **Test à ajouter :** sérialiser puis relire le plist avec `plistlib.loads()` pour des chemins contenant `&` et `<`. Une vérification macOS reste nécessaire pour le chargement effectif ; elle n’a pas été effectuée ici. ## Périmètre et limites de validation La revue a porté sur les chemins concernés de `blink_engine.py`, `blink_registre.py`, `blink_models.py`, `merge_daily.py`, `watch.py`, `serve.py`, `serve_app.js`, `blink_cli.py`, `runtime.py`, `autostart.py`, `tray.py`, `maj.py`, `deploy.py`, `blink_auth.py`, `blink_ts_demux.py` et `blink_webrtc.py`, ainsi que sur les tests pertinents et l’audit du 24 septembre. Il s’agit d’une revue ciblée, pas d’une preuve d’absence de bugs dans le reste du dépôt. - B01 a été reproduit dans un dossier temporaire avec des données synthétiques et des dépendances simulées. - B03 a été reproduit en exécutant la fonction JavaScript extraite du fichier, avec un environnement simulé. - B04 a fait l’objet d’une simulation JavaScript des sondages et d’une vérification statique de la transition ; aucun parcours navigateur complet n’a été exécuté. - B02 et B05 à B08 reposent sur l’analyse statique. Leurs tests de non-régression sont proposés, pas exécutés ni ajoutés. - La suite unitaire complète, `tests.py`, les builds et les essais Linux/macOS n’ont pas été lancés. Aucun résultat de suite « verte » n’est revendiqué. - Après le signalement de fenêtres PowerShell/DOS, les commandes console ont été arrêtées. La suite de l’analyse a utilisé une lecture directe des fichiers et des simulations JavaScript en mémoire, sans processus externe. - Aucun compte Blink, caméra réelle ou secret local n’a été utilisé. Aucun appel de suppression distante n’a été effectué. - `BlinkLiveStream.auth` et son contexte TLS n’ont été ni modifiés ni remplacés. Les sources applicatives restent inchangées. Ordre de traitement proposé : protéger d’abord les acquisitions et agrégats (**B01–B02**), corriger les parcours de galerie et de configuration (**B03–B04**), puis la supervision et le démarrage automatique (**B05–B08**). Chaque correctif devrait être accompagné du scénario de non-régression décrit ci-dessus.