Skip to content

v3.5.0 — audit des zones jamais couvertes : ce que Lanterne dit vs ce qu'il fait - #1

Draft
pymenvert wants to merge 24 commits into
mainfrom
claude/vas-y-avance-8k8dgo
Draft

v3.5.0 — audit des zones jamais couvertes : ce que Lanterne dit vs ce qu'il fait#1
pymenvert wants to merge 24 commits into
mainfrom
claude/vas-y-avance-8k8dgo

Conversation

@pymenvert

@pymenvert pymenvert commented Aug 4, 2026

Copy link
Copy Markdown
Owner

La feuille de route restante étant bloquée sur le matériel (Pi, capture,
multi-machine) et le code ne contenant plus aucun TODO, cette passe porte sur
les zones qu'aucun des audits précédents n'avait couvertes : la cohérence entre
ce que Lanterne dit et ce qu'il fait.

Méthode : audit multi-agents sur 6 dimensions jamais explorées, chaque lot
repassé par une relecture adversariale — puis une seconde passe adversariale
sur mes propres correctifs
, la démarche qui avait justifié la v3.4.1. Plus des
vérifications empiriques sur un vrai node à chaque fois que c'était possible.

Base de départ : 251 tests. Aujourd'hui : 269 tests, clippy -D warnings,
fmt, shellcheck, smoke — CI verte sur les dix jobs. La version passe en
3.5.0 (le manuel était resté en v3.3, le README en v3.0.0).

Les défauts les plus graves

L'archive Raspberry Pi officielle ne projette rien. Compilée
--no-default-features : ni fenêtre de sortie, ni MIDI, ni GStreamer. Le manuel
invitait pourtant, pour le Pi comme pour Ubuntu, à installer des paquets
gstreamer1.0-* et à brancher le vidéoprojecteur. Sur la cible principale du
produit, quelqu'un aurait suivi la procédure et vu du noir.

Aucune archive publique ne portait les mentions légales. docs/TIERS.md
affirme deux fois être « inclus dans chaque archive de release » ; le correctif
de la v3.4.1 n'avait été posé que sur ci.yml, jamais sur release.yml
partout sauf sur les fichiers que les gens téléchargent.

Mettre à jour retirait des fonctions. L'OTA choisissait l'archive sur la
seule plateforme : une machine Windows installée avec le pack vidéo se voyait
proposer le binaire léger. Le node déclare désormais ses capacités et refuse
la mise à jour quand aucune archive ne fait autant que lui.

Un Pi qui projette parfaitement était déclaré « sortie morte » (fps restait
un zéro dur là où rendu devenait null). La conduite pouvait enregistrer
load "undefined"
, découvert le soir à l'heure de la cue. F11 puis un
réglage depuis la tablette
faisait perdre le plein écran. L'export
diagnostic ne sauvegardait rien
: la liste des noms de presets, pas leur
contenu, et aucun fichier d'état.

Des fonctions qui existaient sans être joignables

  • Chataigne ne voyait ni la vitesse, ni la régie : /rate, /blending,
    /cue/go, /dmx/scene, /dmx/chaser marchaient en OSC sans être publiées
    dans OSCQuery. L'arbre publie maintenant 55 adresses contre 45.
  • Les faders MIDI ne pouvaient piloter ni un effet ni la vitesse, alors que
    le manuel le promet depuis la v1 — et scale = "pixelate" empêchait le node
    de démarrer.
  • Le master lumières n'était joignable que depuis la web UI : chaîne
    complète ajoutée (dmx_master/dmx_fader, OSC, OSCQuery, cible MIDI).

La relecture de mes propres correctifs

Elle a trouvé quatorze défauts dans les corrections ci-dessus, dont
plusieurs régressions que j'avais introduites :

  • Un fader OSC poussé à fond rendait la sortie quasi noire (0.999 → 255
    mais 1.0 → 1). Corrigé, puis re-corrigé : la deuxième version refusait
    90.0, que Chataigne envoie pour 90 — ce que ce fichier documente lui-même
    dix lignes plus haut.
  • Le master posé par OSC/MIDI/cue n'était jamais persisté : la chaîne
    s'arrêtait une ligne trop tôt, et le même geste depuis la tablette survivait.
  • Le sélecteur de média des cues restait vide — correctif juste et
    inopérant. Puis mon correctif du correctif aurait effacé la sélection de
    l'opérateur toutes les 5 s.
  • L'OTA annonçait « À jour » alors qu'une version plus récente existait.
  • L'export diagnostic sautait le preset qu'il fallait sauver : un preset
    abîmé — celui qu'on exporte pour le récupérer — était le seul absent.
  • Le fader MIDI « rate » ne pouvait pas rendre la vitesse normale (1×
    tombait entre deux crans). Échelle géométrique désormais.
  • Une cue incomprise disparaissait définitivement : en rendant la lecture
    tolérante j'avais supprimé le filet .corrompu sans le voir.
  • Et la doc redisait ce qu'on venait de corriger — le pire résultat possible
    pour une branche dont le sujet est la dérive doc/réalité.

Deux tests instables sur Windows aussi : ceux du séquenceur pariaient sur le
temps au lieu de l'attendre. Vérifié par 25 exécutions consécutives puis
4 suites en parallèle.

Ce qui demande une décision

deny.toml justifie sa politique par « Lanterne est destiné à être VENDU »
et parle de « distribution propriétaire », alors que LICENSE et Cargo.toml
sont MIT. La politique (refuser le GPL dans le binaire) reste juste ; la
formulation touche à la décision sur gst-plugins-ugly que CLAUDE.md demande
de ne pas rouvrir — non touchée. docs/TIERS.md, lui, disait « logiciel
propriétaire » en citant le LICENSE qui dit MIT : corrigé.

(Le pack vidéo Windows a cessé d'être constructible en cours de route —
pkgconfiglite disparu de Chocolatey, qui annulait toute la transaction. Réglé
en séparant les installations : le pack se reconstruit, voir
la correction.)

Limites assumées (écrites dans le CHANGELOG)

  • L'unité systemd ne projette toujours pas en mode fenêtre. Le fait est écrit
    dans l'unité, avec le bloc à décommenter — l'activer par défaut casserait les
    installations sans bureau, et cela demande un Pi réel.
  • Le réglage de résolution ne s'applique qu'à la sortie KMS ; plutôt que de
    faire semblant, le journal et l'UI le disent.
  • Deux points ajoutés aux « prochaines étapes » : valider le bloc
    DISPLAY/graphical.target sur Pi OS Desktop, et décider si l'on publie une
    archive Linux/Pi avec GStreamer.

claude added 13 commits August 4, 2026 21:40
docs/TIERS.md affirme deux fois etre « inclus dans chaque archive de
release » et « livre avec chaque archive ». Aucune des quatre archives
de release.yml ne le contenait, ni LICENSE.

Le correctif de la v3.4.1 avait ete pose sur ci.yml (artefact de
l'onglet Actions, avec le commentaire « obligation LGPL (GStreamer) et
point de decision GPL ») et jamais sur release.yml -- c'est-a-dire
partout SAUF sur les fichiers que les gens telechargent reellement,
puisque release.yml dit lui-meme servir la page « publique, sans compte
GitHub, contrairement aux artefacts de l'onglet Actions ».

Consequence concrete : la mention IJG, que la licence de jpeg-encoder
EXIGE de faire accompagner le logiciel, ne partait avec aucun binaire
publie. Et le pack le plus expose -- windows-x64-gstreamer, qui embarque
les DLL LGPL et les plugins GPL de gst-plugins-ugly -- partait lui aussi
sans rien.

