Ctrl K
ComunidadGuías…

BACKLOG-FAIT.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Backlog : ce qui est fait

Les entrées de BACKLOG.md une fois traitées, mot pour mot, avec leur statut en tête.
Rien n'est perdu : ce fichier est l'historique, BACKLOG.md ne garde que l'ouvert.

(Entrées du 2026-08 au 2026-10, dans l'ordre où elles étaient écrites.)

- **Indicateur de mise a jour pas assez visible.**

> **Statut : FAIT le 2026-10-07 : le bouton « Réglages… » commun (nico579-commons 0.4.8) donne à lidar2map, gpxsolar et watch2notif la vérification des mises à jour que blink2video avait déjà ; la pastille de mise à jour est servie par les exécutables (elle était en 404 avant, les .spec n'embarquaient pas le fichier).**

  Source : reddit/SR_gAr, r/blinkcameras, 2026-08-20. A dit avoir ete
  "perdu dans toutes les infos" et ne pas avoir vu qu'une mise a jour etait
  disponible, a cru manquer quelque chose. Signal faible (un seul retour)
  mais a surveiller : si ca revient, revoir la visibilite du bouton/de la
  bascule "Mettre a jour" dans reglages (et maintenant dans le menu de
  l'icone tray, 28.58).

- **Latence du direct (MSE) : piste WebRTC construite, active derriere un

> **Statut : FAIT et validé en usage réel le 2026-09-03 (WebRTC est le protocole par défaut).**

  drapeau, validee en usage reel (2026-09-03).** Source : meme fil reddit/cutthin que l'entree Tester-XR.exe
  ci-dessus (plainte initiale sur la lenteur du direct des cameras
  eloignees), approfondi en session le 2026-09-03 apres que l'utilisateur a
  mesure a la main Salon : 10s sur blink2video contre 3s sur l'appli
  officielle Blink. Diagnostic instrumente (horodatage reel ajoute a
  _journal_direct_mse/serve.py et au recv() patche de blink_engine.py,
  direct.log) : l'essentiel du delai n'est pas cote Blink mais dans
  blink2video meme, structurel au MSE - ffmpeg doit voir passer le SPS
  avant de pouvoir ecrire un entete MP4 (empty_moov), pas une histoire de
  reglage. Baisser -analyzeduration/-probesize (5000000 -> 1500000,
  serve.py) a ete teste sans regression sur jardin (la camera batterie
  lente a l'origine du reglage genereux), a garder.

  Prototype WebRTC (aiortc + blinkpy, venv isole, aucune modification du
  process en direct) confirme un vrai gain, mesure deux fois par camera :
  Salon 9,3s (MSE) -> 4,10s (WebRTC decodage+reencodage) -> 3,23s (WebRTC
  passthrough, sans reencodage) ; jardin ~15,8s (MSE, mesure sur Terrasse1,
  meme materiel) -> 6,81s -> 5,60s. Facteur 2,3 a 2,9x selon camera et
  mode. Le mode passthrough (equivalent WebRTC du -c:v copy deja utilise en
  MSE, evite le cout CPU du reencodage) a demande de monkey-patcher
  aiortc.codecs.CODECS["video"] pour declarer le vrai profil H.264 de la
  camera (High, 640028) : aiortc n'annonce que du Baseline en dur
  (42001f/42e01f). Verifie qu'aucune solution officielle n'existe cote
  aiortc (issue aiortc/aiortc#944, fermee sans suite ; la reserve du
  mainteneur porte sur l'encodage, pas sur le passthrough, ne s'applique
  pas a notre cas).

  Construit dans le depot (pas juste un prototype jetable) : blink_webrtc.py
  (module optionnel, DISPONIBLE=False proprement si aiortc absent), route
  POST /live-webrtc/<name> dans serve.py, watchLive()/watchWebRTC() dans
  serve_app.js avec repli automatique sur watchMse() si la negociation
  echoue. Active par la variable d'environnement BLINK_DIRECT_WEBRTC=1, pas
  encore un reglage de la page web. aiortc + cryptography/pyopenssl/cffi
  plafonnes dans requirements.in (cffi<2 : le python systeme partage de
  l'utilisateur porte aussi timezonefinder, sans rapport avec blink2video,
  qui exige cffi<2 - non pertinent si ce depot passe un jour a un venv
  dedie).

  Incident reel a l'activation (2026-09-03) : premiere tentative restee
  bloquee sur "Reconnexion...", .blink_hub.lock jamais libere (MODULE_SLOT
  pareil), plus aucun direct possible - meme MSE, verrou partage. Cause :
  RTCPeerConnection() sans configuration explicite retombe sur le defaut
  d'aiortc (stun:stun.l.google.com:19302, aiortc/codecs/rtcicetransport.py
  RTCIceGatherer.getDefaultIceServers), et setLocalDescription() attend la
  fin de la collecte de candidats avant de rendre la main - un blocage
  reseau/pare-feu dessus bloque tout indefiniment, jamais de
  connectionstatechange donc jamais de nettoyage. Corrige : iceServers=[]
  (navigateur et serveur sur le meme reseau local, un STUN n'a de toute
  facon rien a apporter) plus deux plafonds durs dans blink_webrtc.py,
  memes principes que LIVE_FIRST_FRAME_SECONDS/LIVE_MAX_SECONDS en MSE -
  NEGOCIATION_MAX_SECONDS (15s, echoue proprement plutot que de bloquer) et
  SESSION_MAX_SECONDS (300s, ferme une session jamais terminee proprement
  cote client). Revalide ensuite par l'utilisateur : fonctionne, "beaucoup
  plus rapide".

  Reste ouvert : pas de reglage dans la page web (variable d'environnement
  seulement) ; pas de reprise automatique si la connexion tombe apres la
  premiere image (contrairement a connecterMse) ; detection du profil
  H.264 verifiee sur deux modeles de camera seulement (Salon, jardin) ;
  CODECS["video"] reste un detail d'implementation non documente d'aiortc
  qu'une mise a jour peut casser sans avertissement (issue aiortc/aiortc#944
  toujours sans solution officielle cote projet). A trancher avec
  l'utilisateur : rendre ca un reglage visible, ou garder en drapeau
  experimental encore un temps.

  PyAV retire (2026-09-03), remplace par un demultiplexeur MPEG-TS/PES/NAL
  maison, blink_ts_demux.py (~200 lignes, assez de ISO/IEC 13818-1 pour
  isoler le flux elementaire H.264 : PAT -> PMT -> PID video -> decoupage en
  NAL units des qu'une fin est vue, sans attendre un paquet PES ou une
  image entiere complets). Motive par le retrait de la dependance PyAV elle
  meme, pas par un bug precis d'origine ; a permis au passage de lire le PTS
  reel encode par la camera plutot que de sonder les flux (extradata
  SPS/PPS incomplet a probesize bas, cf. l'essai analyzeduration/probesize
  abandonne plus haut). Valide hors-ligne contre une capture TS reelle
  (373 NAL units, 0 pts=None, PTS = valeur de reference PyAV a l'identique).

  Saccade signalee par l'utilisateur en usage reel (2026-09-03, "le debit
  est plus saccade") apres activation de WebRTC : deux causes distinctes
  trouvees et corrigees ensemble, aucune des deux liee a la latence de
  demarrage deja mesuree plus haut.
  1. PTS invente a la reception (horloge monotone locale) au lieu du PTS
     reel encode par la camera dans l'entete PES (ITU-T H.222.0, 2.4.3.7) :
     un reseau qui livre par rafales plutot qu'a cadence reguliere faisait
     perdre au recepteur l'information de rythme necessaire pour lisser
     l'affichage. Corrige par l'extraction PTS ci-dessus dans
     blink_ts_demux.py (deja cadencee a 90 kHz, meme horloge que WebRTC
     utilise pour la video RTP - aucune conversion requise).
  2. `<video autoplay>` seul ne demarre pas toujours la lecture d'un
     srcObject WebRTC (constate : video paused, currentTime bloque a 0,
     100% des images decodees comptees perdues par
     getVideoPlaybackQuality() jusqu'a un appel explicite video.play()).
     watchWebRTC() dans serve_app.js n'avait jamais reproduit le geste
     defensif que connecterMse() applique deja pour MSE (meme raison :
     autoplay n'est pas fiable a 100%, seuls les evenements
     loadeddata/playing prouvent qu'une image reelle est affichee).
     Corrige par le meme appel video.play().catch(() => {}) dans
     pc.ontrack.
  Valide : suite complete (407 tests) verte, harnais e2e isole
  (scratchpad, hors-depot) sur une fenetre fraiche de 4s apres le fix ->
  0 image perdue, currentTime avance normalement. Redeploye en production,
  webrtc:true confirme, verifie en reel sur Salon (paused:false,
  currentTime avance au rythme reel). Le compteur d'images perdues n'a pas
  pu etre revalide en conditions de prod via l'automatisation navigateur :
  l'onglet controle passait document.hidden=true (throttling Chrome des
  onglets non visibles), ce qui a lui seul suffit a faire compter comme
  perdue toute image decodee, independamment de la qualite reelle du flux -
  artefact de methode de test, pas un signal sur le code.

  Confirme par l'utilisateur (2026-09-03) : Salon fluide, jardin encore
  saccade mais differemment - longues periodes fluides puis saccade,
  hypothese bande passante avancee par l'utilisateur. Diagnostic instrumente
  a nouveau plutot que suppose : capture TS brute de jardin (45s) avec
  horodatage reseau reel par lecture (scratchpad/capture_ts_timing.py),
  rejouee hors-ligne a travers blink_ts_demux.py. Resultat net : le PTS
  camera reste parfaitement regulier (30 fps sans le moindre saut) sur toute
  la capture - la camera encode sans probleme - mais la livraison reseau
  montre un trou d'environ 1,0s tres regulier (6 occurrences mesurees,
  7,95s/8,95s/9,95s/10,95s/11,94s/12,95s - quasi exactement une par
  seconde), 0,62 a 0,86s sans le moindre octet recu a chaque fois. Confirme
  et precise l'hypothese bande passante de l'utilisateur : tres
  probablement le wifi de la camera a pile qui s'endort par intervalles
  pour economiser la batterie (Blink Outdoor, contrairement au Blink Mini
  cable de Salon), pas un signal faible en continu.

  Cause reelle : _PisteH264.recv() (blink_webrtc.py) renvoyait une image des
  qu'elle etait demultiplexee, zero tampon - le moindre trou reseau se
  voyait donc directement comme un arret de lecture. MSE n'a jamais ce
  probleme par construction (SourceBuffer + <video> du navigateur
  bufferisent deja plusieurs secondes d'avance par defaut), pas verifie en
  reel une deuxieme fois pour ca, pas necessaire. Corrige par un tampon de
  lecture (jitter buffer, meme principe que tout flux temps reel - RTP,
  visio) : TAMPON_LECTURE_SECONDS = 1.2, recv() cadence chaque image sur son
  PTS depuis une ancre posee a la premiere image plutot que de la renvoyer
  des son arrivee. Applique uniformement (pas seulement aux cameras a pile)
  pour rester simple ; Salon a largement la marge pour l'absorber (WebRTC y
  reste tres au-dessus de MSE malgre le tampon).

  Valide avant redeploiement (pas de nouveau reveil camera) : le VRAI
  _PisteH264.recv() rejoue contre l'enregistrement reseau reel de jardin
  (scratchpad/valide_tampon_lecture.py, FauxReader qui respecte les memes
  horodatages d'arrivee que la capture) - les 6 trous de regime etabli
  tombent tous a des ecarts normaux (~0,033s) apres le fix, seul un ecart de
  0,89s subsiste, en tout debut de session (image #2, le temps que le
  tampon se remplisse une premiere fois - inevitable, pas une resurgence du
  probleme). Suite complete (407 tests) verte. Redeploye en production,
  webrtc:true confirme. Confirme par l'utilisateur (2026-09-03) : "ca
  marche" sur jardin.

  Suite donnee par l'utilisateur (2026-09-03), deux demandes separees.

  1. Reglage webrtc/mse dans la page (webrtc par defaut), a la place de la
  variable d'environnement BLINK_DIRECT_WEBRTC. MJPEG (troisieme choix
  propose par l'utilisateur) volontairement pas ajoute : deja compare a
  MSE une fois (audit 28.15, MSE gagnant), code mort retire depuis en
  deux temps (28.53, commit 7339f85, doctrine explicite "ne pas laisser de
  code mort") - le reintroduire comme option sans raison nouvelle
  reviendrait sur cette decision. runtime.py : nouveau champ
  "live_protocol" (webrtc/mse) dans REGLAGES_DEFAUT, valide a la lecture
  (PROTOCOLES_LIVE_VALIDES, retombe sur le defaut sinon, meme pattern que
  timezone/booleens), accepte en parametre de ecrire_reglages(). serve.py :
  WEBRTC_ACTIF lit desormais runtime.lire_reglages() au demarrage au lieu
  de os.environ - meme moment de lecture que les autres reglages (redemarre
  deja au changement, comme port/fuseau/etc.), meme validation cote
  POST /api/reglages. Page web : select id=liveProtocol dans le fieldset
  Video existant, deux options, cle i18n FR/EN. Tests : 7 appels
  ecrire_reglages() dans test_runtime_reglages.py mis a jour (nouveau
  parametre requis, pas de defaut - coherent avec le reste de la
  signature), 2 nouveaux tests (valeur inconnue retombe sur webrtc, "mse"
  respecte). 407 tests -> 409.

  2. Changer de camera en direct sans cliquer Arreter ne marchait pas
  ("il faudrait forcer un arret de la camera active"). Cause confirmee :
  MODULE_SLOT (serve.py) est un Semaphore(1) global, partage par WebRTC et
  MSE, acquis en mode non bloquant - la deuxieme camera tombe donc en 409
  immediat. watchWebRTC() n'a aucune reprise (un seul essai, repli MSE) ;
  watchMse() en a deja une, specifique au 409 (MSE_DELAI_MODULE_OCCUPE_MS
  = 10000, jusqu'a MSE_BUDGET_TOTAL_MS = 10 min) - fonctionnelle mais
  jamais concue pour ce cas : elle n'aboutit que quand la premiere camera
  expire d'elle-meme (LIVE_MAX_SECONDS, jusqu'a 5 min), d'ou le "ca ne
  marche pas" en pratique. Corrige cote page web uniquement (aucun
  changement serveur necessaire) : watchLive() (serve_app.js) arrete
  desormais explicitement toute autre camera active (WEBRTC_PC/MSE_ABORT)
  avant de lancer la nouvelle - stopWatch() existait deja, seulement jamais
  appele automatiquement ici. La boucle de reprise 409 deja presente dans
  watchMse() absorbe le residu de course cote serveur (liberation pas
  encore terminee au moment ou la nouvelle requete part) sans code
  supplementaire.

  Verifie en reel (Salon actif -> bascule vers jardin sans Arreter) :
  Salon correctement arrete (revient a la vignette), jardin recoit son
  tour immediatement (flux Blink ouvert quelques secondes apres, pas
  bloque des minutes). Suite complete verte, redeploye, webrtc:true et
  reglage confirmes en page (select prerempli sur "webrtc").

  Observation separate en verifiant, non confirmee : la lecture MSE de
  jardin, apres la bascule, est repassee plusieurs fois en paused avec
  currentTime bloque a 0 malgre buffered non vide et readyState=4 (play()
  manuel la debloque un instant, puis retombe) - pattern different du bug
  play()/autoplay deja corrige cote WebRTC (connecterMse() appelle deja
  video.play()). Piste non creusee : meme reseau bursty que celui
  diagnostique et corrige cote WebRTC (jitter buffer, plus haut), mais
  MSE n'a recu aucun correctif equivalent - purement hors perimetre de
  cette demande (bascule de camera), pas cause par elle. A surveiller si
  signale a nouveau.

  Affine par l'utilisateur (2026-09-03) : "blink demande un certain temps
  entre le passage d'une camera a une autre... on pourrait utiliser le
  verrou de blink, sur le systeme occupe ?". Verifie dans le code source de
  blinkpy (site-packages, pas suppose) avant de repondre : un vrai
  mecanisme existe (LiveStreamAPI.poll(), livestream.py) - tant que la
  connexion TCP vers le relais Blink n'a pas vu EOF, il continue
  d'interroger api.request_command_status() en boucle ; seulement une fois
  EOF vu, son bloc finally appelle api.request_command_done(). _stop_stream
  (serve.py) attend deja cette tache dans son integralite avant de rendre
  MODULE_SLOT - le serveur n'ecourte donc rien. Le vrai temps d'attente est
  celui, reel, que met le relais Blink a repondre a la fermeture du flux
  cote client (mesure en reel : ~8,8 s sur la bascule Salon -> jardin de
  tout a l'heure) : ni instantane, ni infini, borne par les propres
  timeouts internes de blinkpy (COMMAND_POLL_TIME=1s, MAX_RETRY=120). Ce
  qui manquait n'etait donc pas d'attendre le bon signal (deja fait) mais
  de le voir arriver plus vite cote client : MSE_DELAI_MODULE_OCCUPE_MS
  (10 s) est un choix delibere et documente dans le code pour le cas
  generique "un tiers inconnu tient le module" (patience justifiee, ETA
  inconnue) - pas adapte au cas "je viens de declencher moi-meme cette
  liberation, elle est deja en cours".

  Premier correctif (serve_app.js uniquement, depasse par la suite - voir
  plus bas) : delai court (1000 ms) specifique au 409 rencontre juste
  apres une bascule, au lieu du delai generique de 10 s. Mesure en reel
  sur une bascule Salon -> jardin : 8,76 s -> 1,65 s. Retire ensuite
  entierement (cf. ci-dessous), la nouvelle mesure ayant montre que ce
  1,65 s n'etait qu'un echantillon chanceux, pas une valeur stable.

  Repousse par l'utilisateur (2026-09-03), deux retours factuels
  precedant la conception finale :
  1. "mon temps d'attente n'est pas de 1,65s entre salon et jardin;
     plutot 10s" - le vrai temps de liberation cote Blink est variable
     (deja 8,76 s vs 1,65 s dans les deux mesures precedentes), un delai
     fixe, meme raccourci a 1 s, ne peut que mal deviner selon les jours.
  2. "je trouve que c'est plus propre d'attendre une confirmation du
     serveur, plutot que d'essayer en force" - jugement architecture
     explicite en faveur d'un signal reel plutot qu'un delai calibre.
  Et une contrainte produit supplementaire (meme session) : "on ne doit
  pas basculer automatiquement de webrtc a mse ! il y a un reglage manuel
  pour ca" - watchLive() retombait jusque-la sur watchMse() a la moindre
  erreur WebRTC (comportement herite d'avant le reglage live_protocol,
  jamais retire depuis) ; explique aussi pourquoi une bascule atterrissait
  perceptiblement sur MSE malgre le reglage webrtc ("pourquoi MSE ? on
  n'utilise pas webrtc maintenant ?", meme session) - watchWebRTC() n'a
  qu'un seul essai, sans boucle de reprise, donc le moindre 409 pendant
  une bascule le faisait echouer puis basculer vers MSE.

  Conception finale : nouvelle route GET /api/attente-module (serve.py,
  send_attente_module) qui attend reellement MODULE_SLOT.acquire(blocking=
  True, timeout=ATTENTE_MODULE_MAX_SECONDS=25) puis le relache aussitot
  (ne le retient pas pour elle-meme - seulement une confirmation, la
  vraie tentative suit juste apres). Ne declenche aucun arret : stopWatch()
  cote page l'a deja fait avant cet appel, ce serait redondant de le
  refaire ici. watchLive() (serve_app.js) attend cette confirmation
  (attendreModuleLibre()) avant de tenter quoi que ce soit, seulement
  quand une autre camera etait active. Repli automatique WebRTC -> MSE
  retire de watchLive() : un echec WebRTC affiche desormais une vraie
  erreur (failWatch(), meme traitement que MSE) plutot que de substituer
  silencieusement l'autre protocole - coherent avec le reglage
  live_protocol, qui perdrait son sens si contourne en silence des le
  premier accroc. Supprime au passage MSE_DELAI_MODULE_OCCUPE_APRES_
  BASCULE_MS et le parametre viensDeBasculer (premier correctif,
  desormais inutile - watchMse() retrouve son delai unique d'origine,
  10 s, pour le seul cas qui lui reste : un tiers reellement inconnu).

  Verifie en reel (Salon WebRTC actif -> bascule vers jardin sans
  Arreter) : jardin atterrit desormais sur WebRTC (pas de repli MSE),
  paused:false, currentTime avance normalement des la premiere tentative.
  Delai total mesure entre la fin de session Salon et la premiere image
  video de jardin (direct.log) : 2,73 s - integre cette fois le reveil de
  la camera lui-meme, pas seulement la liberation du module (mesure pas
  directement comparable aux 8,76 s / 1,65 s precedents, qui isolaient la
  seule ouverture du flux Blink). Suite complete (409 tests) verte,
  redeploye.

  Regression signalee par l'utilisateur dans la foulee (2026-09-03) : "a
  un moment, les 2 boutons sont a voir le direct, alors que celui de la
  2eme camera aurait du changer de suite". Introduite par le changement
  ci-dessus : watchWebRTC()/watchMse() ne touchent la case qu'apres
  attendreModuleLibre(), qui peut prendre plusieurs secondes - la case de
  la nouvelle camera restait donc sur "Voir en direct" tout ce temps,
  comme si le clic n'avait rien declenche. Corrige (watchLive(),
  serve_app.js) : la case affiche l'indice d'attente (repos() avec le
  libelle "watch.waking") immediatement, avant meme d'arreter l'autre
  camera ou d'attendre - meme texte que celui affiche juste apres par
  watchWebRTC()/watchMse(), pas de changement visible au moment de la
  relve. Verifie en reel : le bouton passe de "Voir en direct" a "Reveil
  de la camera..." de facon synchrone des le clic (avant tout await),
  pendant que l'ancienne camera revient a l'etat repos au meme instant.
  Suite complete verte, redeploye.

  "ca bloque sur le liveview jardin" (2026-09-03, apres publication de
  v0.11.0). Direct.log : "echec (webrtc), TimeoutError:" suivi 24s plus
  tard de "session rendue de force". Cause reelle, verifiee dans
  blink_webrtc.py (pas supposee) : l'attente du SPS/PPS envoye par la
  camera (track.sps_pps_pret.wait(), le vrai reveil materiel) partageait
  NEGOCIATION_MAX_SECONDS (15s) avec les etapes de negociation SDP/DTLS
  purement locales - alors que MSE accorde deja 40s a cette meme attente
  (LIVE_FIRST_FRAME_SECONDS, serve.py) precisement parce qu'une camera a
  pile doit se reveiller. 15s suffisait le plus souvent (d'ou tous les
  succes vus dans ce meme direct.log), pas toujours. Corrige : nouvelle
  constante PREMIERE_IMAGE_MAX_SECONDS = 40 (meme valeur et raison que
  cote MSE), dediee a cette seule attente ; NEGOCIATION_MAX_SECONDS reste
  a 15s pour la suite (SDP/DTLS, purement locale, aucune raison d'etre
  aussi patiente).

  Message d'erreur corrige au passage : un TimeoutError sans reponse
  serveur avant plusieurs dizaines de secondes (negociation + jusqu'a 45s
  de nettoyage cote send_offer_webrtc) s'affichait ensuite comme
  "TimeoutError: " brut sur le bouton Reessayer, sans explication - meme
  qualite de message que MSE desormais ("La camera n'a envoye aucune
  image. Hors de portee du module, endormie, ou deja occupee par une
  autre session.").

  Confusion separee dans la foulee : "pourquoi mse??? on est sur
  webrtc!" - le prefixe de journal "[direct-mse]" (nom herite de
  l'epoque ou seul MSE existait) apparaissait sur CHAQUE ligne de
  direct.log, y compris les echecs WebRTC ci-dessus, laissant croire a
  une bascule silencieuse. Aucune bascule reelle (deja retiree, plus
  haut) : juste un nom de prefixe reste generique par accident.
  _journal_direct_mse() renommee _journal_direct() (serve.py, 12 sites
  d'appel) ; meme prefixe "[direct]" reproduit dans blink_engine.py (son
  propre point de journalisation, hors de portee de la fonction ci-dessus
  pour eviter un import circulaire). Le protocole reste lisible dans le
  texte de chaque message, jamais dans ce prefixe desormais neutre.

  Suite complete (409 tests) verte, redeploye.

  Toujours la, reconfirme par l'utilisateur (2026-09-03) : "ca bloque
  toujours lors du passage salon a jardin; il faut attendre un moment".
  Direct.log de cette nouvelle occurrence (pas suppose) : Salon arrete a
  14:30:17.825, jardin echoue a 14:30:59.126 (41,3s - pile
  PREMIERE_IMAGE_MAX_SECONDES=40, le nouveau plafond deja atteint), une
  nouvelle tentative reussit a 14:31:41 (42s plus tard) - ~84s ressenties
  au total pour cette seule bascule. Le plafond de 40s (juste corrige,
  voir plus haut) n'etait donc pas mal calibre : jardin peut reellement
  prendre plus de temps que ca a repondre, tout particulierement juste
  apres une bascule depuis une autre camera (probable temps de reattache
  materiel du module de synchronisation a une camera differente - non
  verifiable depuis blink2video, hors de sa portee). Le vrai manque
  restant : un seul essai de negociation WebRTC, sans reprise - MSE a
  deja une boucle pour exactement ce cas depuis le debut
  (MSE_MAX_ECHECS_A_VIDE), WebRTC ne l'avait jamais eue ("pas encore de
  boucle de reprise pour ce chemin tout neuf, un nouveau clic suffit" -
  ecrit a la construction initiale, jamais revisite jusqu'ici).

  Corrige : boucle de reprise pour la negociation initiale
  (watchWebRTC()/tenterWebRTC(), serve_app.js), symetrique a celle de
  MSE - memes valeurs que les constantes MSE_* (WEBRTC_MAX_ECHECS=5,
  WEBRTC_DELAI_RECONNEXION_MS=3000, WEBRTC_BUDGET_TOTAL_MS=10 min),
  meme distinction compteur de secondes uniquement au tout premier essai
  vs texte fixe "Reconnexion..." ensuite. Necessite une annulation
  propre pour un essai en cours ET pour l'attente entre deux essais :
  nouveau WEBRTC_ABORT (parallele a MSE_ABORT), verifie par stopWatch()
  et par watchLive() (une camera "active" au sens de la bascule inclut
  desormais une reprise en cours, meme sans RTCPeerConnection etablie
  pour l'instant). Repli automatique vers MSE toujours absent (inchange,
  demande explicite de l'utilisateur) : ceci reprend seulement au sein
  du protocole choisi, jamais vers l'autre. Porte volontairement limitee
  a la negociation initiale, comme documente des la premiere version :
  une coupure apres la premiere image reste, elle, non reprise
  automatiquement (portee differente).

  Valide en reel avant redeploiement :
  - chemin normal (Salon) inchange, succes du premier coup ;
  - boucle de reprise verifiee sur Portail (camera reellement hors
    ligne, echec rapide et repetable, contrairement a jardin) : indice
    "Reconnexion..." visible entre les essais, WEBRTC_ABORT peuple
    pendant la sequence, message d'erreur clair apres epuisement des 5
    essais, meme qualite que le message KeyError deja existant
    ("Blink n'a fourni aucune adresse de flux...") ;
  - annulation en cours de reprise (bouton Arreter pendant l'attente
    entre deux essais) : case revient proprement au repos, WEBRTC_ABORT/
    WEBRTC_PC/MSE_ABORT tous vides ensuite, aucun etat residuel.
  Suite complete verte, redeploye.

- **"Interrogation du systeme Blink..." (mode Direct) plus lent que

> **Statut : FAIT le 2026-09-03.**

  necessaire (2026-09-03).** Question de l'utilisateur : une seule
  requete, ou plusieurs qu'on pourrait paralleliser/differer ? Verifie
  dans le code source de blinkpy avant de repondre (pas suppose) :
  system_state() (serve.py) appelait _blink.refresh(force=True), qui
  enchaine en serie get_homescreen() + par module get_network_info() +
  update_local_storage_manifest() + check_new_videos() + par camera
  get_camera_info()+update() (sync_module.py) - soit 1+1+1+1+N appels
  reseau successifs pour ce compte (N=4 cameras ici). Or l'affichage de
  cette page ne lit en realite que get_homescreen() (nom/batterie/
  temperature/statut par camera) et network_info par module (armement) :
  le reste (manifeste de stockage, nouveaux clips, detail par camera)
  n'est jamais lu par cette route. sync.cameras lui-meme (identifiants
  device_id/network_id, utilises pour rapprocher chaque camera de son
  entree dans l'ecran d'accueil) est peuple une seule fois a la connexion
  initiale (update_cameras(), appele par start(), jamais par refresh()) -
  deja stable, pas besoin d'un nouvel appel pour ca non plus.

  Pas de rendu "rapide puis enrichi" necessaire au final : juste retirer
  ce qui ne sert a rien pour cette page precise. system_state() appelle
  desormais get_homescreen() + get_network_info() par module seulement,
  au lieu de refresh(force=True) complet. Meme donnees affichees,
  verifie champ par champ en reel (armement, batterie, temperature,
  hors-ligne, y compris Salon en null - Blink Mini, normal, deja le cas
  avant). Mesure reelle avant/apres, meme session, dos a dos, dans un
  script isole (scratchpad/comparer_system_state.py, pas suppose) :
  nouvelle sequence 3,34s contre 7,42s pour l'ancienne (2,2x). Chiffre
  exact variable d'un appel a l'autre (l'API Blink elle-meme varie,
  constate plusieurs fois cette session) mais l'ecart structurel (2
  appels reseau au lieu de 8) est solide. Suite complete verte,
  redeploye.

  Regression introduite par ce meme allegement, trouvee par un audit
  general demande par l'utilisateur juste apres (2026-09-03), pas par
  l'utilisateur lui-meme : describe_camera() (serve.py) lit battery/
  battery_signal/voltage/temperature/wifi/firmware/kind/model depuis
  camera.attributes (objet camera de blinkpy), pas depuis raw/info
  (ecran d'accueil). attributes n'est mis a jour que par camera.update()
  (extract_config_info(), camera.py), appele seulement par
  sync_module.refresh() (une fois par camera, precisement ce que
  l'allegement retire de system_state()) ou par update_cameras() au tout
  premier demarrage. Consequence reelle : ces champs se figeaient
  silencieusement a la valeur du demarrage du serveur, plus jamais
  rafraichis ensuite - invisible sur le moment (le serveur venait de
  redemarrer, tout etait encore frais), ne se serait vu qu'apres des
  heures ou des jours d'activite, sans jamais se corriger seul. armed/
  battery_signal/lfr n'etaient eux pas touches (deja lus depuis raw/
  signals, verifie plus haut).

  Corrige en verifiant d'abord (pas suppose) un ecran d'accueil reel
  (scratchpad/inspecter_homescreen.py) : battery, fw_version, type et
  signals.{wifi,temp,battery,lfr} y sont deja tous presents, per-camera,
  sans aucun appel supplementaire. describe_camera() lit desormais ces
  champs depuis info/signals, comme armed/battery_signal/lfr deja avant.
  temperature recalculee depuis signals.temp (Fahrenheit brut, comme
  Blink le rapporte) avec exactement la meme formule que camera.
  temperature_c (blinkpy, camera.py) : round((f-32)/9*5, 1). Seul voltage
  reste lu depuis attributes (donc perime apres le demarrage) : aucun
  equivalent dans l'ecran d'accueil, et verifie non affiche cote page
  (aucune occurrence dans serve_app.js) - perime sans consequence
  visible, pas la peine d'y consacrer un appel reseau dedie.

  Valide en reel : donnees correctes et fraiches par camera (Terrasse1
  49,4 degC, Portail 28,3 degC - distinctes, pas une valeur figee
  dupliquee -, coherentes avec l'ecran d'accueil brut inspecte juste
  avant). Salon (Blink Mini) toujours a null pour battery/temp/wifi,
  normal, deja le cas avant. Suite complete verte, redeploye.

  Audit general par ailleurs : deux autres pistes remontees, pas encore
  traitees, a trancher avec l'utilisateur.
  - collect_videos() (serve.py) reprobe la duree de chaque video
    assemblee a chaque appel de /api/videos (probe_duration() brut, non
    mis en cache), alors que le meme fichier a deja le mecanisme qu'il
    faudrait juste reutiliser : probe_duration_cached() (empreinte
    taille+mtime), deja utilise pour les clips ecartes juste a cote, et
    merge_daily.py a le meme motif pour les clips source. Gain probable,
    risque faible, motif deja eprouve ailleurs dans ce depot.
  - _telecharger_cloud() (blink_engine.py) telecharge les clips du cloud
    strictement en sequence, un await complet avant le suivant. Gain de
    temps total plausible sur un gros retard a rattraper, mais rien
    n'indique que ce soit reellement ressenti comme lent (contrairement
    a /api/system, jamais signale), et rendre ca concurrent demande de
    revoir la progression SSE et l'ecriture du registre pour rester
    correct a plusieurs telechargements en vol - a decider deliberement,
    pas un gain evident au meme titre que le premier point.

  Premier point traite, sur demande explicite de l'utilisateur ("le
  premier seulement") : collect_videos() (serve.py) probait la duree de
  chaque video journaliere/hebdomadaire/mensuelle via probe_duration()
  brut - celui-ci a bien un cache, mais seulement en memoire (_DURATIONS,
  module-level), qui ne survit pas a un redemarrage du serveur. Chaque
  redemarrage relancait donc ffmpeg -i pour TOUTES les videos existantes
  des la premiere ouverture de l'onglet Clips. Corrige en reutilisant
  telle quelle la mecanique deja en place pour les clips ecartes
  (probe_duration_cached, empreinte taille+mtime) : nouveau fichier
  ASSEMBLED_DURATIONS ("assembled_durations.json", meme dossier "thumbs"
  que EXCLUDED_DURATIONS), nouvelle load_assembled_durations() miroir de
  load_excluded_durations(). Identite de cache = kind/camera/nom (pas
  seulement camera/nom) : daily et weekly peuvent chacun contenir un
  fichier du meme nom pour la meme camera, sans rapport entre eux -
  couvert par un test dedie.

  Trois tests dedies (test_serve_collect_videos_cache.py, nouveau
  fichier - aucun test existant sur collect_videos avant ceci) : duree
  sondee une seule fois a travers deux appels, fichier remplace re-sonde
  (empreinte changee), meme nom dans deux periodes differentes sonde
  separement. Suite complete (412 tests) verte, redeploye. Verifie en
  reel : /api/videos repond correctement (27 quotidiennes, 7
  hebdomadaires, 3 mensuelles chez l'utilisateur), rapide des le premier
  appel post-redemarrage - ffprobe s'est avere deja tres rapide sur ces
  fichiers, le vrai gain se verra surtout sur une bibliotheque nettement
  plus grande ou un disque plus lent, mais le mecanisme est identique a
  celui deja eprouve pour les clips ecartes.

- **"Redemarrer" depuis l'icone de zone de notification arrete tout sans

> **Statut : FAIT le 2026-09-03, confirmé par Nico en cliquant réellement sur « Redémarrer ». Un autre défaut, distinct, du même bouton (exécutable sans console) est corrigé dans watch2notif 0.6.4 et nico579-commons 0.4.8.**

  jamais rien relancer (2026-09-03).** Signale par l'utilisateur en
  usage reel : "j'ai fait redemarrer avec le systray, ca s'est arrete,
  mais pas reparti". Pas une regression de cette session (aucun fichier
  concerne touche avant cet incident) : un incident deja rencontre et
  partiellement investigue, documente dans le code lui-meme (tray.py,
  commentaire de blink_cli.py) - constate a l'epoque sur Windows 7,
  "sans qu'on ait pu etablir pourquoi le second processus detache
  n'aboutissait pas toujours". Cette fois reproduit et sa cause
  exacte identifiee dans le code (pas supposee).

  redemarrer()/arreter() (tray.py) lancent nettoyer() - et pour
  redemarrer, _relancer() ensuite - sur un thread demon separe du
  thread de la pompe de messages de l'icone (necessaire : le geler
  empecherait icon.stop() d'etre traite a temps). Mais icon.run()
  rendait la main des icon.stop(), sans jamais attendre ce thread demon.
  blink_cli.py (l'appelant) fait ensuite son propre nettoyage (deja
  present pour couvrir un crash sans passer par le menu, idempotent -
  "l'appeler deux fois... ne coute qu'un aller-retour inutile si la
  premiere a deja tout nettoye", commentaire deja en place) : rapide,
  puisque les workers sont deja arretes par le premier passage. Course
  reelle entre les deux : si ce second nettoyage (rapide, synchrone,
  sur le thread principal) termine avant le thread demon (nettoyer()
  peut prendre jusqu'a 15+5s), le thread principal termine et le
  processus sort - tuant net le thread demon avant qu'il n'ait atteint
  _relancer() (un thread demon ne survit jamais a la fin du thread
  principal). "Redemarrer" arrete alors tout sans jamais rien relancer.
  Timing-dependant, jamais garanti dans un sens ou l'autre : explique
  le "pas toujours" de l'investigation d'origine.

  Corrige : icon.run() (tray.py) attend desormais le thread lance par
  redemarrer()/arreter() avant de rendre la main a l'appelant (nonlocal
  thread_de_sortie, join(timeout=30) - 30s, au-dela des 15+5s que
  nettoyer() s'accorde deja a elle-meme). Le processus ne peut plus
  sortir avant que _relancer() ait eu sa chance de s'executer.

  Nouveau fichier test_tray_redemarrer_race.py (aucun test existant sur
  tray.py au-dela de disponible()=False avant ceci) : FauxIcon remplace
  pystray.Icon sans aucun vrai backend graphique, run() simule un clic
  en appelant directement l'item de menu vise (item(icon), signature
  confirmee dans pystray/_base.py MenuItem.__call__). Verifie en
  rouge-puis-vert (pas seulement vert) : le test echoue bien avant le
  correctif (ordre == [], la fonction rend la main avant meme que
  nettoyer() n'ait eu le temps de s'executer une seule fois), passe
  apres. Suite complete (414 tests) verte, redeploye localement.

  Confirme par l'utilisateur en cliquant reellement sur Redemarrer
  depuis l'icone (2026-09-03) : "j'ai teste la, ca marche".

- **Release v0.11.5 : "publier la release" echoue systematiquement, 3

> **Statut : CORRIGÉ en v0.11.6 (filtre sur les artefacts). Reste une décision, reprise dans BACKLOG.md : le brouillon de release v0.11.5.**

  tentatives identiques (2026-09-03).** Chaque nouvelle version depuis
  le debut de cette session (v0.11.0 a v0.11.4) avait publie sans
  probleme : premiere fois que ceci se produit. Diagnostic instrumente
  (logs reels de chaque tentative via gh run view --log-failed, pas
  suppose) : les 3 fois, exactement le meme artefact echoue au
  telechargement ("Artifact download failed after 5 retries"), les 4
  autres (blink2video-windows7-x86_64-experimental, -windows-x86_64,
  -linux-x86_64, -macos-arm64) reussissent a chaque fois - pas un alea
  reseau generique, un artefact precis et reproductible.

  Cause reelle : docker/build-push-action@v6 (job "publier l'image
  Docker") depose automatiquement, en plus de l'image poussee sur
  Docker Hub (push: true, deja suffisant en soi), un artefact de
  provenance/attestation nomme "<compte>~<repo>~<id>.dockerbuild" -
  jamais nomme explicitement par ce workflow, comportement par defaut
  de l'action depuis la v5/v6. actions/download-artifact@v4 (job
  "publier la release"), sans filtre, telecharge TOUS les artefacts du
  run, y compris celui-la - qui echoue systematiquement a l'extraction
  (format different, jamais concu pour etre un fichier de release).

  Corrige (.github/workflows/release.yml) : pattern: blink2video-* sur
  le download-artifact de "publier la release" - n'inclut que les 4
  vrais binaires, exclut l'artefact d'attestation Docker sans jamais
  toucher a la publication de l'image elle-meme (deja faite par push:
  true, aucun rapport avec cet artefact). Tag v0.11.5 laisse tel quel,
  orphelin (aucune vraie release jamais publiee pour lui, les 3
  tentatives ont toutes echoue avant l'etape Publier) - pas nettoye
  d'office, decision laissee a l'utilisateur. Nouvelle version (v0.11.6)
  tentee ensuite avec le correctif en place.

## Mutualisation restante entre les quatre applications (2026-10-06)

> **Statut : FAIT le 2026-10-07 (nico579-commons 0.4.9, quatre applications alignées).**


Ce qui est dans nico579-commons et branché dans les quatre : icône et menu,
raccourci de bureau, recherche de version, relance, environnement système,
écritures atomiques, dossiers d'état, serveur web et garde d'hôte, démarrage
automatique (`demarrage`, 0.4.4). Dans gpxsolar, lidar2map et watch2notif :
l'installation automatique (`maj_install`, `Installateur`, bandeau commun).
Dans blink2video et watch2notif : téléchargement et extraction sûrs
(`maj_archive`). Reste, par ordre d'intérêt :

- **blink2video : remplacement de l'installation (fait le 2026-10-07, 0.19.2).**
  `maj_install` a été étendu à ce que `maj.py` savait faire (plusieurs
  processus à relancer, permutation élément par élément avec marqueur de
  reprise, `RestaurationIncomplete`, réservation d'installation, ménage des
  restes), et `maj.py` n'en garde que des enveloppes minces : `_permuter`,
  `_nettoyer`, `_poser`, `_finaliser`. Les anciens tests de restauration, de
  finalisation et d'arrêt (`test_maj_restauration`, `test_maj_finaliser_arret`,
  `test_maj_instances_stockage`) ont rejoué tels quels, sauf six, adaptés parce
  qu'ils visaient l'ancien verrou (un fichier créé en exclusif) : le verrou du
  commun est celui du système, relâché à la mort du processus, et son fichier
  `.blink_maj-installation.lock` reste en place, vide, entre deux mises à jour.
  Reste local, par conception : la phase 1 (`installer`) et le passage de main
  au binaire neuf, qui exécute lui-même `update --finaliser` depuis son dossier
  de préparation (la stratégie « assistant » de `maj_install`, qui confie le
  remplacement à un script, ne convient pas à une application qui arrête
  plusieurs processus et en relance autant), le choix de ce qu'on relance (les
  compositions lues dans le registre des instances) et la sortie du service
  systemd.

- **Amorçage des dépendances en mode sources (fait le 2026-10-07, 0.20.0).**
  Trois versions indépendantes, fondues en une : `nico579_commons.amorcage`
  (0.4.6). Comme il tourne avant l'installation du commun, chaque application en
  garde une copie octet pour octet, `_amorcage.py`, que son test
  `test_amorcage_commun.py` compare au paquet installé : une dérive fait échouer
  la CI. Reste local à chaque application : l'appel (`Amorcage("nom", racine,
  ...)`), la désinstallation et le ménage de l'ancien lanceur (lidar2map et
  gpxsolar), le contexte TLS (lidar2map). Garder la copie à jour : après une
  nouvelle version du commun qui touche `amorcage.py`, recopier le fichier du
  paquet dans les trois dépôts et monter l'épingle.

- **Petits jumeaux (traités le 2026-10-07).** `single_instance` de watch2notif
  est une enveloppe sur `atomique.verrou_inter_processus` (même fichier et même
  octet : une ancienne version encore en cours reste exclue). `send_json` est celui
  du commun (UTF-8 lisible et flush, commun 0.4.7) : l'override de blink2video a
  disparu. Laissés à part, à dessein : les deux TLS (`blink_tls.py` passe un
  contexte strict avec le bundle certifi à blinkpy, `_bootstrap_tls.py` de
  lidar2map remplace la fabrique HTTPS de tout le processus avant l'amorçage : deux
  mécanismes, et `AGENTS.md` demande la prudence sur le TLS de blink2video),
  `end_headers` de blink2video (CSP à nonce, absente des autres) et `web_bridge.js`
  (un helper de 12 lignes et sept méthodes communes ; le reste est l'API de
  chaque application).

## Revue de code du 2026-08-20 (commit 0eab463)

> **Statut : FAIT : les onze bugs de la revue, et les deux points reformulés ci-dessous.**


Les onze bugs numerotes de la revue sont tous traites (28.59 a 28.68) :
Dockerfile CMD start, autostart.py quoi manquant, CSRF/Origin, verrou
disque a double proprietaire, validation MP4/adoption, trois copies
divergentes de safe_name, plafond cloud silencieux, course a la
sauvegarde de session, reglages JSON mal types, surveillance watch.py
(batterie au premier passage + clips ecartes). Restent deux points
reformules ci-dessous, volontairement pas ceux d'origine - chacun touche
une decision de conception plutot qu'un simple oubli, a trancher avec
l'utilisateur - et le lot d'optimisations, non urgentes.

- **Journalieres/hebdo/mensuelles sans distinction si deux cameras se
  nettoient pareil.** FAIT le 2026-10-05 (decision de Nico : oui). Deux noms
  differents qui donnent le meme dossier par safe_name ("Garage" / "Garage!",
  ou "Salon" / "salon" sous Windows et macOS) recoivent maintenant des cles
  distinctes, comme les homonymes de deux reseaux : le nom deja propre garde
  son dossier, les autres prennent " (2)", " (3)"... (merge_daily.
  _cles_camera_par_collision). Registre reel de production : aucune
  collision, rien ne change pour les installations existantes.

- **Silence d'une camera n'ayant jamais enregistre.** FAIT le 2026-10-05
  (decision de Nico : oui). watch.py garde dans WATCH_STATE ("first_seen") la
  date du premier releve de chaque camera qui n'a encore aucun clip, et
  alerte une seule fois quand elle atteint SILENCE_DAYS, en ligne et armee,
  comme le controle de silence existant. Une camera qui obtient un clip, ou
  disparait de l'installation, sort du suivi ; une camera en sourdine ne
  declenche rien. Le premier releve ne declenche jamais d'alerte (camera
  fraichement installee).

- **Idees d'affichage de Markus (issue #40) : trier les cameras, taille des

> **Statut : FAIT en 0.16.0 (2026-10-05) : tri naturel, taille des images, caméras masquées. Reste un point à vérifier, repris dans BACKLOG.md.**

  images reglable, masquer des cameras.**
  Source : MarkusKress, issue #40 "Ideas", 2026-09-29. Reponse publiee le
  meme jour. Classement : tri des cameras et taille des images, faciles (la
  page seule) ; masquer des cameras dans les reglages avec une case sur la
  page pour les reafficher un instant, moyen (liste stockee, les cameras
  masquees doivent continuer d'enregistrer). Priorite a fixer par Nico.
  A verifier en meme temps : le nombre d'images par ligne change d'une vue a
  l'autre a taille de fenetre egale, Markus pense que cela depend de la
  largeur de la ligne d'informations du haut ; s'il se confirme, c'est un
  defaut de mise en page, pas un choix.

- **Webhooks pour scripts (issue #40) : etat des cameras et armement, a la

> **Statut : FAIT en 0.16.0 (2026-10-05) : webhooks d'état et d'armement (commit 847239b).**

  place d'un planning integre.**
  Source : MarkusKress, issue #40, 2026-09-30, demande confirmee ("would
  really be a nice feature", pour les utilisateurs avances qui scriptent leur
  propre planification, chez lui ioBroker + Telegram). Precise l'entree sur
  l'armement par horaire : au lieu d'un planning dans blink2video, exposer
  des URL. (1) Etat de toutes les cameras en JSON (en ligne, arme, batterie
  en statut ok/low et tension si Blink la donne, temperature, wifi,
  firmware) : la page lit deja ces champs (describe_camera, serve.py).
  (2) Armer/desarmer par camera et par Sync Module, avec LE MEME secret que
  le webhook d'image (decision du 2026-09-30 : j'avais propose un secret
  distinct, Nico a suivi Markus, tout tourne sur sa machine et le risque est
  le meme ; un secret separe reste facile a ajouter si ca gene un jour).
  (3) Camera hors ligne : rendre son etat et une erreur nette plutot
  qu'attendre un timeout. JSON : champs tels que Blink les donne, null
  quand une camera ne les rapporte pas, dit dans la doc (Markus : les
  capteurs ne rapportent pas tous les memes informations). Hors de portee : la sonnerie de la
  Doorbell, blinkpy n'a aucun evenement de sonnerie (verifie le 2026-09-30).
  Reponse publiee, "pas cette semaine" ; a grouper avec les autres codages
  de confort (Nico les traitera fin de semaine ou semaine suivante).

- **FFmpeg du paquet Linux : figer la version et verifier son empreinte.**

> **Statut : FAIT (chore/ffmpeg-fige, PR #62 ; constat O04 de l'audit).**

  Source : analyse de l'issue #49 (BjoernD000), 2026-09-30 ; decision de Nico
  le meme jour, a faire avec la 0.15.5 (semaine du 2026-10-05). Sous Windows
  et macOS, ffmpeg vient de imageio-ffmpeg, fige par le verrou
  (requirements-build.txt, 0.6.0 a empreintes). Sous Linux, celui de
  imageio-ffmpeg n'a pas drawtext : build.py (FFMPEG_SECOURS) telecharge alors
  a chaque construction la compilation nocturne de BtbN
  (ffmpeg-master-latest-linux64-gpl), sans version fixee ni SHA-256 verifie,
  et en tire aussi le ffprobe qui valide chaque clip telecharge. La 0.15.4
  embarque ainsi N-126965-gd85cdd2597-20260929. Deux releases Linux
  construites a un jour d'ecart peuvent donc livrer deux FFmpeg differents, et
  le comportement change sans une ligne de code. Precedent : issue #10, 1041
  clips sur 1041 refuses sous Linux seul, contourne en passant la validation a
  ffprobe, cause jamais etablie. C'est aussi un executable telecharge a la
  construction et livre aux utilisateurs sans verification d'empreinte. A
  faire : pointer une archive precise (plutot une branche de release que
  master) et verifier son SHA-256 avant extraction, comme les verrous d'uv le
  font pour les dependances Python. A verifier avant de choisir : combien de
  temps BtbN garde ses archives datees ; si elles disparaissent, garder une
  copie de l'archive retenue dans une release du depot.

- **Validation MP4 : test de non-regression sur un vrai clip Blink USB.**

> **Statut : FAIT (PR #64 : un vrai clip USB de Sync Module dans la suite ; constat O05 de l'audit).**

  Source : issue #49 (BjoernD000), 2026-09-30 ; decision de Nico le meme jour,
  semaine du 2026-10-05, avec la ligne de diagnostic du chemin USB. La CI n'a
  jamais vu ce bug, present depuis la 0.12.29 : le job « chaine video » a un
  vrai ffprobe (FFmpeg d'Ubuntu), mais ses clips sont fabriques par FFmpeg et
  n'ont pas l'unite d'acces H.264 sans image que produit le Sync Module, et
  les tests de valid_mp4_complet simulent la reponse de ffprobe au lieu de
  l'executer. A faire (Nico : « c'est important d'avoir un vrai clip Blink
  de test sur GitHub ») : un VRAI clip USB du Sync Module, enregistre expres
  sur une scene neutre (mur, feuille de papier : rien de prive, le depot
  est public), court (quelques secondes), verifie avant commit : ffprobe y
  ecrit bien « missing picture in access unit ». Un clip cloud neutre en
  plus pour comparer. Test en CI Linux : le vrai valid_mp4_complet sur ces
  clips (acceptes) et sur des copies tronquees (refusees). Un clip
  synthetique seulement si le vrai clip ne peut pas etre publie. Les 104 clips reels de Nico restent dans la VM de test pour
  comparer, jamais dans le depot (images privees, depot public).
  FAIT le 2026-10-05 : un vrai clip USB neutre de Nico (bureau, 21,5 s, 1,6 Mo,
  audio = bruit de micro seulement) est dans fixtures/ avec son empreinte, et
  test_validation_clip_usb_reel.py le passe a la vraie validation. Verifie dans
  une VM avec le vrai ffprobe : 5 tests verts, et 2 echouent des qu'on retire le
  filtre du bruit (l'ancien comportement). Le job Linux de la CI le joue avec
  l'ffprobe d'apt.

- **Icone de zone de notification sous GNOME Wayland : menu absent (voie

> **Statut : FAIT en 0.18.0 (2026-10-05) : icône StatusNotifierItem en Python pur (nico579_commons.tray_sni). Reste à la voir dans une vraie session GNOME, repris dans BACKLOG.md.**

  StatusNotifierItem).**
  Source : essai de Nico dans sa VM Ubuntu 26.04 (GNOME, Wayland), 2026-10-01.
  Le paquet Linux affiche l'icone, le clic gauche ouvre la page, le clic
  droit n'ouvre aucun menu. Diagnostic : la session a l'extension
  ubuntu-appindicators et un hote StatusNotifierWatcher actifs, mais
  blink2video n'y est pas enregistre (trois icones seulement, toutes
  d'Ubuntu). Le paquet n'a pas gi (PyGObject) et le processus n'a charge
  aucune bibliotheque d'icone : pystray retombe tres probablement sur son
  mode X11 (XEmbed, python-xlib, Python pur), dont le menu est une fenetre X11
  que Wayland n'ouvre pas. Meme avec le Python du systeme, le mode
  AppIndicator echouerait sur une Ubuntu standard : le typelib
  gir1.2-ayatanaappindicator3-0.1 n'est pas installe (« Namespace
  AyatanaAppIndicator3 not available »), donc embarquer gi seul ne suffit pas.
  Touche aussi lidar2map, gpxsolar et watch2notif (menu commun, nico579_commons.tray).
  Voies : (1) documenter, l'icone a menu marche sous KDE, XFCE, X11 ; page et
  raccourci du Bureau suffisent ailleurs ; (2) embarquer GTK et AppIndicator
  (+40 a 60 Mo, CI plus lourde, fragile selon les distributions) ; (3) CHOIX DE
  NICO : parler directement le protocole StatusNotifierItem (standard
  freedesktop de GNOME, KDE...) avec une petite bibliotheque D-Bus en Python
  pur, dans nico579_commons.tray, sans rien exiger d'installe sur la machine,
  une centaine a deux cents lignes dont un menu dbusmenu, testable dans la VM
  (verification par gdbus : enregistrement aupres du watcher, GetLayout, Event).
  A faire dans la vague de mutualisation sur la zone de notification, apres
  les codages de confort ; prototype dans la VM refuse pour l'instant (quota).

- **Vignettes : les fabriquer au telechargement du clip, et ne pas travailler

> **Statut : FAIT en 0.18.0 : vignette fabriquée au téléchargement, plus de ffmpeg pour une requête abandonnée. Reste une borne possible sur la file d'attente, reprise dans BACKLOG.md.**

  pour une requete abandonnee.**
  Source : test a froid de Joel sur la PR #59, 2026-10-02 (2 672 clips reels,
  cache de vignettes vide). Avant le chargement paresseux : 2 672 requetes de
  vignettes, 2 673 lancements de ffmpeg, 47 s de CPU, 83 s avant la derniere
  reponse. La PR ramene cela a 12 requetes et 12 ffmpeg pour une ouverture, ce
  qui regle le cas courant. Reste cote serveur : send_thumb() (serve.py)
  fabrique chaque vignette a la demande par ffmpeg, au plus min(8, coeurs) a la
  fois (THUMB_SLOTS), et chaque requete en attente occupe un fil du serveur et
  une des six connexions HTTP/1.1 du navigateur. Il ne s'apercoit pas qu'un
  client est parti : dans le defilement complet a froid de Joel, 359 requetes
  de vignettes sans statut (annulees quand les lecteurs sont liberes) ont
  garde leur place dans la file et fait travailler ffmpeg pour rien, et une
  requete /api/status a echoue, absente a chaud (indice d'une famine, pas une
  preuve). A faire : fabriquer la vignette au moment ou le clip est telecharge
  (le cache est alors toujours chaud, la fabrication a la demande ne sert plus
  qu'aux anciens clips), et ne pas lancer ffmpeg pour une requete dont le
  client est parti ; a evaluer : une borne sur la file d'attente. Hors de la
  PR #59, a faire apres les issues en cours.
  FAIT le 2026-10-05 : plus de ffmpeg pour une requete dont le navigateur est
  parti (serve._client_parti, controle apres l'attente du creneau).
  FAIT le 2026-10-05 (demande de Nico) : le telechargeur fabrique la vignette de
  chaque clip qui arrive (blink_engine._preparer_vignettes, avant la
  notification, 40 par passage au plus), avec la meme extraction que serve.py
  (merge_daily.extraire_vignette) et au meme chemin de cache : la page trouve
  la vignette prete. Mesure sur le vrai clip USB : 0,14 s. Limites connues :
  avec --thumbs personnalise la vignette faite au telechargement est
  inutilisee ; une vignette est refaite par serve.py quand la version
  normalisee du clip (avec l'horodatage incruste) est plus recente. RESTE : une
  borne sur la file d'attente de serve ; le test a froid de Joel sur les vrais
  clips dira si elle est encore utile.

- **Sourdine d'un Sync Module dans watch.** FAIT le 2026-10-05 (demande de

> **Statut : FAIT le 2026-10-05.**

  Nico). `watch --ignore-module REF...` et `--unignore-module REF...` ; REF =
  identifiant Blink du module, identifiant de reseau, nom, ou libelle
  « nom (reseau) ». Un nom partage par deux modules (le nom par defaut est
  identique) est refuse comme ambigu, avec la liste des identifiants, et rien
  n'est ecrit. Comme une camera en sourdine : ni alerte ni retour a la
  normale. La fenetre « Module hors ligne » propose maintenant la commande
  --ignore-module avec l'identifiant du module concerne. Etat :
  WATCH_STATE["ignored_modules"]. La boucle d'alerte elle-meme (deux modules
  homonymes) etait corrigee en 0.16.3.

- **Étude Electron pour les quatre applications.** Demande de Nico du 2026-10-02 :

> **Statut : ÉCARTÉ le 2026-10-07, décision de Nico : non.** Pas d'étude complète : l'avis
> préliminaire suffit (coût en taille et en signature, moteur Python conservé, Windows 7 exclu).
> La décision est aussi dans nico579-commons, ANALYSE-MUTUALISATION-2026-09-29.md, §11.

  Electron (https://www.electronjs.org/fr/) peut-il simplifier les quatre projets ? Aucune étude
  n'est faite, rien n'est décidé. Cadre dans nico579-commons, ANALYSE-MUTUALISATION-2026-09-29.md,
  §11 : ce qu'il remplacerait (serveur local et navigateur, icône et menu, raccourci et démarrage
  automatique, installateur et mise à jour) et ce qu'il ne remplace pas (le moteur Python :
  blinkpy, ffmpeg, aiortc, GPX, LiDAR). Avis préliminaire du 2026-10-07, à confirmer par une étude
  écrite : non. Chaque application embarquerait son Chromium et son Node (de l'ordre de 150 à 200 Mo
  de plus, quatre fois) sans rien retirer ; l'icône sous GNOME Wayland et la mise à jour automatique
  sont déjà résolues par le commun ; la mise à jour d'Electron suppose des programmes signés (la
  candidature SignPath est en attente) ; Electron ne tourne plus sous Windows 7, que l'édition
  « legacy » vise. Seul vrai gain : une fenêtre à soi plutôt qu'un onglet, donc plus de navigateur
  récent exigé pour le direct WebRTC. Alternative à comparer : un raccourci en mode application
  du navigateur. À faire maintenant que la mutualisation est terminée.

- **Brouillon de release v0.11.5, orphelin.** Les trois tentatives de publication de septembre ont

> **Statut : SUPPRIMÉ le 2026-10-07** (sur décision de Nico). Il n'y a plus aucun brouillon de release
> dans le dépôt.

  échoué (corrigé en v0.11.6, voir BACKLOG-FAIT.md) ; il reste un brouillon `v0.11.5` avec 6 fichiers
  sur GitHub, sans tag. À supprimer si Nico est d'accord (décision laissée à lui, rien n'a été effacé).

- **Issue #40 (MCas86) : trois réglages promis le 2026-10-19.** Le titre de l'en-tête, et pourquoi

> **Statut : FAIT le 2026-11-09 : titre et icône de l'en-tête dans Réglages, Général (réglage de l'installation, valable pour tous les navigateurs) ; dernière vue retenue par navigateur, et ronde qui reprend à la réouverture si seule la fermeture de la page l'avait coupée ; caméras hors ligne en fin de liste dans chaque système.**

  pas l'icône, réglables : il les avait changés dans le code pour distinguer son instance, et une mise
  à jour écraserait ces changements. La dernière vue ouverte retenue par navigateur, plutôt qu'une vue
  par défaut unique : l'écran mural rouvre la ronde, le navigateur du bureau la vue Direct. Les
  caméras hors ligne en fin de liste. Une page ne peut pas passer seule en plein écran à son
  chargement, il faut un clic : pour un écran mural, le mode kiosque de Chromium ou d'Edge
  (`--kiosk`), conseillé dans la même réponse.