Les noms de ports n'étaient modélisés nulle part — des marqueurs littéraux
dans le générateur, remplacés à la main dans la sortie et perdus à chaque
régénération. Seul endroit du devis où le travail était refait à répétition.
Ils se déclarent par équipement sous quatre clefs correspondant aux quatre
natures de lien : `hyperviseurs` et `frontiere` (terminaux, portfast),
`rayons` (côté routeur), `montante` (côté switch d'accès).
Le devis émet les vrais ports, y compris PLUSIEURS vers les hyperviseurs —
il n'en supposait qu'un, alors que le cluster en compte trois. Non déclarés,
les marqueurs reviennent, par équipement : un switch renseigné et un autre
non cohabitent.
La partie B devient par switch : gestion, montante et ports terminaux
diffèrent d'une machine à l'autre, un bloc commun n'avait plus de sens.
Gardes exercées : port déclaré deux fois, rayon vers un switch inconnu,
`rayons` sur autre chose que le routeur, `montante` sur le routeur.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La partie A déclarait ses ports de bord deux fois — l'interface en section
4, son portfast en section 6 — alors que la partie B posait tout dans le
même bloc. Deux conventions pour la même chose dans un seul document.
Le portfast est posé avec son interface (sections 4 et 4b). La section 6 se
réduit au global — mode, priorité du pont racine — plus les deux
avertissements : les rayons de la 4c n'en sont volontairement pas, et
pourquoi BPDU guard n'est pas émis.
Vérifié : aucune interface déclarée deux fois dans une même partie,
3 portfast avec `stp`, 0 sans lui, 0 sur un rayon.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'ajout du spanning-tree venait de rendre dangereuse une imprécision de
titre. La section 4 s'appelait « Trunk vers Proxmox + inter-switch » et
n'émettait qu'un port, que la section 6 déclarait en bord de réseau.
Réutiliser ce placeholder pour les rayons revenait à mettre portfast sur les
liens qui portent les BPDU — désactiver la protection anti-boucle là où elle
sert.
Les rayons sont dérivés et émis à part (4c côté routeur, B3a côté accès), un
par switch d'accès, marqués comme non-bord. La section 4 ne désigne plus que
les hyperviseurs ; la partie B distingue sa montante de son trunk terminal.
`underlay.switches_acces()` devient la source unique du « qui est un switch
d'accès » — pour leur devis ET pour les rayons côté routeur.
Vérifié : 3 commandes portfast avec `stp` déclaré, 0 sans lui, 0 sur un
rayon.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le devis ne disait rien du spanning-tree. Sur une fabric convergée à trois
switches, une boucle par brassage accidentel est une tempête de diffusion.
La topologie se déclare (`stp: {mode: rstp, topologie: etoile}`) et le devis
en tire la configuration. Le routeur est désigné pont racine : il est le
centre de l'étoile, tous les chemins passent déjà par lui, donc l'arbre
logique suit le câblage physique plutôt qu'une élection arbitraire. Les
switches d'accès reçoivent une priorité haute — jamais racine.
Ports terminaux déclarés en bord de réseau. BPDU guard délibérément NON
émis : un pont Linux dont le STP serait activé enverrait des BPDU et ferait
tomber le port côté hyperviseur. Le devis dit pourquoi et à quelle condition
l'ajouter.
En étoile aucun lien n'est redondant : RSTP est un filet, pas une nécessité.
Le devis le dit au lieu de laisser croire à une protection indispensable.
Validation : `mode` et `topologie` contrôlés, `stp` sans `routeur` refusé
(aucun pont racine désignable). Sans `stp`, la section signale l'absence de
protection au lieu de disparaître.
Réserve : la forme `binardat` du spanning-tree n'est pas vérifiée sur le
matériel, comme les `ip route`.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les VLAN et les trunks étaient déjà filtrés par fabric ; le roster de la
section 0, non. Un équipement d'une autre fabric — les switches de stockage,
le jour où ils seront déclarés — serait apparu en commentaire dans un devis
qui ne le concerne pas. Un devis est une configuration qu'on applique, pas
un inventaire.
Vérifié en déclarant un hôte de la fabric stockage : il reste absent, et la
sortie du jour est inchangée.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le stockage jumbo (iSCSI, Ceph) est porté par un réseau indépendant de deux
switches 10G, sans câble commun avec la fabric convergée des sleipnir. Le
modèle l'ignorait : le devis déclarait les VLAN 20/30/31 sur les switches
convergés et les mettait dans leurs trunks. C'était faux.
Chaque réseau porte une `fabric` (`principal` par défaut). Le devis ne
configure que celle du routeur, et énonce explicitement ce qu'il ne couvre
pas au lieu de le taire — VLAN, trunks et spanning-tree d'une autre fabric
ne le regardent pas.
Les `deny` de l'ACL couvrent en revanche toutes les fabrics : la règle porte
sur l'adresse de destination, pas sur le câblage. Si un chemin s'ouvre un
jour vers le stockage, il est déjà fermé.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les frontières occupent le bas du /29, le SVI du switch le haut. `.3` reste
libre pour une future IP virtuelle CARP si les deux OPNsense passent en
haute disponibilité — ce jour-là, `passerelle_sortie` pointera sur la VIP
plutôt que sur un boîtier nommé, et ce sera le seul changement.
Plan du lien :
10.0.4.1 bifrost-1 frontière active, sortie par défaut de la flotte
10.0.4.2 bifrost-2 seconde frontière
10.0.4.3 libre réservée VIP CARP
10.0.4.6 sleipnir-01 SVI du switch routeur
Les deux devis suivent sans intervention : routes du switch vers 10.0.4.1,
routes tenants de la frontière via 10.0.4.6.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`bifrost-0` et `bifrost-1` sont réservés aux deux frontières OPNsense —
Bifröst est le pont vers l'extérieur. Les switches internes deviennent
`sleipnir-01..03` : le cheval qui traverse les mondes, pas le pont qui en
sort. La division du nom suit celle de l'architecture.
Les deux boîtiers sont déclarés comme hôtes du lien de transit (10.0.4.2,
10.0.4.3) : hors flotte Ansible, la déclaration documente le lien et réserve
les noms. Le /29 choisi plus tôt les loge tous les deux.
Conséquence traitée : la partie B du devis aurait listé les deux pare-feux
parmi les « switches d'accès ». Elle ne retient plus que les hôtes du réseau
de management — un équipement déclaré ailleurs ne reçoit aucune ligne de
configuration de switch.
Le devis frontière nomme le boîtier quand il est déclaré.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deux incohérences visibles à la relecture du devis complet.
`bifrost-01` portait le SVI Vlan10 -> 10.0.0.1 et était déclaré dans
`underlay.hotes` à 10.0.0.2. Une interface VLAN n'a qu'une adresse primaire :
les deux ne pouvaient pas être vraies. L'entrée datait d'avant la désignation
du routeur, quand 10.0.0.1 était une passerelle abstraite. Le routeur est
déclaré à l'adresse du SVI qu'il porte, et `make underlay` refuse désormais
la divergence.
La partie B créait `vlan 40` alors que son trunk ne le transporte pas — le
transit ne relie que le routeur à la frontière. VLAN exclu de la partie B,
comme il l'est déjà des trunks généraux.
Preuves : 24 OK, 0 échec ; garde de cohérence exercée.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sans MLAG, le routage est porté par un unique switch (`underlay.routeur`).
Le devis émettait un jeu unique de SVI sans dire à quel switch il
s'adressait : appliqué sur les trois, il aurait créé autant de conflits
d'adresses qu'il y a de zones.
Il se scinde désormais en deux :
- partie A (switch routeur) : VLANs, SVI, ACL, trunks, routes ;
- partie B (switches d'accès, L2 pur) : mêmes VLANs pour commuter les trames
étiquetées, une adresse de gestion par switch tirée de `underlay.hotes`
avec `ip default-gateway` vers le routeur, et les trunks. Aucun SVI de
zone, aucune ACL, aucune route.
Gardes : `make underlay` refuse un `routeur` inconnu des hôtes déclarés ; la
partie B avertit qu'un seul de ses blocs de gestion va sur chaque machine.
Sans routeur désigné, l'en-tête signale le risque de duplication au lieu de
laisser croire que le devis est applicable partout.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'ACL d'isolation bloquait l'autre tenant, puis se terminait par
`permit ip <tenant> any`. Ce `any` autorisait 10.27.x -> 10.0.0.0/24 : le
management des switches, celui de Proxmox et l'OOB/IPMI, plus iSCSI et Ceph.
Une VM compromise atteignait la console physique des hyperviseurs.
Le commentaire du générateur disait « Reste -> passerelle OPNsense », ce qui
est faux pour l'underlay : ce trafic est routé LOCALEMENT par le switch et ne
passe jamais par la frontière, donc elle ne le filtre jamais.
Un `deny` par sous-réseau underlay est désormais émis avant le `permit`
final, dérivé de underlay.yml, dialecte respecté (masque normal ou wildcard).
Vérifié qu'aucun flux du registre ne vise l'underlay : rien de déclaré ne
casse. Sans underlay déclaré, l'ACL retrouve sa forme d'avant.
Consigné en §6 : le registre n'a pas de mot-clé `underlay` (un besoin
légitime, superviser l'hyperviseur, ne pourrait pas être déclaré), et le
devis émet un jeu unique de SVI pour trois switches sans MLAG.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deux omissions que le devis faisait porter à l'opérateur.
Le VLAN de transit était étiqueté sur le trunk `<PORT-VERS-PROXMOX>`, et
aucune interface vers le pare-feu n'était émise. Les hyperviseurs n'ont pas
d'interface sur le transit, et le pare-feu n'a rien à faire des VLAN
tenants — il route vers eux, il ne les étiquette pas. Le transit sort du
trunk Proxmox et prend son propre port (section 4b), dérivé de l'underlay.
Les routes de la section 5 déplacent la sortie du switch, y compris celle de
ses propres réponses. Tant que la frontière ne répond pas, elles coupent
l'accès d'administration AU SWITCH LUI-MÊME — le mécanisme qui a rendu une
VM muette le 2026-07-29, appliqué à l'équipement depuis lequel on travaille.
Le devis les crachait à la suite des autres comme si elles étaient
équivalentes ; il énonce maintenant les préalables et l'ordre.
Sans transit déclaré, les deux sections s'affichent en clair comme
manquantes plutôt que de disparaître.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`opnsense_prochain_saut` quitte le panneau Intrants. Il dérive du réseau de
transit de l'underlay — la `passerelle` du réseau portant `passerelle_sortie`.
Un seul bloc alimente les deux devis : 10.0.4.1 est le SVI côté switch ET le
prochain saut des routes tenants côté frontière ; 10.0.4.2 est la route par
défaut du switch. Le saisir en doublon rouvrait la possibilité de deux
valeurs contradictoires pour un seul lien — le mode de panne qu'on venait de
fermer pour les réseaux d'administration.
Le devis frontière expose le bloc `transit` (JSON compris) et cesse de
réclamer en section 0 les routes que `devis-reseau` émet désormais : il dit
lesquelles sont déjà émises, ou signale l'absence de transit déclaré. Sans
underlay, le marqueur revient — le repli reste explicite.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Solde la dette documentaire repérée à l'audit du 2026-07-03. Tous les rôles ont
désormais un README, au format maison (intention → rôle → variables →
notes/limites → prérequis). Ils documentent ce qui ne se lit PAS dans les tâches :
- serveur_debian : rôle-catégorie sans tâches — il ne porte que le flux SSH du
plan de gestion, sans quoi les nftables générés couperaient l'accès Ansible à
toute la flotte ; le vrai travail est dans le playbook de groupe.
- hosts_statiques : le plancher /etc/hosts comme CONDITION de l'ordre de
reconstruction (résolution par nom avant que PowerDNS existe) ; fichier
entièrement géré, alias d'exposition best-effort.
- resoudre_base / resoudre_annuaire : entrées, sorties (facts), et pourquoi le
FQDN plutôt que le nom court (fédération + nom canonique pour verify-full).
- serveur_dovecot : les 3 réglages Dovecot 2.4 qui conditionnent la remise
(static_allow_all_users, mail_inbox_path vide, mail_path par utilisateur) et
le local-part seul comme chemin commun LMTP/IMAP ; limite mono-domaine.
- serveur_postfix : liens mailstore/milter, recopie de /etc/hosts dans le chroot.
- serveur_rspamd : clé DKIM idempotente ; le domaine signé doit être PUBLIC en
prod, sinon la signature ne vaut rien pour les MX distants.
- client_backup / serveur_backup : jobs déclaratifs, chiffrement côté client,
deux pièges (jobs vides = silencieux ; dépôt neuf vide après un from-zero) ;
hors-nœud n'est pas hors-site.
- serveur_oauth2_proxy : le patron réutilisable Keycloak-devant-n'importe-quoi,
et pourquoi allow_unverified_email est nécessaire avec un annuaire LDAP.
- serveur_icingaweb2 : modes ldap vs external, écoute à restreindre en SSO.
- client_unbound : le garde-fou de bascule (apply ET confirm, validation réelle
avant de toucher /etc/resolv.conf).
docs/carte-set-ops.md ne décrivait plus l'état du code :
- expose EST consommé au déploiement (annoncé comme « Phase 3 à venir ») :
expositions_des_applications + expositions.conf.j2, alias /etc/hosts, SANs
d'edge dérivés par instancier.py ;
- 3 mécanismes transverses ajoutés (résolution d'annuaire, plancher de
résolution, resoudre_base nommé dans les bindings app→base) ;
- 5 entrées d'index ajoutées : réseau/pare-feu, ordre de déploiement,
preuve/recette, exploitation courante, wiki — pans du corpus non indexés ;
- ce qui reste ouvert est marqué : meta/liens.yml sur le seul serveur_postfix,
requiert non consommé au déploiement (l'ordre vient des couches + du graphe).
make verifier vert : ansible-lint 514 fichiers, tests, syntax-check,
23 preuves CONFORME / 0 échec / 0 sauté.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La série d'illustrations (commit précédent) rejoint sa page. En fin de section
② de l'unité, une galerie « Les vues, annotées » présente les 8 figures — vues
éditables (Serveurs, Applications, Bases × 2, Domaines) puis dérivées (Flux,
Couches, Réseau) — chacune un SVG auto-contenu avec ses repères + légende.
Mécanique, sans duplication dans l'arbre source :
- symlink wiki/img -> ../docs/img : foyer unique dans docs/img/, refs relatives
(img/*.svg) qui résolvent en local et sur le navigateur de dépôt ;
- make wiki-publier embarque désormais les docs/img/*-annote.svg (déréférencés)
dans le wiki Forgejo publié — jusqu'ici seules les .md voyageaient, donc aucune
image n'aurait rendu une fois publiée.
Preuves : 22 OK / 0 échec / 1 sauté (dont P22 « plan de recette à jour »).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Complète la série d'illustrations commencée avec la vue Serveurs : les 7
autres vues de la console (Applications, Bases × 2 — serveur BD et fiche de
base —, Domaines, Flux, Couches, Réseau), chacune en capture PNG + version
annotée en SVG AUTO-CONTENU (PNG intégré en base64, un seul fichier portable
qui rend dans les .md, le wiki Forgejo, un navigateur).
Même moule que Set-OPS-Serveurs-annote.svg : 8 repères aux couleurs Alliance
Boréale + légende deux colonnes. Points saillants par vue :
- Applications : catalogue des applis, éditeur (rôle/hôte/port/FQDN exposé),
relations déclaratives (requiert/liens/bases), dépendances causales.
- Bases : serveurs SGBD + bases applicatives « nom @ serveur », bases hébergées.
- Bases (fiche) : portée/consommateur/serveur, secret Vault (jamais en clair),
DSN dérivé masqué.
- Domaines : zones DNS publique vs interne, autorité/edge/DNSSEC, expositions.
- Flux : matrice d'audit (source de nftables ET justification), sens/chiffrement.
- Couches : ordre de déploiement en 6 couches (socle → agents), lecture seule.
- Réseau : flotte multi-instances, adressage dérivé du seed, bascule, devis switches.
La série des 8 vues est désormais complète — prête à intégrer aux .md / au wiki.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le modele derive l'adressage par tenant (VLAN 1000+index*10+zone), mais la
fabric physique qui porte la flotte (mgmt switches/Proxmox/OOB, iSCSI, Ceph
public+cluster) n'appartient a aucun tenant. Elle est desormais codifiee.
- scripts/underlay.py + make underlay : charge/affiche/valide underlay.yml
(VLAN < 1000, sous-reseaux hors des supernets tenant 10.(10+index).0.0/16).
- underlay.yml gitignore (comme le vault) ; gabarit public underlay.yml.example ;
surchargeable par SETOPS_UNDERLAY.
- devis_reseau : section 0. Underlay + VLAN underlay sur le trunk, selon dialecte.
- P23 : underlay.py --verifier ; sautee si underlay.yml absent (comme P16 sans vault).
23/23 preuves. Doc : docs/audit/README.md, wiki page reseau, CHANGELOG.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le switch Binardat differe de Cisco sur deux points que le devis ignorait :
- masque d'ACL normal (255.255.0.0) au lieu du wildcard inverse (0.0.255.255) ;
- pas de commande 'remark' (commentaires d'ACL).
devis_reseau.py gagne un dialecte (defaut cisco, option binardat) : masque_acl()
et remarque() adaptent la sortie. Reglable par --dialecte, SETOPS_DIALECTE, ou
make devis-reseau DIALECTE=binardat. Le GUI suit l'env. Public reste generique.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Les 78 exercices « À toi de jouer » du wiki forment un plan de tests d'acceptation.
Formalisé sans dupliquer :
- scripts/plan_recette.py + make plan-recette : GÉNÈRE docs/audit/plan-de-recette.md
depuis les exercices du wiki. Grille auto-contenue par unité, colonnes : ce qu'on
éprouve · le geste · type (observe/casse-répare) · Preuve auto (le Pxx extrait du
texte -> quels gestes manuels sont AUSSI gardés par la machine). Générée -> ne peut
pas dériver du wiki.
- Preuve P22 : plan_recette.py --verifier échoue si le fichier committé est périmé.
Le plan de recette devient auto-gardé.
- Honnêteté de couverture assumée : « — » = manuel seul ; pas d'exhaustivité au-delà
des exercices du wiki.
Pendant humain de make prouver (le harnais prouve le moteur P01-P21, la recette valide
l'exploitation) ; checklist du protocole-operateur-independant (« exploitable sans IA »).
Validé : 78 gestes / 19 unités, 5 doublés d'un Pxx ; P22 détecte une dérive (testé) ;
make verifier -> CONFORME 22/22 (instance cohérente).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cinquième page de la vague méthode. Au moule à 4 temps : éditer la SOURCE (le
plan) pas l'artefact, dry-run avant apply, et l'invalide rendu impossible à
saisir (le <select> « Hôte » ne propose que des hôtes réels -> pas d'hôte
fantôme, garde P19). Exercices ancrés dans le réel (dont réparer un hôte
fantôme via le select). Raccordée dans _Sidebar (Socle & méthode).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le wiki enseignait les fondamentaux SERVICES mais pas la MÉTHODE. Quatre pages
au moule à 4 temps (concept -> Set-OPS -> transférable -> à toi de jouer), avec
exercices concrets :
- Le plan & l'adressage dérivé (un seed, tout en découle).
- Multi-instance & fédération (un moteur, N écosystèmes ; découverte par
convention, garde-fou P21).
- La preuve (ne jamais affirmer plus que ce qu'on prouve ; make prouver P01-P21).
- Glossaire (24 concepts ; était « à venir »).
Raccordées dans Home.md et _Sidebar.md (section « Flotte & preuve »). Le wiki
pointe vers docs/, ne recopie pas. 855 -> 1573 lignes, 22 pages, aucun lien mort.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Symétrique d'instance-creer, produit un modèle dans le dépôt PRIVÉ
(Set-OPS-Modeles/, jamais exemples/modeles/). scripts/model_creer.py, 2 modes :
- MODE=base : copie un modèle générique.
- MODE=instance : PROMEUT une instance en modèle (généralise identité ->
exemple.*, index -> 1, production -> false, nftables_admin_ssh -> [], cle de
sauvegarde videe, proxmox.yml generique [D1], inventaire principal [D2]).
Surete : aucun secret ne sort (vault.yml/proxmox.vault.yml jamais copies ; refus
si l'un subsiste ; .example conserve). Copie CIBLEE (plan/ + inventories/ seulement,
symlinks resolus) -> robuste aux depots imbriques et boucles de symlinks. Le modele
produit doit valider (modeles.py verifier), sinon annule.
Teste en isolement (depot prive jamais touche) : promotion du labo reussie +
validee (aucun chezlepro residuel, aucun secret, proxmox generique) ; copie socle
OK ; refus d'une instance INVALIDE (garde-fou : hote fantome detecte).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le moteur reste independant des instances ET des modeles assembles (prives) :
il ne contient que le modele public socle ; les modeles assembles sont
DECOUVERTS au runtime via SETOPS_MODELES (depot prive de l'operateur), jamais
embarques dans le code public.
- scripts/instance_creer.py : copie un modele (exemples/modeles/* + SETOPS_MODELES)
vers un depot frere ../<nom>, y fixe l'index, et refuse un nom existant, un
modele inconnu, ou un index en collision avec une instance federee (verifie
AVANT toute copie). .git et hosts.genere.yml non copies.
- make instance-creer NOM=.. MODELE=.. [INDEX=N] ; make instance-modeles.
- GUI (vue Reseau) : formulaire « Creer une instance depuis un modele » sous la
flotte. POST /api/instance-creer ; /api/instances renvoie aussi modeles +
index_pris. Ne bascule pas l'active.
Corrige : gabarit de voute du labo complete (P18 a echoue en passant l'active
sur le labo, dont le vault.yml.example etait reste a 17 cles ; aligne sur 23).
Valide : garde-fous de creation testes en isolement (collision refusee avant
copie, ecrasement refuse, modele inconnu refuse, aucune pollution) ; node --check ;
make verifier rc=0 CONFORME 21/21 (active = labo).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Première figure d'illustration de la doc : capture de la vue Serveurs du GUI
(docs/img/Set-OPS-Serveurs.png) + sa version annotée en SVG AUTO-CONTENU
(docs/img/Set-OPS-Serveurs-annote.svg) — le PNG y est intégré en base64, un
seul fichier portable (rend dans les .md, le wiki Forgejo, un navigateur).
8 repères aux couleurs Alliance Boréale + légende : inventaire actif & flotte,
onglets/registres, « Appliquer le plan », carte serveur dérivée, tuiles
VMID/IP/VLAN dérivées du seed, formulaire IDENTITE (édition du plan),
intégrations client_*, dépendances causales (garde-fou requiert/manquant).
À intégrer aux .md quand la série de captures sera complète.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Demande explicite et repetee de l'operateur : gerer les instances DEPUIS le GUI.
- Inventaire resolu DYNAMIQUEMENT : le serveur figeait l'inventaire au demarrage
(args.inventaire.resolve()). Il est desormais relu depuis le symlink instance/
a chaque requete (propriete Gestionnaire.inventaire) -> la bascule prend effet
sans redemarrer. Les FICHIER_* de plan suivaient deja le lien. SETOPS_INVENTAIRE
force encore un inventaire fixe (CI).
- POST /api/instance-utiliser + basculer_instance(nom) : repointe le symlink avec
les garde-fous de make instance-utiliser + validation STRICTE (nom doit etre une
instance decouverte -> pas de traversee de chemin ; ../etc refuse, teste).
- Vue Reseau : bouton « Activer » par instance dans la table de flotte. Confirme
(avertissement renforce si production), bascule, recharge -> toutes les vues et
les deploiements visent la nouvelle instance.
L'edition de federe reste au CLI (rarement change) ; la bascule est CLI ET GUI.
docs/carte-set-ops.md : la ligne Multi-instance du tableau des mecanismes documente
la decouverte (glob des dossiers freres, aucun registre) et le garde-fou P21.
Valide : node --check ; bascule testee de bout en bout (repoint -> inventaire
dynamique suit -> traversee refusee -> restauration) ; make verifier rc=0
CONFORME 21/21.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GUI (vue Réseau) :
- nouvelle vue FLOTTE (endpoint /api/instances) : liste les instances, marque
l'active (star), montre index / plage VLAN / statut federe-local / prod, et
affiche une banniere de COLLISION d'index — le pendant visuel de make instances
et de la preuve P21.
- le devis n'affiche plus que les instances FEDEREES (coherent avec le filtre
federe ; le labo local en est ecarte).
- retire les deux mentions perimees de « ip-miroir » (concept supprime a la
rupture ; la decouverte se fait desormais sur `index`).
La bascule d'instance et l'edition de `federe` restent au CLI (chirurgie de
symlink / drapeau rarement change) : plan de controle gele.
docs/multi-instances.md :
- section « Gerer la flotte » (make instances, bascule, les deux reflexes).
- section « Comment le moteur decouvre les instances » : convention de dossiers
freres (glob ../*/plan/nomenclature.yml), pas de registre.
- « Adressage federe » reecrit : il mentait encore (VMID compact 21101/1CSNN,
sandbox « sans index », plafond 9). Remplace par le modele derive du seed
(tableau des formules, VMID ip-miroir, drapeau federe: false, plafond reel 245).
- corrige les mentions residuelles de `supernet` a saisir (c'est `index`).
Valide : node --check du GUI, make verifier rc=0 CONFORME 21/21.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
La bascule d'instance existait deja (make instance-utiliser / instance-courante,
repointage du symlink). Manquaient la vue d'ensemble et le filet de securite.
- make instances (scripts/instances.py) : liste les instances de la federation
(depots freres avec plan/nomenclature.yml), marque l'active (*), montre index,
plage VLAN derivee, statut federe/local et production. Lecture seule.
- Detection de collision d'index entre instances FEDEREES (memes VLAN/VMID sur le
trunk) : le piege exact vecu (prod + labo tous deux a l'index 1) est desormais
crie. Mode --verifier -> rc=2 sur collision.
- Preuve P21 : cable ce garde-fou dans make prouver / make verifier. No-op quand
moins de deux instances federees sont presentes (comme P17 sans SETOPS_MODELES).
Valide : make instances liste les 3 instances (labo LOCAL, Technolibre + Chezlepro
federees, index 2 et 13) ; detection testee en synthetique (deux federees meme
index -> collision ; labo federe:false -> coherent) ; make verifier rc=0
CONFORME 21/21.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deux corrections, revelees quand Chezlepro a eu deux instances (labo index 1 +
prod index 13) decouvertes par le devis :
1. Collision de noms. prefixe() reduisait OPS-Chezlepro ET OPS-Chezlepro-lab au
meme "CHEZ" (retrait de -lab + troncature 4 lettres) -> deux VLAN "CHEZ-Frontiere"
et surtout deux ACL "CHEZ-ISOLATION", que le switch aurait FUSIONNEES (config
cassee). Les noms sont desormais qualifies par l'index (CHEZ1-*, CHEZ13-*,
TECH2-*) : uniques, et le tenant + son index se lisent dans le nom. Les numeros
de VLAN encodaient deja l'index ; les noms le font maintenant aussi.
2. Drapeau federe. decouvrir() saute toute nomenclature avec `federe: false`
(defaut true). Permet d'exclure un bac a sable LOCAL (labo, sans remote, pas sur
la fabric de production) : on ne provisionne pas ses VLAN sur les switches de
prod. Le labo genere toujours son propre inventaire.
Verifie : make devis-reseau -> labo (1011-1016) exclu, restent Technolibre
(1021-1026) et Chezlepro prod (1131-1136) ; SVI/ACL coherents (access-group ==
nom d'ACL) ; make verifier rc=0 CONFORME 20/20.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Principe : les valeurs de configuration se derivent des intrants, elles ne se
reecrivent pas a la main. La nomenclature dupliquait ce qu'index determine deja
(supernet, sous-reseaux, passerelles, VLAN). Corrige en rupture nette.
- inventory_rules : source unique de derivation — supernet_de, base3_de,
sous_reseau_de, passerelle_de, vlan_de. Modele 6 zones encode une fois
(2e octet = 10+index, 3e octet zone = 15+categorie, VLAN = 1000+index*10+zone).
deriver_nomenclature ne lit plus aucun adressage stocke ; mode compact supprime.
- devis_reseau : importe ces helpers (fin de la duplication) ; decouvre les
tenants sur `index` present (filtre vmid_schema retire).
- GUI : `index` devient un INTRANT (section Reseau). Il vit dans la nomenclature
(plan reseau uniforme, contrairement aux intrants des modeles heterogenes) et
le GUI l'ecrit chirurgicalement (une ligne, sans reformater). Le miroir JS
derive le VLAN du seed (fin de la lecture de c.vlan stocke).
- socle public : nomenclature au format maigre.
Preuve P20 (preuve_nomenclature_derivee) : aucune nomenclature ne stocke
d'adressage — garde-fou permanent, teste en negatif.
Valide : DIFF VIDE sur les 3 instances (la derivation reproduit exactement
l'adressage stocke), 7 modeles valident, devis_reseau genere les memes VLAN
(1011-1016 derives), make verifier rc=0 CONFORME 20/20, node --check du GUI OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le harnais ne verifiait qu'UNE instance et le seul modele socle. Tout ce qui vit
a cote du moteur echappait au controle. Trois preuves ferment ces angles morts :
- P17 (scripts/modeles.py) : TOUS les modeles valident, pas seulement socle.
SETOPS_MODELES=../Set-OPS-Modeles inclut les modeles assembles prives. A trouve
6 modeles invalides sur 7 (corriges dans Set-OPS-Modeles).
- P18 (scripts/voute.py) : le gabarit vault.yml.example couvre EXACTEMENT les secrets
que le plan exige (bases + roles actifs + group_vars). Ne dechiffre jamais la vraie
voute : compare des noms.
- P19 (scripts/couverture_gui.py) : tout champ present dans un plan reel est editable
par le GUI. A trouve applications.websocket (comble). Nomenclature toleree (trou connu).
GUI :
- champ « Liens (bindings) » dans l'inspecteur d'application : role -> cible en listes
deroulantes, les roles proposes = ceux que le role porteur accepte (meta/liens.yml).
Comble un manque : les bindings ne se declaraient qu'en editant le YAML a la main.
- champ « WebSocket » (Collabora).
- CHAMPS_ECRITS_PAR_GUI : declaration de ce que le GUI sait ecrire, verifiee par P19.
Garde-fou de fond : valider_applications refuse une application posee sur un hote non
declare (l'hote fantome exact qu'integral portait). Cable partout + POST du GUI.
liens_acceptes()/catalogue_liens() dans inventory_rules : source unique partagee par le
validateur, le GUI et instancier.py (dont la copie locale est retiree).
Valide : make verifier rc=0, CONFORME 19/19, ansible-lint 0 echec, 7 modeles valident,
DIFF VIDE, node --check du GUI OK. Piece justificative : docs/audit/preuve-2026-07-22.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Palette (promo/, les 4 pages) — les teintes rose, verte et violette etaient
trop presentes une fois etalees au tour precedent. Reduction d'environ 40 % :
rose .075 -> .044, vert .058 -> .034, violet .062 -> .036, arrets
intermediaires abaisses dans la meme proportion, opacite de la nappe .aurora
.44 -> .32. Geometrie inchangee : meme largeur, meme position, seule
l'intensite baisse. 0 conflit CSS entre les quatre pages.
forge-01 — la note precedente presentait comme une contradiction a trancher
le fait que le plan dise `etat: planifie` et que le CHANGELOG du 2026-07-03
dise le branding « prouve sur forge-01 ». Ce n'en etait pas une : l'hote a ete
cree, puis supprime. Le plan decrit l'etat COURANT, le CHANGELOG un etat PASSE
— les deux disent vrai. La preuve reste valable, simplement non rejouable tant
que l'hote n'est pas recree. roles/serveur_forgejo/README.md corrige.
Valide : equilibre accolades/parentheses du CSS, 0 conflit CSS,
prouver.py --verifier -> CONFORME 16/16.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Palette (promo/, les 4 pages) — ajoute --rose:#ff6fc4 (frange magenta) et
--vert:#5cff9d (vert fluo), uniquement dans les DÉGRADÉS FONCÉS : fond fixe du
corps, nappe .aurora dérivante, filets .rule. Opacités de 5,5 % à 8,5 %, une
teinte et non un motif. Le vert entre par la gauche et le rose sort par la
droite, comme une vraie aurore. --aurora (texte et boutons) est inchangé :
l'identité de marque ne bouge pas. Appliqué identiquement aux quatre fichiers,
0 conflit CSS après coup.
Thème Forgejo (alliance.css, 29 → 118 lignes) — la feuille étant injectée par
templates/custom/header.tmpl sur toutes les pages, le wiki est couvert sans
feuille distincte. Réorganisée en deux sections de risque explicite :
§1 Variables — couleurs officielles + ciel nocturne. Sûr, résiste aux
mises à jour. ÉPROUVÉ sur forge-01 (CHANGELOG 2026-07-03).
§2 Décor — fond aurore, filet sous les titres, citations, tableaux et code
en ligne de .markup. Fragile (classes internes). JAMAIS RENDU.
Supprimer §2 ramène au thème sobre. Le ciel nocturne ne s'applique qu'aux
thèmes sombres, pour ne pas casser le thème clair.
docs/theme-forgejo-hors-flotte.md — pose manuelle sur une instance Forgejo non
gérée par Set-OPS (la forge historique qui héberge ce dépôt et son wiki).
Forgejo n'ayant aucun réglage web pour le CSS, il faut déposer la feuille sur
le serveur. La procédure ne duplique aucun fichier : elle pointe vers ceux du
rôle. Inclut la détection du répertoire custom, un garde-fou pour ne pas
écraser un header.tmpl existant (ajout de ligne), la vérification curl et la
marche arrière.
Corrige deux affirmations fausses de ma part :
- « thème jamais rendu par un Forgejo réel » était faux pour §1, éprouvée.
Le statut est désormais donné section par section.
- Contradiction consignée sur forge-01 : le plan la dit `etat: planifie`, le
CHANGELOG 2026-07-03 dit le branding « prouvé sur forge-01 ». Les deux ne
peuvent pas être vrais ; l'écart est écrit dans le README du rôle, à trancher.
Validé : équilibre accolades/parenthèses du CSS, 0 conflit CSS entre les quatre
pages, lien de doc résolu, ansible-lint 0 échec sur 485 fichiers,
prouver.py --verifier → CONFORME 16/16.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Les pages ayant été écrites l'une après l'autre, six dérives s'étaient
installées entre elles. Passe de normalisation :
- Adresse au lecteur : index.html tutoyait (17 occurrences), les trois autres
vouvoyaient. Vouvoiement partout — ce sont des pages commerciales.
- Marque : trois pages affichaient « Alliance Boréale », tarifs affichait
« Chezlepro · tarifs ». Désormais « Set-OPS · Chezlepro » dans la barre et
« Chezlepro inc. · Saint-Bruno-de-Montarville, Québec » au pied.
- Navigation : quatre barres différentes (libellés et ordres distincts) →
Capacités · Offres · Tarifs · Valeur + « Nous parler », identique partout,
page courante marquée aria-current="page".
- Pieds de page : quatre structures → une seule, licence AGPLv3 incluse.
- Vocabulaire d'état : index utilisait .tag proven/soon, les autres
.st ok/wip/road. Un seul système : Éprouvé · En chantier · Feuille de route.
- CSS : 35 sélecteurs partagés avaient des déclarations divergentes → 0 conflit.
Deux étaient de vrais conflits de mise en page (.beats 4 colonnes contre 3,
.stats grille contre flex) ; passés en repeat(auto-fit,minmax(...)), une seule
règle qui s'adapte au nombre d'éléments.
Ajoute une section « Nos réserves » à index.html — seule page à en manquer,
alors que c'est celle qui affirme le plus de capacités. Les quatre assument
désormais leurs limites (mainteneur unique, épreuve de l'opérateur indépendant).
promo/README.md consigne les conventions communes en tableau et fournit le
script de détection des conflits CSS à lancer avant commit.
Validé : liens, balises, identifiants, équilibre CSS et node --check du JS sur
les quatre pages ; 0 conflit CSS ; prouver.py --verifier → CONFORME 16/16.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
promo/offres.html — les écosystèmes livrables, nommés par persona (Socle,
Identité, Maison OBNL, Cabinet, Atelier, Vigie, Intégral, Présence Web). Le
périmètre annoncé est tiré des modèles réels : le nombre de machines est celui
du plan, pas une estimation. Les états sont honnêtes — collaboration marquée
« en chantier », presence-web marquée « feuille de route, non vendable ».
promo/tarifs.html — grille tarifaire de Chezlepro inc., alignée sur les six
offres du site chezlepro.ca (diagnostic, socle opérateur, tenant portable,
audit-ready ops, exploitation gérée, observabilité) plutôt que sur les noms de
modèles Set-OPS. Diagnostic dès 4 500 $ et déductible, socle dès 18 000 $,
tenant dès 12 000 $ (7 500 $ les suivants), parcours combiné dès 38 000 $ au
lieu de 42 000 $, exploitation à 750/1 500/2 800 $ par mois. Trois postes,
aucune facturation par siège, licences à 0 $.
Montants = estimations de marché (taux effectif ~1 000-1 100 $/jour, firme
spécialisée québécoise), pas des prix validés par une vente réelle. Dollars
canadiens avant taxes. Le README le consigne, ainsi que le fait que cette
grille est publique et versionnée.
Corrige au passage une contradiction introduite : offres.html affirmait « un
prix affiché serait un prix inventé » ; son tableau porte désormais les prix de
départ et renvoie à tarifs.html, source de vérité.
Validé : node --check du JS des 4 pages, balises équilibrées, liens internes et
croisés vérifiés, prouver.py --verifier → CONFORME 16/16.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Page sœur de promo/index.html, même thème Alliance Boréale, liée dans les deux
sens. index.html s'adresse au sysadmin (« que puis-je faire ? ») ; valeur.html
s'adresse à la personne qui signe (« qu'est-ce que ça rapporte, et comment le
vérifier ? »).
Quatre pièces :
- calculateur de coût total interactif (personnes, abonnements, matériel, mise
en place, exploitation) sur 5 ans, avec point de bascule calculé ;
- tableau des équivalences : 11 briques, ce qu'elles remplacent, le libre qui
le fait, et leur état réel (Nextcloud/Collabora marqué « en chantier ») ;
- la preuve : make prouver 16/16, registre de 54 affirmations, et le fait que
le registre a le droit de perdre ;
- « Ce que nous ne prétendons pas » : seuil de rentabilité réel, besoin d'une
personne responsable, mainteneur unique, épreuve de l'opérateur indépendant
à venir — y compris si elle échoue.
Le calculateur affiche honnêtement le cas défavorable : sous ~40 personnes aux
valeurs par défaut, l'autohébergement ne se rembourse pas sur l'horizon retenu,
et la page le dit plutôt que de le cacher. Les valeurs par défaut sont des
ordres de grandeur à remplacer, pas des tarifs.
Validé : node --check du JS (rc=0), balises équilibrées, liens internes
résolus, prouver.py --verifier → CONFORME 16/16.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AFF-002 (« Set-OPS s'exploite sans aucune IA ») était ✅ par inspection : ses
écarts bloquants soldés et le parcours relu, mais jamais exécutée par un
opérateur qui n'est pas l'auteur. Aucune commande locale ne peut la prouver.
Ajoute le protocole de l'épreuve humaine : départ à froid depuis le modèle
public `socle`, sur la grappe Proxmox de l'opérateur. Règle du silence
(N0 journalise / N1 déblocage après 30 min, consigné comme défaut / N2 arrêt
sécurité), interdits (aucune IA, aucun dépôt privé, aucune lecture de
docs/audit/ qui divulguerait les pièges connus), critères R1→R6 fixés
d'avance, périmètre matériel, gabarit de rapport.
Le registre peut perdre : la redescente d'AFF-002 en 🟡 ou ❌ selon le verdict
est explicitement prévue.
Le protocole ne nomme personne — il est réutilisable, et l'identité de
l'opérateur vit dans son rapport, sous réserve de consentement.
Validé : prouver.py --verifier → CONFORME 16/16. Aucun code touché.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Page web autonome (un seul fichier, zéro dépendance externe) présentant tout ce
qu'un sysadmin peut faire avec Set-OPS, sous l'identité Alliance Boréale (ciel
nocturne + aurore + constellation, reprise des thèmes Keycloak/Forgejo).
- 7 piliers (identité, PKI, DNS/réseau, données, courriel, edge TLS, observabilité)
+ socle transverse (durcissement, pare-feu, sauvegardes, plan/GUI) + forge.
- Console opérateur, cycle de vie VM, section preuve/souveraineté, feuille de route.
- Honnêteté prouvable : « éprouvé » vs « feuille de route » clairement distingués.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- prouver.py : nouveau mode --verifier — exécute toutes les preuves du registre
(verdict + code de sortie) sans écrire de rapport, pour ne pas écraser la pièce
justificative committée docs/audit/preuve-<date>.md.
- Makefile : make verifier se termine par `python3 scripts/prouver.py --verifier`
→ verifier échoue si une preuve échoue. make prouver seul écrit toujours le rapport.
- docs/audit/README.md : section « Rapport avec make verifier » mise à jour.
Vérifié : make verifier → CONFORME 16/16 (voûte), aucun churn du rapport ; make
prouver écrit toujours ; ansible-lint 0 failure.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- prouver.py : la preuve P16 (ansible-inventory --list) devient une fonction qui
parse le JSON et rapporte « N hôtes, M groupes » au lieu de la dernière ligne
brute (`}`).
- docs/audit/preuve-2026-07-20.md : régénéré voûte exportée — 16 OK, 0 échec,
0 sautée (P16 inclus : 14 hôtes, 29 groupes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le dépôt fait / explique / prouve ce qu'il affirme, vérifiable en une commande.
- Phase 1 : docs/audit/affirmations.md — 54 affirmations publiques tracées vers
une commande de preuve et un statut (✅/🟡/❌/⚪).
- Phase 2 : CLAUDE.md réduit à un pointeur mince ; contradiction SSH levée (le
code applique déjà PasswordAuthentication no + AuthenticationMethods publickey,
conforme à AGENTS.md) ; section AGENTS « Codex » → « agents IA ».
- Phase 3 : parcours démarrage réparé (QUICKSTART renvoyait à un modèle absent,
chemins de voûte faux, commandes make périmées) ; make verifier vert
(ansible-lint 33 → 0 : site.yml généré nommé, pipefail, name[template]) ;
voûte Proxmox unifiée lue par le clonage (all/vault.yml).
- Phase 4 : make prouver → docs/audit/preuve-<date>.md, harnais rejouable qui
rappelle l'outillage existant (aucune validation réimplémentée).
- Phase 5 : parcours QUICKSTART prouvé hors-ligne sur le socle ; modèle socle
rendu valide (autorite interne → auto-heberge) ; split-brain d'inventaire
corrigé (repli sur le répertoire existant, pas principal/).
make prouver : 15 OK, 0 échec, 1 sautée (voûte). ansible-lint : 0 failure.
Écarts découverts en cours de traitement (AFF-097..100) : tous résolus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Nouvel onglet lecture seule affichant le devis make devis-reseau (VLANs+SVIs+ACLs)
via GET /api/devis-reseau, avec bouton Copier. node --check OK, endpoint eprouve
(2 tenants, 121 lignes). Un operateur genere et transmet la config sans CLI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
scripts/devis_reseau.py decouvre les instances federees (ip-miroir) et emet
VLANs + SVIs + ACLs d'isolation inter-tenant, derives des nomenclatures.
Plus jamais saisi a la main. Eprouve sur Chezlepro + Technolibre.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Quand une action échoue, un encadré résume les tâches en erreur (étape ·
hôte · type · message clair), avec un bouton Copier pour transmettre à un
humain — au lieu d'obliger à lire le dump Ansible.
Backend _extraire_echec : parse fatal:/UNREACHABLE!, privilégie stderr sur le
générique 'non-zero return code', gère no_log (secret masqué) et injoignables.
Marqueur __RESUME__ streamé, rendu côté JS. Éprouvé sur les 4 échecs réels du
from-zero du jour. node --check OK, smoke serveur OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>