Les quatre archives embarquent desormais docs/TIERS.md et LICENSE. Les
deux archives Windows recuperent au passage deploy/installer-windows.ps1,
que le README presente comme l'installateur a profils alors qu'il
n'etait livre nulle part.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
Premier lot de l'audit des zones jamais couvertes. Toutes ces corrections
repondent a la meme question : ce que Lanterne DIT correspond-il a ce qu'il
FAIT ?

1. LES BINAIRES LINUX ET PI NE LISENT AUCUNE VIDEO. `gstreamer` n'est pas
   une feature par defaut : linux-x64 et windows-x64 sont compiles sans, et
   raspberrypi-arm64 part en --no-default-features -- donc sans fenetre de
   sortie, sans MIDI et sans GStreamer. Cette archive ne peut RIEN projeter.
   Le manuel disait pourtant, pour Ubuntu comme pour le Pi, qu'il suffisait
   d'installer les paquets gstreamer1.0-* : Pym aurait suivi la procedure,
   branche le videoprojecteur et vu du noir. La carte Windows legere, elle,
   annoncait honnetement « sans lecture video » depuis toujours.
   Les cartes du manuel, la ligne de depannage et les notes de release
   disent maintenant ce que chaque archive sait faire, et comment obtenir
   la video (compiler avec --features gstreamer, mode KMS sur Pi OS Lite).

2. UN REGLAGE MORT DEPUIS TOUJOURS. `[paths] shaders` etait documente comme
   reglable, cree par les trois installeurs... et lu par personne : les
   shaders sont embarques par include_str!. Champ retire ; les installeurs
   creent `luts/` a la place, qui lui est bien reel. Un `shaders = …` reste
   dans un ancien node.toml est ignore sans bruit (pas de
   deny_unknown_fields), et node.toml.example precise que `luts/` n'est PAS
   deplacable.

