# Audit du 24 septembre 2026 — bugs, améliorations, optimisations État : sources basées sur `d2282f8` (0.13.3). Lecture complète des modules cœur (`serve.py`, `runtime.py`, `blink_engine.py`, `blink_registre.py`, `blink_models.py`, `blink_auth.py`, `blink_ts_demux.py`, `blink_webrtc.py`, `blink_cli.py`, `maj.py`, `watch.py`, `tray.py`, parties critiques de `merge_daily.py`) et contrôle ciblé de `serve_app.js` (échappement HTML : `h()` systématique et CSP à nonce, rien à signaler). Les audits précédents (`AUDIT-2026-08-13.md`, `AUDIT-2026-09-07.md`) ont été pris comme acquis : rien de ce qui y est déjà corrigé n'est répété ici. Le TLS du relais Live View (`BlinkLiveStream.auth`) n'a pas été touché (voir `AGENTS.md`). ## Corrigé dans cette branche Chaque correction a son test de non-régression, vérifié en échec sur le code d'origine puis en succès après correction. ### 1. Course sur `merge_daily.save_json` — requêtes en échec au chargement de la page `save_json` écrivait toujours dans `path.with_suffix(".tmp")`, un nom partagé par tous les écrivains. `serve.py` tourne sur un `ThreadingHTTPServer` et `load()` (`serve_app.js`) demande `/api/clips` et `/api/videos` en parallèle. Dès qu'il existe au moins un enregistrement du direct et une vidéo assemblée, les deux routes réécrivent `assembled_durations.json` : le second `replace` ne trouve plus le temporaire consommé par le premier. Reproduction : 2 threads × 300 écritures → **197 `FileNotFoundError`**. Côté page, la requête touchée échoue sans réponse (connexion coupée). Sous Windows, un temporaire unique ne suffit pas : `os.replace` échoue en `PermissionError` (« Access is denied ») quand un autre écrivain ou un lecteur tient la cible ouverte au même instant (162 échecs sur 600 en CI Windows sur la première version de cette correction). Correction : temporaire unique par appel via `runtime._ecrire_texte_atomique`, un verrou par fichier pour les threads d'un même processus, et quelques nouvelles tentatives rapprochées sur `PermissionError` (lecteurs d'un autre thread ou processus, antivirus). Test : `test_merge_daily_save_json_concurrence.py`, dont un cas qui simule la sémantique Windows de `os.replace` sur tous les systèmes. ### 2. Onboarding : port, fuseau et hôte de confiance des réglages ignorés `blink_cli.accueillir()` lançait `serve` avec le seul supplément tapé à la main et sondait `_port_demande(supplement) or 8765`. Avec un port personnalisé dans les réglages (souvent choisi précisément parce que 8765 est pris), une session expirée rendait la reconnexion impossible : le serveur temporaire tentait 8765, et le navigateur s'ouvrait sur ce port. Même problème pour `--trusted-host` (page de connexion en 403 via tunnel). Correction : même bloc fixe que la composition complète (`runtime.standard()[1:LONGUEUR_BLOC_SERVE]`), le supplément gardant la priorité. Tests : `test_blink_cli_supervision.py` (deux cas ajoutés). ### 3. Suppression manuelle : faux « supprimé de la source » après renumérotation USB `_marquer_sources_supprimees` marquait `source_deleted` toute entrée USB du module dont le numéro (tiré du nom de fichier) manquait au manifeste. Or le code affirme lui-même, à trois endroits, que le Sync Module renumérote ses clips lors d'une réindexation — et la suppression, elle, s'en protège en rapprochant par caméra et instant. Après une réindexation, tous les clips encore présents mais renumérotés étaient marqués, ce qui **masquait définitivement leur case « Supprimer »** dans la galerie. Correction : n'est marquée absente qu'une entrée dont ni le numéro, ni le couple (caméra, instant ±2 s) ne figurent au manifeste — hors clips que ce lot vient de supprimer. Règle strictement plus prudente ; un faux négatif reste bénin (la demande suivante répond `deja_absent` et marque alors). Test : `test_serve_selection_blink.py`. ### 4. Hôte de confiance homonyme d'un verbe, ou commençant par « - » `runtime.standard()` écrit toujours `--trusted-host `, mais cette option manquait à `_OPTIONS_VALEUR_UNIQUE` : une valeur `watch`, `list`, `open`… ouvrait un nouveau groupe dans `decouper_verbes()`, et `serve` refusait de démarrer faute de valeur. De même, le validateur acceptait `-x`, qu'argparse lit comme une option. Corrigé des deux côtés. Tests : `test_blink2video_audit.py`, `test_serve_reglages_validation.py`. ### 5. `do_POST` : corps JSON non-objet, octets non UTF-8, `Content-Length` négatif Un corps `[]`, `42` ou `null` levait `AttributeError` sur `payload.get` ; des octets non UTF-8 levaient `UnicodeDecodeError` (non intercepté) ; un `Content-Length: -1` faisait `rfile.read(-1)`, qui attend la fermeture d'une connexion HTTP/1.1 persistante et bloquait le thread. Tous répondent désormais 400. Seuls des clients déjà munis du jeton y avaient accès : robustesse plus que sécurité. Test : `test_serve_post_routage.py`. ### 6. Optimisation : `known_identities()` relisait tout le registre à chaque requête média Chaque `/thumb/clip/…` et chaque requête `Range` du lecteur vidéo passaient par un parse JSON complet du registre : **~27 ms par requête pour 10 000 clips** (5,4 Mo) sur une machine rapide, bien davantage sous Windows 7 — plusieurs secondes de CPU pour une page de vignettes. Mémorisé désormais tant que le fichier ne change pas (clé : chemin, `mtime_ns`, taille, inode ; le registre est toujours remplacé atomiquement, donc l'inode change à chaque écriture). Appels suivants : ~5 µs. Test : `test_serve_known_identities_cache.py`. ## Non corrigé — à arbitrer ### Bugs probables - **Identité de processus sous Linux sensible aux sauts d'horloge** (`runtime.identite_processus`, `ps -o lstart=`). `lstart` est recalculé à chaque appel depuis `btime` (`/proc/stat`), que le noyau décale quand l'horloge murale est réglée d'un coup (NTP en mode « step », VM après reprise, réglage manuel). Un décalage d'une seconde ou plus change l'identité de *tous* les processus vivants : `verrou()` purge alors le verrou d'un propriétaire bien vivant, `lire_instances()` efface des fiches actives et `stop` refuse de tuer (« PID réattribué »). Linux et Docker seulement (Windows et macOS mémorisent une heure de création fixe). Non reproduit : impossible de décaler l'horloge dans l'environnement d'audit. Piste : champ 22 de `/proc//stat` (ticks depuis le démarrage, insensible aux réglages d'horloge, et sans sous-processus), en acceptant l'ancien format le temps d'une migration. - **Agrégats hebdomadaires/mensuels volumineux** (`merge_daily.concat_copy`). La copie de flux `-c copy +faststart` est coupée après 300 s fixes (`SILENCE_MAX`). Un mois chargé (≈ 10 Go pour une caméra active) demande une copie puis une réécriture complète pour `faststart` : sur disque dur lent ou USB 2.0, 300 s ne suffisent pas. L'échec déclenche alors le repli `run_ffmpeg_batch`, c'est-à-dire un **réencodage complet du mois**, recommencé chaque jour où l'empreinte change. Ordre de grandeur estimé, non mesuré sur matériel réel. Pistes : `-progress pipe:1` et chien de garde au silence comme `run_ffmpeg_batch`, ou délai proportionnel à la taille des parties. Hebdo et mensuel sont désactivés par défaut, ce qui limite l'exposition. - **Verrous définitivement bloqués après un plantage mal placé** (`runtime.verrou`). Un processus tué entre `O_CREAT|O_EXCL` et l'écriture laisse un verrou vide, lu « illisible » à jamais ; tué dans la section de purge, il laisse un `.purge` qui interdit toute purge future. Fenêtres de quelques microsecondes, mais conséquence permanente (intervention manuelle). Piste : écrire PID et identité dans `.purge`, traiter un verrou vide plus vieux que quelques secondes comme périmé. - **Superposition du registre** (`blink_registre._ecrire_registre`). Chaque sauvegarde réécrit la copie complète de l'appelant ; seule l'exclusion a une fusion dédiée. Un champ posé par un autre processus entre la lecture et l'écriture (`source_deleted` par la suppression manuelle pendant un passage de téléchargement) est donc perdu. Impact faible : la case « Supprimer » réapparaît, et une nouvelle demande répond `deja_absent`. ### Robustesse et maintenance - **Journaux sans rotation** : `direct.log`, `serve_erreurs.log`, `watch.log`, `merge.log`, `maj.log` croissent indéfiniment. `serve_erreurs.log` reçoit un bloc par requête refusée en 403 : avec `BLINK_BIND`, un balayage du réseau suffit à le faire grossir sans limite. - **Direct MSE** : `-loglevel info` y est marqué « temporaire ». À ce niveau, ffmpeg termine ses lignes de progression par `\r`, que `readline()` ne découpe pas : vérifié, 7 mises à jour en 4 s arrivent en une seule ligne de 719 octets, soit environ 60 Ko d'une traite, à la fin de chaque direct de 5 min. Ajouter `-nostats` ou revenir à `error`. - **`BlinkSession.call`** : tant que `connect_saved()` échoue, chaque appel crée une `ClientSession` jamais fermée, soit une fuite de connecteurs. - **`LoginFlow`** : les boucles asyncio sont arrêtées, jamais fermées. - **`runtime.marquer()`** : écriture non atomique de `.blink_passages.json` et lecture-modification-écriture sans verrou entre download, watch et merge. - **Caches sans purge** : `_VERROUS_MINIATURE`, `_DURATIONS`, et les JSON `excluded_durations`/`assembled_durations` ne retirent jamais une entrée dont le fichier a disparu. - **WebRTC** : le type de charge utile dynamique `110 + 2n` dépasse 127 au 9ᵉ profil H.264 distinct vu par le processus. Le PTS 33 bits n'est pas traité au rebouclage : environ 0,3 % des sessions de 5 min perdent ensuite leur cadencement. - **`/api/choisir-dossier`** crée une fenêtre Tk depuis un thread HTTP. Sous macOS, Tk exige le thread principal, avec un risque de plantage du processus en mode sources. Le bundle n'est pas concerné (tkinter exclu). - **Traduction** : plusieurs messages du serveur restent en français seul (`_slot_occupe_message`, erreurs de réglages, du direct et de connexion), alors que l'interface est bilingue. - **Mise à jour** : l'empreinte SHA-256 vient du même canal (GitHub) que l'archive, si bien qu'un compte GitHub compromis suffit à publier un binaire accepté. Piste : signature détachée (minisign, Sigstore). ### Optimisations - `runtime.traduire()` → `lire_langue()` → `app_dir()` : deux lectures de fichier et un `resolve()` par message affiché, y compris dans les boucles par clip. - `processus_vivant()` et `identite_processus()` lancent `ps` à chaque appel sous Linux et macOS : un par verrou pris, et un toutes les 3 s via `/api/travail` pendant chaque calcul. `/proc` suffirait sous Linux. - `/api/passages` relit tout le registre toutes les 60 s pour n'en compter que les entrées : même cache que `known_identities()` possible. - `collect()` : jusqu'à trois `stat` par clip de la fenêtre, et `valid_mp4` sur chaque enregistrement du direct à chaque `/api/clips`. ## Vérifications - Suite unitaire complète (`python -B -m unittest discover`, CPython 3.11, Linux) : **914 tests réussis**, 9 ignorés (896 avant l'audit, 18 ajoutés). - `python tests.py` (chaîne vidéo et interface, ffmpeg 6.1 de la distribution) : tout est vert. - `python docs.py --check` et `python -m compileall -q .` : OK. - Non exécuté ici : CPython 3.8 (build Windows 7), Windows et macOS. - Aucun réveil de caméra, aucune suppression distante, aucune session Blink : tout est simulé ou local.