3. REINSTALLER SOUS WINDOWS CASSAIT LE PACK VIDEO. `Copy-Item lib cible`
   cree cible\lib quand cible existe deja : des la deuxieme installation,
   les plugins GStreamer atterrissaient dans lib\lib\gstreamer-1.0, la ou
   le binaire ne regarde jamais, pendant que les DLL de la racine etaient
   bien mises a jour. Version melangee, panne difficile a comprendre. On
   copie desormais le CONTENU, apres avoir vide lib\ (sinon les plugins
   fantomes de l'ancienne version restaient).

4. UN PREFIXE RELATIF PRODUISAIT UNE UNITE SYSTEMD INVALIDE. systemd exige
   un chemin absolu ; mkdir, install et chown s'accommodent tres bien d'un
   chemin relatif. `./install.sh --prefix lanterne` se deroulait donc de
   bout en bout sans un mot, et seul `systemctl start` echouait, plus tard.
   Le prefixe est rendu absolu des l'analyse des options. Verifie en reel :
   « Prefixe relatif converti en chemin absolu : /…/lanterne ».

5. METTRE A JOUR LAISSAIT TOURNER L'ANCIEN BINAIRE. install.sh remplacait
   l'executable sans jamais toucher au service : le processus en cours
   gardait l'ancien inode, et le conseil final `systemctl start` ne faisait
   rien puisque le service tournait deja. La mise a jour semblait faite et
   ne l'etait pas. Le service actif est desormais redemarre explicitement.

Le README decrivait par ailleurs un depot qui n'existe plus : dossier
webui/ fantome, « GStreamer a venir », et quatre crates bien reelles
absentes (render, gst, artnet, ndi).

Verifie : 254 tests, clippy -D warnings, fmt, shellcheck. Installation a
froid rejouee en reel (prefixe relatif, luts/ cree, node.toml genere).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
Deuxieme lot. Trois defauts qui se rejoignent : une fonction existe, elle
repond, mais l'operateur n'a aucun moyen de la trouver.

1. DES ADRESSES OSC QUI MARCHENT SANS ETRE PUBLIEES. Le namespace OSCQuery
   est la SEULE source que Chataigne lit pour construire son interface. Il
   annoncait 45 adresses et taisait /rate, /blending, /cue/go, /dmx/scene
   et /dmx/chaser -- soit la vitesse de lecture, le fondu de bords, et la
   TOTALITE du declenchement de spectacle (cues du sequenceur et scenes
   lumieres). Toutes acceptees en entree OSC, aucune visible.
   Verifie en reel sur un node : `/rate 0.5` en UDP fait bien passer la
   lecture a 0,5x, alors que le module OSCQuery n'offre aucun curseur de
   vitesse. Les cinq sont declarees.

2. UN RETOUR D'ETAT SANS DESTINATAIRE. event_to_osc emet /transport et
   /media -- les deux voyants les plus elementaires d'un lecteur : ce qu'il
   fait, ce qu'il a charge. Aucun des deux n'existait dans l'arbre :
   Chataigne recevait les messages sans avoir de parametre ou les ranger.
   Ajoutes en LECTURE SEULE (ACCESS 1), avec /playlist/position, via une
   nouvelle feuille_lecture() -- leaf() ne savait produire que de
   l'ecriture. /preset/loaded, /mapping/loaded et /sync/scheduled restent
   volontairement absents : ce sont des notifications sans etat
   correspondant dans NodeState, les publier imposerait une VALUE
   perpetuellement vide qui se lirait « aucun preset ».

3. LE SELECTEUR DE MEDIAS LISAIT UN CHAMP QUI N'EXISTE PAS. /api/media
   renvoie des MediaInfo { path, bytes } ; l'UI lisait `m.name` a trois
   endroits, donc la chaine « undefined ». Le plus grave n'est pas la liste
   de « undefined » affichee dans « Fichiers du parc » : c'est l'onglet
   Sequences, ou l'action « Charger un media » enregistrait
   {cmd:"load", path:"undefined"} dans la conduite. Rien ne le refusait --
   « undefined » est un chemin relatif parfaitement licite -- et la panne
   n'apparaissait que le soir, a l'heure de la cue. Le selecteur du parc
   filtre en plus les sous-dossiers, que validate_upload_name refuse.

4. LES LUT ETAIENT PLAFONNEES A 2 Mo AU LIEU DE 64. lut_upload extrait le
   corps avec `Bytes` : axum applique alors DefaultBodyLimit (2 Mio) tant
   qu'on ne le releve pas, et aucun DefaultBodyLimit n'existait dans le
   routeur. Le garde maison « 64 Mo max » etait donc du code mort, et une
   LUT 64 points (~7 Mo, taille courante d'un pack d'etalonnage) repartait
   en 413 sans corps JSON -- l'UI n'affichait qu'une erreur generique.
   Contre-epreuve : le test echoue bien (413) quand on retire la couche.

Trois tests ajoutes (adresses pilotables, etat en lecture seule, plafond
de corps). 254 tests, clippy -D warnings, fmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
`apt-get update` sortait en 100 sur les runners :

  E: The repository 'https://packages.microsoft.com/ubuntu/24.04/prod
     noble InRelease' is no longer signed.

Sous `set -o pipefail`, le step echouait et emportait le job « check »
entier -- fmt, clippy, tests, smoke -- alors qu'aucun de ces jobs n'a
besoin du depot Microsoft : ils veulent libasound2-dev, GStreamer,
shellcheck ou le linker ARM64, tous dans le depot Ubuntu.

La panne ne vient pas du code et se reproduit sur main : c'est la
signature d'un depot preinstalle sur l'image GitHub qui a expire de son
cote. Les huit steps apt du depot retirent desormais les listes
microsoft* avant `apt-get update`. On garde la strictitude : un vrai
echec d'installation fait toujours tomber le job.

Le job shellcheck gagne au passage le `apt-get update` qui lui manquait
(il installait shellcheck sur les listes preinstallees).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
L'unite est ordonnee sur multi-user.target et ne recoit aucune variable
d'affichage (DISPLAY, WAYLAND_DISPLAY, XDG_RUNTIME_DIR). Or le mode de
sortie par DEFAUT est `fenetre`, et install.sh ne propose jamais le mode
KMS. Un processus lance dans ce contexte n'a aucun serveur d'affichage a
contacter.

Le node ne plante pas pour autant -- il journalise « fenetre de sortie
indisponible » et continue sans projeter (comportement verifie en reel
sur une machine sans affichage). C'est justement ce qui rend la panne
sournoise : le service est actif, l'UI repond, tout parait normal, et le
videoprojecteur reste noir. L'en-tete promettait pourtant « un Pi branche
a un videoprojecteur reprend son show sans intervention ».

L'unite dit maintenant a quoi elle sert reellement -- pilotage seul, ou
sortie DRM/KMS -- et porte le bloc a decommenter pour un kiosque en mode
fenetre sur Pi OS Desktop, avec la marche a suivre pour trouver les
bonnes valeurs.

PAS active par defaut, et volontairement : basculer sur graphical.target
empecherait le service de demarrer sur une machine sans bureau (Pi OS
Lite), et l'UID comme le type de session dependent de l'installation.
Cela demande un Pi reel -- ce que CLAUDE.md reserve deja au retour de Pym.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
Troisieme lot. La mesure de performance annoncait « une absence, pas un
faux zero rassurant » -- elle ne tenait cette promesse que sur la moitie
des chiffres.

1. `fps` RESTAIT UN ZERO DUR LA OU `rendu` DEVIENT null. Le drapeau
   mesure_disponible (= feature `render` compilee ET mode != KMS) ne
   protegeait QUE la cle `rendu`. La ligne juste en dessous publiait `fps`
   sans condition -- immediatement apres le commentaire qui explique
   pourtant que « des zeros se lisent comme tout va bien, une absence se
   lit comme une absence ».
   Or dans les deux cas ou le drapeau est faux -- mode KMS, et binaire
   sans fenetre, c'est-a-dire l'artefact officiel raspberrypi-arm64 --
   personne ne peuple le compteur. Un run d'endurance de 4 h sur un Pi qui
   projette sans le moindre defaut ressortait donc « Sortie sans aucune
   image : 100 % du run ». Exactement le defaut corrige en sens inverse
   par 2cee3ea, et reintroduit par l'autre bout.
   `fps` suit desormais la meme regle que `rendu`, aux QUATRE endroits qui
   le publient (/api/system, ZIP de diagnostic, /api/outputs, evenement
   position du WebSocket). Le depouillement ignore alors ces points au
   lieu de les compter comme morts, et le badge img/s se masque au lieu
   d'afficher « 0 img/s ».

2. Ctrl+C N'ARRETAIT PAS LE HARNAIS D'ENDURANCE. `trap nettoyer EXIT INT
   TERM HUP` : un gestionnaire de signal qui ne se termine pas par `exit`
   rend la main au script, qui REPREND ou il en etait. Ctrl+C eteignait
   donc la charge et restaurait le mapping, puis la boucle continuait a
   interroger le node pendant les heures restantes en empilant des lignes
   dans le meme CSV -- et `nettoyer` repassait une seconde fois a la
   sortie. Le meme SIGHUP d'une session SSH coupee produisait cela sur un
   Pi distant.
   Reproduit sur un cas minimal (le nettoyage s'affiche, la boucle va
   jusqu'au bout, le nettoyage s'affiche encore), puis verifie corrige :
   sortie immediate, nettoyage une seule fois, code 130.

3. LE README DU HARNAIS DISAIT ENCORE QUE LE p95 ATTRAPE L'A-COUP. Le
   commit ba41112 avait corrigé cette idee partout (mesure.rs, UI, manuel,
   CHANGELOG) SAUF dans le seul document que l'operateur lit pour
   interpreter son CSV. Un figement de 200 ms par minute ne fait pas
   bouger le p95 d'une microseconde -- c'est max_us qui le voit. Les deux
   se lisent ensemble, et la colonne fps documente sa cellule vide.

Corriges au passage, meme famille : la table JSON/OSC de command.rs se
donne pour la reference du vocabulaire et ignorait 8 des 40 commandes
(dont blackout_set, cue_go et dmx_scene, qui ONT une adresse OSC) --
completee et desormais tenue par un test qui compare la table a l'enum ;
l'evenement DmxChaserDemande n'etait jamais miroite en retour OSC, si
bien qu'une scene rappelee allumait le voyant et un chaser lance
n'allumait rien ; trois cles [modules] (sequencer, sync, ndi) que rien ne
lisait, commentees « phase 2 » et « phase 3 » pour des fonctions livrees
en v2.0.0 ; la liste des ecrans, lue une seule fois au chargement de la
page, qui laissait une tablette de regie afficher « aucun ecran detecte »
apres le branchement du videoprojecteur ; docs/TIERS.md qui ouvrait sur
« logiciel proprietaire » alors que LICENSE et Cargo.toml sont MIT ; et
deploy/README qui annoncait en « phase 4 » l'OTA et le mot de passe UI,
livres depuis deux versions.

256 tests, clippy -D warnings, fmt, shellcheck.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
Quatrieme lot : ce qui se passe quand une installation EXISTANTE change de
version. Le scenario n'avait jamais ete audite.

1. L'OTA REMPLACAIT LE BINAIRE PAR UNE VARIANTE PLUS PAUVRE. Le choix de
   l'archive se faisait sur la seule plateforme, jamais sur la variante
   reellement installee. Sous Windows il visait toujours
   `toolbox-node-windows-x64.zip`, le binaire LEGER compile sans
   `gstreamer` -- alors que l'installation recommandee, celle que
   l'installateur pose et que la page Releases met en avant, est le pack
   `…-gstreamer.zip`. Un clic sur « Mettre a jour » depuis l'onglet
   Systeme retirait donc la lecture video a une machine de spectacle : les
   DLL GStreamer toujours la, plus aucun code pour s'en servir.
   Sur un Pi, pire : la seule facon de projeter est de compiler sur place
   (l'archive officielle est en --no-default-features), et l'OTA proposait
   precisement cette archive -- ni video, ni fenetre de sortie.

   Le node declare desormais ses capacites reelles au demarrage (lui seul
   connait ses features), et l'archive visee en decoule. Quand aucune
   archive publiee ne fait autant que le binaire en place -- un Linux ou
   un Pi compile sur place -- la mise a jour est REFUSEE avec la raison,
   au lieu de degrader l'installation. Sans declaration, on suppose le
   binaire le plus pauvre : jamais l'inverse.

2. « REVENIR A LA VERSION PRECEDENTE » ARRETAIT LE NODE SANS LE RELANCER,
   sous Windows. `appliquer()` pose un .bat qui attend, remplace et
   relance ; `revenir_en_arriere()` renommait les fichiers et rendait la
   main, puis le handler tuait le process. Or sous Windows il n'y a ni
   service ni Restart=always : le demarrage automatique n'est qu'un .bat
   du dossier Demarrage, execute a l'ouverture de session. Le node ne
   revenait donc pas, alors que le message promet « le node redemarre,
   reconnexion automatique » -- et c'est le bouton qu'on utilise
   justement quand une mise a jour se comporte mal EN PLEIN SPECTACLE.
   Meme mecanique que `appliquer()`, et le script est nettoye au
   demarrage suivant comme son homologue.

3. TROIS FICHIERS D'ETAT REJETAIENT LE PREMIER CHAMP AJOUTE. NodeConfig,
   Reglages et FeatureFlags portent leur filet `#[serde(default)]` ;
   EtatLumieres et OutputSettings ne l'avaient pas -- tous leurs champs
   etaient obligatoires. Le jour ou une version ajoute un champ, le
   `lumieres.json` du client part en `.corrompu` et la console repart
   vide : scenes et chasers du spectacle perdus, et ceux-la ne se
   reconstituent pas de memoire.

4. UNE CUE INCOMPRISE FAISAIT PERDRE TOUTE LA CONDUITE. Le filet
   `actions_tolerantes` existait pour les ACTIONS d'une cue, pas pour les
   cues elles-memes : `Declencheur` etant un enum etiquete sans repli, un
   declencheur inconnu -- conduite ecrite par une version plus recente,
   relue apres un retour arriere -- faisait echouer le fichier ENTIER.
   Meme patron applique aux cues : on ne perd plus que celle qu'on ne
   comprend pas.

Cinq tests ajoutes, dont la table de correspondance archive/capacites pour
Windows, Linux et Pi (logique rendue pure : testable pour toutes les
plateformes depuis n'importe quelle machine).

261 tests, clippy -D warnings, fmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
La section « Non publie » devient la 3.5.0 et couvre les DEUX chantiers :
la mesure de performance (deja ecrite) et l'audit des zones jamais
couvertes. Le manuel etant deja passe en v3.5, Cargo.toml devait suivre --
sinon l'UI affichait une version et le manuel une autre.

Corrige au passage, meme famille que le reste de l'audit :

LES REGLAGES DE PERFORMANCE ANNONCAIENT UNE RESOLUTION QU'ILS
N'APPLIQUAIENT PAS. `config.resolution` n'est lu qu'a UN endroit du depot :
la construction de la sortie DRM/KMS. En mode fenetre -- le defaut, et le
seul que pose install.sh -- la fenetre rend a la taille de sa surface, et
la valeur est purement decorative. Or l'installateur detecte un Pi 3,
ecrit 960×540 et annonce « Reglages de performance pi3 ecrits », le
journal confirmait « reglages appliques (largeur=960 hauteur=540 ) », et
la carte Systeme proposait les deux champs sans reserve. Personne ne
pouvait deviner que rien ne changeait.

Plutot que de faire semblant : le journal ne mentionne la resolution que
lorsqu'elle sert (mode KMS) et emet sinon un warn! explicite ; les deux
champs de l'UI portent la mention « (KMS) » et l'infobulle correspondante ;
le conseil Pi 3 de l'UI, celui de install.sh et celui du manuel disent
« 960×540 en sortie sans bureau » au lieu de le promettre partout.

Implementer vraiment la resolution de rendu en mode fenetre serait un
changement du chemin de rendu, a valider sur un Pi : c'est note en limite
assumee dans le CHANGELOG, pas fait en aveugle.

Restent dits ailleurs : node.toml.example previent que [output] et [midi]
sont sans effet sur l'archive ARM64 officielle, et deploy/README pointe
enfin la page Releases (publique) plutot que les seuls artefacts de
l'onglet Actions -- en disant ce que chaque archive sait faire.

Le nombre de tests du README est desormais le chiffre reel (261), pas une
estimation.

261 tests, clippy -D warnings, fmt, shellcheck, smoke.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
L'UI visait le node distant directement, en `mode: "no-cors"`. Depuis la
v3.4.0, toute requete mutatrice dont l'Origin ne correspond pas au Host est
refusee -- et le navigateur JOINT toujours Origin sur un POST
inter-origines. La cible repondait donc 403, trace en WARN dans SON journal
(que personne ne regarde). Comme la reponse d'un appel no-cors est opaque,
le `.catch` ne se declenchait jamais : l'UI affichait « Mire coins envoyee
a <node> » a tous les coups. Le geste sert justement a savoir quel
videoprojecteur correspond a quelle machine -- il repondait toujours oui,
sans rien faire.

Relais serveur-a-serveur, sur le modele de `fleet_media` : la cible doit
etre un node connu du parc, et c'est le jeton de parc -- jamais le mot de
passe de l'UI -- qui authentifie. L'anti-CSRF n'est pas affaibli.

`/api/identify` n'est VOLONTAIREMENT pas ajoute aux routes ouvertes par le
jeton de parc : ce jeton est commun a toutes les machines et les cibles
viennent d'un mDNS non authentifie, alors que la mire « coins » couvre la
sortie pendant 4 secondes -- donc en plein spectacle. Identifier un node
protege par mot de passe reste refuse, mais le message le DIT desormais, au
lieu d'annoncer un succes ; et il ne conseille pas de poser un fleet_token,
qui n'y changerait rien.

Verifie en reel sur le binaire 3.5.0 :
- OSCQuery publie 53 adresses contre 45 auparavant ; /rate, /blending,
  /cue/go, /dmx/scene, /dmx/chaser, /transport, /media et
  /playlist/position sont tous presents, /transport avec ACCESS 1.
- En mode KMS, /api/system et /api/outputs renvoient bien `fps: null` (et
  non plus 0.0, qui faisait passer la sortie pour morte) en meme temps que
  `rendu: null`.
- En mode fenetre avec un reglages.json pi3, le journal n'annonce plus
  « largeur=960 hauteur=540 appliques » mais un WARN « resolution de rendu
  IGNOREE : elle ne s'applique qu'a la sortie KMS » ; en mode KMS, aucun
  warn -- elle sert vraiment.

261 tests, clippy -D warnings, fmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
Le manuel promet depuis la v1, section Effets : « une intensite 0..1 par
effet — un fader MIDI ou OSC suffit a l'activer et le doser ». En OSC,
oui. En MIDI, non : `ScaleTarget` ne comptait que 9 cibles (le volume et
les 8 reglages couleur). Ni les cinq effets, ni la vitesse de lecture.

Et il n'y avait pas de contournement : un binding CC sans `scale` ne peut
renvoyer qu'une commande FIXE, donc une valeur CONSTANTE -- tourner le
fader rejouait la meme valeur. Un operateur qui suivait le manuel ecrivait
`scale = "pixelate"`... et le node REFUSAIT DE DEMARRER, sur une erreur de
desserialisation d'un enum, pour une simple cible inconnue.

Deux corrections, donc :

1. Six cibles ajoutees -- pixelate, posterize, noise, sharpen, mirror
   (bornees 0..1, la valeur du CC passe telle quelle) et rate (mise a
   l'echelle sur les bornes de SetRate, 0,25×–4×, pour que le bus n'ait
   aucune raison de refuser la commande a fond de course ; verifie par
   contre-epreuve en appliquant les deux extremes a l'etat).

2. `scale` recoit le meme filet que `command` : une cible mal
   orthographiee est ignoree avec un avertissement nommant la faute et la
   liste des cibles possibles, au lieu d'empecher le demarrage. C'est
   exactement la panne que la tolerance sur `command` avait ete ecrite
   pour eviter -- elle n'avait simplement jamais ete etendue au champ
   d'a cote.

Les cibles sont enfin ecrites la ou on les cherche : node.toml.example et
la section MIDI du manuel les listent toutes les quinze.

263 tests, clippy -D warnings, fmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
1. LE CLIENT MJPEG ETAIT ANNONCE SANS JAMAIS ETRE VERIFIE. Les deux
   collecteurs lancaient curl en arriere-plan et n'examinaient jamais son
   sort. Or /flux.mjpg repond 404 si la fonction « Apercu » est coupee --
   ce que le profil Pi 3 conseille explicitement --, 503 au-dela de
   4 clients et 401 si un mot de passe est pose. Dans les trois cas curl
   rendait la main en quelques millisecondes, le compositeur partage
   n'etait pas sollicite de tout le run, et « Charge : ~N commandes/s +
   client MJPEG » s'affichait quand meme. Le run mesurait autre chose que
   ce qu'il annoncait.
   Les deux harnais sondent maintenant le flux avant de l'annoncer, et
   disent clairement « charge reduite » avec le code HTTP sinon.

   Sonde verifiee en reel sur les trois cas : 200 flux sain, 404 apercu
   coupe, 000 node injoignable. Premiere version ecrite avec un
   `|| echo 000` de repli : elle rendait « 200000 » sur un flux sain, un
   flux MJPEG ne se terminant jamais, curl sort toujours en timeout apres
   avoir deja ecrit le code. Corrige en `|| true`.

2. LE VERDICT MEMOIRE IGNORAIT LES REDEMARRAGES. Le bandeau annonce
   « tendances sans valeur » quand le node a redemarre, et les images
   perdues sont bien neutralisees -- mais quarante lignes plus bas, la
   section memoire calculait sa pente A TRAVERS les vies du process. Une
   RSS qui passe de 380 Mo a 90 Mo puis remonte a 120 donne une
   regression NEGATIVE : le script imprimait « VERDICT : stable. » sur un
   run traverse par un plantage, et par une fuite possible. Le verdict est
   desormais « non calculable » avec la raison.

3. LES SCRIPTS POWERSHELL N'ETAIENT VERIFIES NULLE PART. shellcheck ne
   couvre que les .sh ; l'installateur Windows et le collecteur
   d'endurance ne tournent jamais en CI. Une coquille de syntaxe n'y etait
   decouverte que par l'utilisateur, sur sa machine, au moment ou il en a
   besoin. Le job check-windows analyse maintenant tous les .ps1 du depot
   avec le parseur PowerShell et echoue en nommant les lignes fautives.
   (C'est ce controle qui manquait pour valider les modifications
   ci-dessus : elles ne sont pas executables ailleurs que sous Windows.)

   Au passage : la sonde PowerShell neutralise
   $PSNativeCommandUseErrorActionPreference le temps de l'appel. Le script
   tourne sous $ErrorActionPreference = "Stop" et, depuis PowerShell 7.4,
   un code de retour non nul d'une commande native est une erreur
   terminante -- la sonde aurait tue le run qu'elle prepare, curl sortant
   toujours en timeout sur un flux sans fin.

263 tests, clippy -D warnings, fmt, shellcheck.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
Le ZIP contenait la LISTE des noms de presets -- pas leur contenu -- et
aucun des fichiers d'etat du node : ni lumieres.json (la console), ni
sequences.json (la conduite du spectacle), ni fonctions.json, reglages.json
ou demarrage.json.

C'est pourtant le seul export que le produit propose, donc la seule
sauvegarde qu'un operateur non developpeur puisse faire avant une mise a
jour -- et le manuel annonce « une archive avec […] les presets ». Le
scenario qui fait mal : coupure de courant pendant une ecriture, fichier
tronque, `charger_ou_mettre_de_cote` met lumieres.json de cote en
`.corrompu` au redemarrage, la console repart vide. L'archive exportee « au
cas ou » ne permettait pas de revenir en arriere. Ce sont justement les
fichiers qu'on ne reconstitue pas de memoire.

L'archive porte desormais `presets/<nom>.json`,
`presets/mapping/<nom>.json` et `etat/<fichier>.json`. Les fichiers d'etat
sont lus BRUTS, pas re-serialises : si l'un est corrompu, c'est l'original
qu'on veut examiner. Tout est en meilleur effort -- un preset illisible ne
doit pas faire echouer l'export, c'est justement quand ca va mal qu'on le
demande.

Verifie en reel sur un node : apres un preset_save et un mapping_save,
l'archive contient presets/soiree.json (1165 o) et
presets/mapping/salon.json (376 o) ; avec lumieres.json, sequences.json et
reglages.json sur le disque, elle contient les trois sous etat/, au
contenu identique a l'original.

Le manuel dit ce que l'archive contient vraiment, et qu'elle sert de
sauvegarde avant mise a jour.

263 tests, clippy -D warnings, fmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
…perdu

La fenetre ne detenait qu'un Receiver sur les reglages de sortie : aucun
Sender, donc aucune republication possible. F11 et Echap changeaient l'etat
de plein ecran directement sur la fenetre winit, et personne ne le disait au
reste du node.

Consequences en chaine :
- /api/outputs continuait d'annoncer l'ancienne valeur ;
- la case « Plein ecran » de l'UI restait desynchronisee ;
- et surtout, comme l'UI renvoie TOUJOURS les deux champs, le reglage
  suivant reappliquait le `fullscreen` perime. Mettre la sortie en plein
  ecran avec F11 sur la machine (le geste que l'infobulle de la carte
  « Sortie » recommande), puis changer d'ecran cible depuis une tablette,
  faisait sortir la sortie du plein ecran toute seule -- en plein
  spectacle, et sans que personne ne comprenne pourquoi.

La fenetre recoit desormais le MEME canal en ecriture et publie son etat
reel.

Le piege etait dans le retour : republier declenche le relais, donc
Wake::SettingsChanged, donc apply_settings -- dont la branche « fenetre »
execute set_outer_position(). Sans precaution, la fenetre aurait saute a
l'origine de l'ecran juste apres chaque Echap. La fenetre retient donc le
dernier reglage qu'elle a REELLEMENT applique et ignore l'echo de ses
propres publications ; apply_settings devient idempotent.

La decision de publication est isolee dans une fonction pure
(`publication_plein_ecran`) pour etre testable : le reste demande un event
loop winit, donc un serveur graphique. Le test verifie qu'on publie bien
dans les deux sens, que l'ecran cible n'est pas emporte au passage, et
qu'on ne publie RIEN quand l'etat est deja le bon -- sinon chaque bascule
reveillerait l'event loop pour rien et relancerait un cycle d'echo.

264 tests, clippy -D warnings, fmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
@pymenvert pymenvert changed the title Audit des zones jamais couvertes : dérive doc/réalité, mentions légales, contrats de contrôle v3.5.0 — audit des zones jamais couvertes : ce que Lanterne dit vs ce qu'il fait Aug 5, 2026
claude added 9 commits August 5, 2026 05:48
`CommandeLumieres` compte 12 variantes, mais seules les scenes et les
chasers avaient un pont vers le bus. Le grand master et les niveaux de
fader n'existaient dans AUCUN point du vocabulaire `Command` : ils ne
passaient que par POST /api/dmx, donc uniquement depuis l'interface web.

Consequence : poser un fader de surface MIDI sur le grand master -- le
geste le plus canonique d'une console lumiere -- etait impossible. Un
binding CC sans `scale` ne peut envoyer qu'une valeur constante, et aucune
cible n'existait. Depuis Chataigne, meme impasse : pas d'adresse OSC. Et
une cue du sequenceur ne pouvait pas davantage poser un niveau.

La chaine complete est posee, dans l'ordre ou elle est traversee :

- `Command::DmxMaster { valeur }` et `Command::DmxFader { id, valeur }`
  (ajout PUR : aucun format JSON existant n'est modifie), avec leurs deux
  lignes dans la table de documentation -- que le test ajoute plus tot
  dans cette branche exige desormais ;
- les evenements correspondants, et leur traduction en `CommandeLumieres`
  dans l'abonne Art-Net (il consomme des Event, pas des Command) ;
- les adresses OSC `/dmx/master <0..255>` et `/dmx/fader <id> <0..255>`,
  en entree comme en retour d'etat ;
- les deux feuilles OSCQuery, sans quoi Chataigne ne les verrait pas --
  c'est le defaut que cette branche a deja corrige pour la regie ;
- la cible de fader MIDI `dmx_master` (0..127 → 0..255, et 127 donne bien
  255, pas 254).

Le parseur OSC accepte un entier 0..255 (la convention de la console) ET
un flottant FRACTIONNAIRE entre 0 et 1, parce que beaucoup de surfaces OSC
envoient leurs faders normalises : sans cela, un 0.5 devenait 0, c'est-a-
dire un noir au lieu d'un demi-niveau. Hors bornes : refuse, jamais
tronque en silence.

Verifie en reel de bout en bout : `/dmx/master 77` envoye en UDP a un node
fait bien passer le master de 255 a 77 dans /api/dmx, et les deux adresses
apparaissent dans l'arbre OSCQuery.

264 tests, clippy -D warnings, fmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
La 3.5.0 gagne une section « Ce qui devient pilotable » : corriger ce qui
etait DIT a mis au jour des fonctions reellement injoignables (faders MIDI
sur les effets et la vitesse, master et faders lumieres, /transport et
/media en lecture seule dans OSCQuery, export diagnostic devenu une vraie
sauvegarde).

CLAUDE.md porte l'etat de la version, ses deux limites assumees (unite
systemd qui ne projette pas en mode fenetre ; resolution de reglages.json
limitee a la sortie KMS) et le point laisse a Pym sur la formulation de
deny.toml -- la politique elle-meme n'a pas ete touchee, elle touche a la
decision sur gst-plugins-ugly qu'on ne rouvre pas.

Les prochaines etapes gagnent deux points precis pour le retour au
materiel : valider le bloc DISPLAY/graphical.target du service systemd sur
Pi OS Desktop, et decider si l'on publie une archive Linux/Pi AVEC
GStreamer.

Le chiffre « 45 a 55 adresses OSCQuery » est mesure sur un node reel, pas
estime.

264 tests, clippy -D warnings, fmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
Defaut introduit par le commit precedent, trouve en relisant mes propres
correctifs -- exactement le genre que la v3.4.1 avait ete creee pour
attraper.

`octet_arg` distinguait « niveau DMX » et « fader normalise » sur
`fract() != 0.0`. Or 1.0 n'a pas de partie fractionnaire : un fader
normalise pousse A FOND repartait sur le chemin entier et donnait 1, pas
255. La discontinuite tombait donc precisement la ou personne ne la
cherche :

  Float(0.5)   -> 128
  Float(0.999) -> 255
  Float(1.0)   -> 1     <-- quasi noir

Mesure sur le parseur reel avant correction, pas deduite.

C'est desormais le TYPE qui decide, jamais la valeur : un entier est un
niveau DMX 0..255 (ce que declare OSCQuery, donc ce que Chataigne envoie),
un flottant est un fader normalise 0..1 (la convention des surfaces OSC).
Regle simple, sans cas particulier, et enoncee telle quelle dans le
manuel.

Le test couvre maintenant les extremes des deux conventions -- 0.0, 1.0,
et les refus hors bornes dans les deux sens (Int(-1), Int(300),
Float(1.5)) -- au lieu du seul 0.5 qui passait des deux facons.

264 tests, clippy -D warnings, fmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
Suite de la relecture de mes propres correctifs. `publier_plein_ecran`
lisait les reglages courants, modifiait sa copie, puis la publiait avec
`send_replace` : une lecture-modification-ecriture NON atomique.

Course possible, etroite mais reelle : F11 presse sur la machine pendant
qu'un changement d'ecran cible arrive de la tablette. La copie lue avant
le changement etait republiee apres, et ramenait la sortie sur l'ancien
ecran -- le symptome meme que ce correctif etait cense faire disparaitre.

`send_modify` fait la modification SOUS le verrou du canal : le champ
`fullscreen` est le seul touche, les autres gardent la valeur qu'ils ont
au moment de l'ecriture. Le marquage « deja applique » est pris APRES,
depuis la valeur reellement retenue, pour que l'echo renvoye par le relais
reste reconnu comme le notre.

264 tests, clippy -D warnings, fmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
`l_enchainement_suit_le_nom_pas_l_index` a echoue sur check-windows
(97213ec) apres avoir passe cinq executions de suite sur cette meme
branche : aucun code en cause, une marge de temps trop courte.

Le test dormait 400 ms pour un enchainement programme a 300 ms, soit
100 ms de marge -- sur un runner Windows charge, ou chaque enregistrement
de cue passe par un fsync de sequences.json, la marge est parfois
depassee.

Il ATTEND desormais l'evenement au lieu de le parier : sonde toutes les
20 ms, jusqu'a 5 s, qu'une cue ENCHAINEE ait joue. Le detail qui compte :
la condition porte sur « `derniere` n'est plus A », pas sur « une cue a
joue » -- A est la cue lancee a la main, elle devient `derniere`
immediatement, et une premiere version de ce correctif sortait donc de la
boucle avant meme l'enchainement (le test l'a dit tout de suite :
left: Some("A")).

La garantie du test est intacte, et c'est verifie et non suppose :
contre-epreuve en remettant volontairement l'enchainement par INDEX --
le test echoue bien, en nommant le coupable (left: Some("C")). Puis
20 executions consecutives du test corrige : 20 ok, 0 echec.

Les deux assertions sont aussi inversees pour que la plus parlante tombe
en premier : savoir QUELLE cue a joue explique la panne, un volume
inattendu la fait seulement soupconner.

264 tests, clippy -D warnings, fmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
Relecture adversariale des correctifs de cette branche -- la demarche meme
qui avait justifie la v3.4.1. Quatre defauts confirmes DANS mes propres
corrections, dont deux regressions que j'avais introduites.

1. LE MASTER POSE PAR OSC/MIDI/CUE N'ETAIT JAMAIS PERSISTE. Le commit
   f188a89 annoncait « la chaine complete est posee ». Elle s'arretait une
   ligne trop tot : le bras `commandes` (web UI) fait
   `if appliquer(...) { a_sauver = true }`, le bras `evenements` -- celui
   que j'avais etendu -- jetait la valeur de retour. Deux entrees vers la
   meme console, deux comportements : un master pose depuis Chataigne ou
   un fader MIDI vivait en memoire et disparaissait au redemarrage,
   pendant que le meme geste depuis la tablette survivait.
   Verifie en reel : `/dmx/master 77` en UDP, puis arret du node ->
   lumieres.json porte bien master 77, et le node relance le relit.

2. `/dmx/chaser ""` N'ARRETAIT AUCUN CHASER. Le retour d'etat emet la
   chaine vide pour un arret, « symetrique de /lut » disait le
   commentaire ; l'entree, elle, gardait `Some("")` et cherchait un chaser
   nomme « ». Or vider le champ est le SEUL geste d'arret possible depuis
   Chataigne, qui ne sait pas envoyer un message sans argument sur un
   parametre type « s ». La symetrie n'existait que dans un sens.

3. octet_arg REFUSAIT `90.0`. Ma correction precedente decidait sur le
   seul TYPE : entier = niveau DMX, flottant = fader normalise. Mais
   `int_arg`, dix lignes plus haut, documente que « Chataigne envoie
   volontiers 90.0 pour 90 » -- l'emetteur le plus courant du projet se
   voyait donc refuser ses messages. Troisieme version, et la bonne : un
   flottant FRACTIONNAIRE est normalise, un flottant a valeur entiere est
   un niveau DMX, et la seule ambiguite irreductible (1.0 : niveau 1, ou
   fader a fond ?) est tranchee en faveur du fader a fond -- un master a
   1/255 ne se vise pas a la main, et reste atteignable en entier. Le
   choix est ecrit dans le code, pas laisse implicite.

4. LE FADER MIDI « rate » NE POUVAIT PAS RENDRE LA VITESSE NORMALE. Mise a
   l'echelle lineaire sur [0,25 ; 4] : 1x tombait a CC 25,4, une position
   qu'aucun controleur ne peut emettre. Les deux crans encadrants donnaient
   0,988x et 1,018x -- assez pour faire deriver une boucle video d'une
   bande son, et sans retour possible depuis la surface. Echelle
   GEOMETRIQUE desormais : 0,25x / 0,5x / 1x / 2x / 4x aux quarts, comme
   se pense un fader de vitesse.

Corrige aussi : la liste des cibles de fader affichee au demarrage
oubliait `dmx_master`, ajoutee la veille -- elle est maintenant unique
(ScaleTarget::NOMS) et tenue par un test, au lieu d'etre recopiee a la
main ; et la regle « entier ou flottant » n'etait ecrite que dans le
manuel : elle l'est aux quatre points de contrat (table de command.rs,
deux feuilles OSCQuery, manuel).

265 tests, clippy -D warnings, fmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
Deuxieme lot de la relecture adversariale de mes propres correctifs.

1. L'OTA ANNONCAIT « A JOUR » ALORS QU'UNE VERSION PLUS RECENTE EXISTAIT.
   En apprenant a refuser une mise a jour degradante, j'ai fait rendre
   `asset: None` dans des cas REELS (tout Linux/Pi compile avec GStreamer,
   tout aarch64 compile sur place). Or l'UI, que je n'avais pas touchee,
   teste `if (etat.plus_recente && etat.asset)` et tombe sinon dans la
   branche « A jour (vX) ». Le refus prudent se transformait donc en
   mensonge tranquille : le node savait qu'une v3.6.0 existait et affirmait
   etre a jour.
   `EtatMiseAJour` porte desormais une `raison`, et l'UI l'affiche :
   « v3.6.0 existe, mais : ce binaire a ete compile sur place… ».

2. LE MESSAGE DE REFUS PARLAIT DE LA VIDEO A QUI N'EN A PAS. Le meme texte
   servait aux trois cas de refus. Sur un Pi compile sur place SANS
   gstreamer -- le cas que decrit install.sh -- il annoncait « vous ferait
   perdre la lecture video » a quelqu'un qui n'en a jamais eu, et taisait
   ce qu'il perdrait vraiment : la fenetre de sortie, donc toute
   projection. Le message est construit depuis les capacites reelles.

3. L'EXPORT DIAGNOSTIC SAUTAIT LE PRESET QU'IL FALLAIT SAUVER. J'avais
   applique la lecture BRUTE aux cinq fichiers d'etat, et laisse les
   presets passer par `load()` -- donc par la validation ET une
   re-serialisation. Trois consequences : un preset au JSON abime etait
   silencieusement absent de l'archive (c'est-a-dire exactement celui
   qu'on exporte pour le recuperer), un preset refuse par la validation
   aussi, et tout champ ecrit par une version plus recente disparaissait
   a la re-serialisation.
   `PresetStore::octets` lit le fichier tel qu'il est. Verifie en reel :
   avec un `presets/casse.json` volontairement tronque, l'archive le
   contient desormais octet pour octet.

4. LE FILTRE D'ECHO DE LA FENETRE POUVAIT AVALER UN REGLAGE LEGITIME.
   `send_modify` avait supprime l'ecrasement DANS le canal, mais le
   marqueur « deja applique » recopiait ensuite le canal entier. Si un
   reglage venu de l'UI y etait deja sans que l'event loop l'ait traite
   (il peint une frame), un F11 le faisait passer pour notre propre echo :
   l'ecran cible choisi depuis la tablette n'etait jamais applique. Le
   marqueur part maintenant de ce que la fenetre a REELLEMENT applique.

5. Le rapport.txt EMBARQUE dans l'archive -- le premier fichier qu'on
   ouvre, et le seul qui voyage avec elle -- enumerait encore l'ancien
   contenu. Il annonce les dossiers presets/ et etat/, et dit que
   l'archive sert de sauvegarde avant mise a jour.

265 tests, clippy -D warnings, fmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
`go_joue_et_enchaine` a echoue sur check-windows apres que
`l_enchainement_suit_le_nom_pas_l_index` a echoue au run precedent : deux
tests differents du MEME module, tour a tour, sans qu'aucun code soit en
cause. J'avais corrige le premier en concluant qu'il n'y avait pas lieu de
toucher aux autres. C'etait la mauvaise lecture : ce n'est pas UN test
fragile, c'est une famille -- les tests du sequenceur dorment une duree
fixe pour attendre un enchainement dont l'echeance est du meme ordre, sur
un service qui fsync sequences.json a chaque enregistrement de cue.

Une aide commune `attendre(etat, condition)` remplace les paris : elle
sonde l'etat publie toutes les 20 ms, jusqu'a 5 s. Les trois attentes des
deux tests concernes y passent.

Le cas interessant est le dernier bloc de `go_joue_et_enchaine`, qui
affirme qu'il ne se passe RIEN apres un Stop. Une attente ne peut pas y
remplacer le delai -- mais elle garantit l'etat de DEPART, et c'est la que
le bat blessait : dormir 100 ms en esperant que l'enchainement soit arme,
c'etait risquer d'envoyer Stop avant qu'il y ait quoi que ce soit a
annuler. Le test aurait alors reussi sans rien prouver, ce qui est pire
qu'un echec. Il attend maintenant que `en_attente` soit rempli, l'affirme,
puis annule et verifie l'annulation avant de laisser passer l'echeance.

Verifie : 25 executions consecutives de tous les tests du sequenceur, puis
4 suites en parallele pour reproduire la charge d'un runner -- 0 echec.
Les autres `sleep` fixes du depot (player, bascules, sync, http) sont
courts et sans echeance concurrente ; ils restent en l'etat plutot que
d'etre reecrits en aveugle.

265 tests, clippy -D warnings, fmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
`build-windows-gstreamer` echoue depuis le 2026-08-05 sur « dossier
GStreamer : INTROUVABLE ». La cause est ailleurs, et elle accuse le
mauvais coupable :

  Unable to find package 'pkgconfiglite'.
  Chocolatey installed 0/0 packages.

`pkgconfiglite` a disparu du depot communautaire. Or chocolatey traite une
commande comme UNE transaction : le paquet introuvable a annule les deux
autres, et GStreamer -- parfaitement disponible, lui -- n'a jamais ete
installe. Le message d'erreur pointait donc GStreamer alors que seul un
utilitaire annexe manquait.

Les deux workflows installent desormais les paquets separement, et le
message dit ce qui manque vraiment. GStreamer sera bien installe ; il
restera a trouver un pkg-config si le paquet ne revient pas.

CE N'EST PAS UNE PANNE DE CETTE BRANCHE : elle se reproduit sur main, et
le job est en continue-on-error, donc la PR n'est pas bloquee. Mais elle
merite d'etre signalee : ce job produit
`toolbox-node-windows-x64-gstreamer`, c'est-a-dire le SEUL binaire publie
qui lit des videos. Tant que pkg-config manque au runner, le pack video ne
peut pas etre reconstruit -- point a trancher avec Pym (paquet de
remplacement, ou pkg-config telecharge directement).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ

Copy link
Copy Markdown
Owner Author

Point à trancher : le pack vidéo Windows ne peut plus être construit — cause externe, pas cette branche.

Le job build-windows-gstreamer échoue depuis aujourd'hui sur « dossier GStreamer : INTROUVABLE ». Le message accuse le mauvais coupable ; la vraie cause est deux étapes plus haut :

Unable to find package 'pkgconfiglite'.
Chocolatey installed 0/0 packages.

pkgconfiglite a disparu du dépôt communautaire Chocolatey. Comme Chocolatey traite une commande comme une transaction, le paquet introuvable a annulé les deux autres : GStreamer — parfaitement disponible, lui — n'a jamais été installé.

Ce n'est pas une régression de cette PR : la même commande est sur main, et le job est en continue-on-error, donc la PR n'est pas bloquée.

Ce que j'ai fait : séparer les installations dans ci.yml et release.yml, pour qu'un paquet annexe manquant n'emporte plus GStreamer et que le journal nomme ce qui manque réellement.

Ce que je n'ai pas fait, et pourquoi : trouver un remplaçant à pkg-config sur le runner. Sans lui les crates *-sys ne compilent pas, donc le pack reste non constructible. Les pistes (autre paquet Chocolatey, pkg-config téléchargé directement, vcpkg) ne sont vérifiables que sur un runner Windows — je ne peux pas les tester ici, et poser un correctif non vérifié sur la chaîne qui produit un binaire livré ne me paraît pas le bon geste.

Pourquoi ça compte : ce job produit toolbox-node-windows-x64-gstreamer, c'est-à-dire — comme cette PR vient de le documenter partout — le seul binaire publié qui lit des vidéos. Tant que ce point n'est pas réglé, une nouvelle release ne contiendrait aucun binaire capable de lire une vidéo.


Generated by Claude Code

claude added 2 commits August 5, 2026 06:32
…rriger

Troisieme et dernier lot de la relecture adversariale de mes correctifs.

CE QUI NE MARCHAIT PAS, DANS MES PROPRES CORRECTIFS :

1. LE SELECTEUR DE MEDIA DES CUES RESTAIT VIDE. J'avais corrige la valeur
   qu'il ecrit (m.path au lieu de m.name) sans voir qu'il n'etait rempli
   qu'a l'evaluation du script -- donc AVANT l'arrivee de la mediatheque.
   Et « Charger un media » est le type d'action par DEFAUT : en ouvrant
   l'onglet Sequences on trouvait un menu vide, et il fallait changer de
   type puis revenir pour le peupler. Le correctif etait juste et
   inoperant. `loadMedia` rappelle desormais `majChampsAction`.

2. LE FILTRE DU PARC N'APPLIQUAIT QU'UNE DES DEUX REGLES du serveur.
   `validate_upload_name` exige un nom plat ET un jeu de caracteres
   restreint : « ete.mp4 » avec un accent restait propose, pour etre
   refuse ensuite par un message technique. Le filtre est maintenant le
   miroir de la regle, et le cas « aucun media eligible » est dit au lieu
   d'un menu vide sans explication.

3. LE SONDAGE DES ECRANS RECONSTRUISAIT LE MENU TOUTES LES 5 SECONDES.
   En ajoutant le rafraichissement (pour voir un videoprojecteur branche
   a chaud) j'ai rendu la carte « Sortie » penible a la souris : le menu
   se refermait sous le doigt. Il n'est reconstruit que si la liste a
   VRAIMENT change, et la selection n'est pas ecrasee quand l'operateur a
   le champ sous le curseur.

4. « RESOLUTION IGNOREE » REGARDAIT LE MODE, PAS LA FEATURE.
   `config.resolution` n'est lu que par la sortie KMS, elle-meme sous
   `#[cfg(feature = "gstreamer")]`. Un binaire sans GStreamer configure en
   mode « kms » annoncait donc la resolution comme appliquee alors que
   rien ne la lit. Le terme manquant est ajoute.

LA DOC REDISAIT CE QU'ON VENAIT DE CORRIGER -- le pire resultat possible
pour une branche dont le sujet est justement la derive doc/realite :

5. La ligne /dmx/master du manuel affirmait encore, EN GRAS, la regle
   « c'est le type qui decide » -- celle que le commit precedent avait
   supprimee comme un defaut. Je n'avais reecrit que la ligne /dmx/fader.
6. La carte « Ubuntu / Debian » avait perdu les paquets d'EXECUTION en
   gagnant ceux de compilation : quelqu'un qui recompile avec
   --features gstreamer se serait retrouve sans decodeurs, donc toujours
   sans video. Les deux temps sont retablis.
7. L'arret d'un chaser par chaine vide -- implemente la veille -- n'etait
   documente nulle part, ni dans le manuel ni dans OSCQuery.
8. La liste des cibles de fader du manuel oubliait `dmx_master` : la faute
   EXACTE que le commit precedent pretendait avoir eradiquee, sur une
   troisieme copie manuscrite que je n'avais pas vue.
9. Trois documents annoncaient trois nombres de tests differents, dont
   aucun n'etait le bon. Ils annoncent le chiffre mesure : 267.
10. Le doc-comment de ScaleTarget::Rate decrivait encore l'echelle
    lineaire remplacee par le meme commit.

Deux tests ajoutes la ou RIEN ne retenait le correctif : `fps` null quand
la mesure n'existe pas (contre-epreuve faite : le test echoue bien si on
remet le zero dur), et le refus d'un node inconnu du parc par le relais
d'identification.

267 tests, clippy -D warnings, fmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ
Fin de la relecture adversariale. Trois defauts restaient ouverts ; l'un
d'eux etait dans le correctif que je venais d'ecrire.

1. LE SELECTEUR DE CUE AURAIT EFFACE LA SELECTION DE L'OPERATEUR. Mon
   correctif precedent faisait rappeler `majChampsAction` par `loadMedia`,
   pour que le menu « Charger un media » ne soit plus vide. Mais loadMedia
   est SONDE toutes les 5 s tant que la page Medias est visible : le menu
   aurait ete reconstruit sous le doigt, et le media deja choisi dans
   l'onglet Sequences remplace en silence par le premier de la liste --
   l'operateur aurait enregistre une cue qui ne charge pas ce qu'il croit.
   La selection est desormais preservee (meme precaution que
   renderParcMedia), pour les medias comme pour les presets, et le rappel
   est limite au type d'action concerne (sinon un GET /api/dmx toutes les
   5 s pour rien).

2. UN TEST QUI ASSURAIT UNE PROPRIETE JETEE A L'EXECUTION.
   `publication_plein_ecran` construisait un OutputSettings dont la
   production ne gardait plus que `.is_none()` : l'assertion centrale du
   test -- « l'ecran cible ne doit pas bouger » -- ne protegeait donc plus
   rien. Une refonte aurait pu ramener le defaut d'origine avec une suite
   verte. La fonction rend maintenant la valeur que la production RETIENT
   vraiment, et prend les deux entrees qui comptent : le canal decide s'il
   y a quelque chose a dire, ce que la fenetre a applique sert de base. Un
   test couvre le cas qui compte -- un reglage de l'UI encore en attente ne
   doit pas etre absorbe par un F11.

3. UNE CUE INCOMPRISE DISPARAISSAIT DEFINITIVEMENT. En rendant la lecture
   tolerante, j'avais supprime sans le voir le filet qui existait : avant,
   un fichier illisible partait en `.corrompu` et restait recuperable EN
   ENTIER. Maintenant qu'on le lit avec succes, la premiere reecriture de
   la conduite l'amputait pour de bon. Le cas vise est precisement le
   retour arriere apres une mise a jour, ou les cues perdues sont
   parfaitement valides pour la version qui les a ecrites.
   `sequences.json.incomplet` conserve l'original quand au moins une cue a
   ete ecartee, avec un WARN qui le dit. Deux tests : la copie porte bien
   la conduite entiere, et une conduite saine n'en laisse aucune.

269 tests, clippy -D warnings, fmt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01We2sFzNwRT8Juwd8Ai1LDZ

Copy link
Copy Markdown
Owner Author

Correction du commentaire précédent : le pack vidéo se reconstruit bien.

J'y écrivais que « le pack reste non constructible » tant que pkg-config manquait. C'est faux, et la séparation des installations a suffi.

Run 30982052964 : build-windows-gstreamer success, artefact toolbox-node-windows-x64-gstreamer produit et téléversé — 696 fichiers, 295 Mo. Les dix jobs de la CI sont verts.

Ce qui s'est passé : Chocolatey traite une commande comme une transaction, donc pkgconfiglite introuvable annulait aussi gstreamer et gstreamer-devel. En séparant les installations, GStreamer s'installe, et pkg-config s'est avéré disponible — le job va jusqu'au bout.

Il n'y a donc rien à trancher de ce côté : la release peut produire son pack vidéo. Le seul point qui demandait une décision dans ce fil est résolu ; je laisse le commentaire d'origine pour la trace du diagnostic.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants