2026-06-24 20:17:46 -04:00
#!/usr/bin/env python3
""" Interface web locale pour gerer un inventaire Set-OPS. """
from __future__ import annotations
from http . server import BaseHTTPRequestHandler , ThreadingHTTPServer
import argparse
import json
import os
import re
import secrets
2026-07-01 17:39:44 -04:00
import socket
2026-06-24 20:17:46 -04:00
import subprocess
import tempfile
2026-07-07 12:49:17 -04:00
import time
2026-06-24 20:17:46 -04:00
import threading
2026-07-01 17:39:44 -04:00
from concurrent . futures import ThreadPoolExecutor
2026-06-24 20:17:46 -04:00
from pathlib import Path
from urllib . parse import urlparse
import yaml
2026-07-07 15:57:47 -04:00
import devis_reseau
resolution d'instance : une seule, partagee — au lieu de neuf copies
Cinq jours, cinq defauts, tous de la meme famille : « quelle instance, quel inventaire ? »
Neuf modules portaient chacun leur reponse.
- 18 aout : P03 comparait chaque instance a l'inventaire d'une AUTRE ;
- 19 aout : verifier_ports codait `principal/` en dur ; verifier_intrants et
_frontiere_absente lisaient le symlink au lieu de la variable ;
- 20 aout : devis_placement rendait un verdict juste sur le mauvais tenant ;
- 22 aout : P35, puis P36 — la dixieme, trouvee par la preuve elle-meme.
Aucune n'etait une faute d'inattention : chacune avait ete ecrite de bonne foi, a un
moment ou le besoin semblait local. C'est le mode de panne de la duplication — pas
l'erreur, mais la DERIVE, invisible depuis l'interieur d'un fichier.
LA RESOLUTION UNIQUE. `inventory_rules` porte instance_courante(), inventaire_de(),
dossier_inventaire() et plan_de(). Trois niveaux de repli, dont le TROISIEME manquait a la
moitie des copies : un hosts.yml existant, puis un REPERTOIRE existant (instance neuve —
c'est ce qui faisait echouer `make instancier` sur le modele public), puis le defaut.
Vingt-huit modules y sont branches.
CE QUI REND CE REFACTOR SUR : avant de toucher quoi que ce soit, chaque module a ete
interroge sur ce qu'il resolvait, pour les DEUX ecosystemes. Apres refactor, meme mesure :
17 modules x 2 instances, diff VIDE. Aucune resolution n'a change — prouve, pas suppose.
P41 echoue des qu'un module reintroduit une copie. Eprouvee en negatif : une copie
replacee dans genome.py est signalee avec son numero de ligne. Trois exemptions nommees :
instances.py et inventory_gui.py manipulent le SYMLINK lui-meme (bascule d'instance), et
devis_opnsense lit deliberement quelle instance est ACTIVE. Elles parlent du lien, pas de
la resolution.
make verifier 41 OK, 0 echec, 0 saute ; make ci idem ; lint vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:19:55 -04:00
from inventory_rules import instance_courante # noqa: E402
2026-06-24 20:17:46 -04:00
from inventory_rules import (
plan : l ecriture des registres devient atomique — tout, ou rien
`path.open("w")` TRONQUE avant d ecrire : entre les deux, le fichier est vide.
Une exception dans yaml.safe_dump, un disque plein, un Ctrl-C, et
instance/plan/serveurs.yml reste mutile.
L asymetrie fait la gravite : hosts.yml se regenere d un
make instancier-appliquer, le PLAN ne se regenere de rien. C est la source
unique de verite. Git est le filet, mais encore faut-il savoir qu on est tombe.
TREIZE SITES, UNE SEULE FONCTION
Le defaut n etait pas dans le GUI seul : douze sites dans sept fichiers, dont
les miroirs CLI des MEMES registres. Corriger le GUI seul aurait recree la
divergence que P41 garde depuis les neuf resolutions d instance. La fonction
vit donc dans inventory_rules.py, que les sept importaient deja. Une source,
pas douze.
TROIS DETAILS QUI FONT LA DIFFERENCE ENTRE « CA MARCHE » ET « CA TIENT »
temporaire dans le MEME dossier os.replace n est atomique qu au sein d un
meme systeme de fichiers ; un /tmp sur une
autre partition casserait la garantie sans
rien dire
fsync AVANT le rename sinon le renommage peut atteindre le disque
avant le contenu : au retour d une coupure
brutale, un fichier neuf et VIDE — le defaut
qu on ferme, deplace d un cran
report des droits mkstemp cree en 0600, le plan est en 0664 et
doit rester lisible par le groupe sur les
runners
LE TEST PORTE SON PROPRE CONTROLE NEGATIF
scripts/tests/test_ecriture_atomique.py rejoue D ABORD l ancienne forme et
verifie qu elle DETRUIT. Sans ce controle, « le fichier est intact » ne
prouverait rien — il pourrait l etre parce que rien n a ete ecrit du tout. Une
garantie qu on n a jamais vue echouer n est pas une garantie, c est une
habitude.
Branche sur P02, dont le titre annoncait « inventory_host » alors qu il lance
maintenant trois tests. Corrige au passage.
LA VOUTE DU GUI : VERIFIEE, PAS DE DEFAUT
Le soupcon etait qu executer_flux pose ANSIBLE_VAULT_PASSWORD_FILE (un seul mot
de passe) alors que creer une VM ouvre DEUX voutes depuis « une voute, une cle ».
Eprouve contre deux voutes JETABLES a mots de passe distincts — jamais les
vraies. Les deux variables se CUMULENT : Ansible essaie tous les secrets, et un
PASSWORD_FILE errone n empeche rien. Confirme en sondant l environnement qu une
recette make recoit reellement : le mot de passe saisi ET les cinq cles
calculees par voutes.py.
Ce qui sauve ce chemin n est donc pas le mot de passe saisi, c est
l IDENTITY_LIST que make pose par-dessus. Chacun couvre ce que l autre ne
couvre pas — le PASSWORD_FILE sert le runner qui n a que sa cle, l IDENTITY_LIST
le poste qui les a toutes. Ecrit au-dessus du code, pour que personne ne
« simplifie » en retirant l un des deux.
make prouver : CONFORME, 59 OK, 0 echec, 1 saute.
make instancier : DIFF VIDE, quatre registres relus, droits 664 preserves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 14:57:57 -04:00
ecriture_atomique ,
2026-06-24 20:17:46 -04:00
GROUPE_HOTES_ACTIFS ,
GROUPE_HOTES_PLANIFIES ,
GROUPES_ETAT_HOTE ,
chaine_connexion ,
charger_applications ,
charger_bases_donnees ,
charger_dependances ,
charger_domaines ,
charger_nomenclature ,
charger_serveurs ,
est_groupe_operationnel ,
groupes_operationnels_connus ,
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
integrations_universelles ,
2026-06-24 20:17:46 -04:00
reconcilier_serveur ,
valider_applications ,
2026-07-22 21:32:42 -04:00
catalogue_liens ,
2026-06-24 20:17:46 -04:00
valider_bases ,
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
valider_domaines ,
2026-06-24 20:17:46 -04:00
valider_serveurs ,
)
RACINE = Path ( __file__ ) . resolve ( ) . parents [ 1 ]
resolution d'instance : une seule, partagee — au lieu de neuf copies
Cinq jours, cinq defauts, tous de la meme famille : « quelle instance, quel inventaire ? »
Neuf modules portaient chacun leur reponse.
- 18 aout : P03 comparait chaque instance a l'inventaire d'une AUTRE ;
- 19 aout : verifier_ports codait `principal/` en dur ; verifier_intrants et
_frontiere_absente lisaient le symlink au lieu de la variable ;
- 20 aout : devis_placement rendait un verdict juste sur le mauvais tenant ;
- 22 aout : P35, puis P36 — la dixieme, trouvee par la preuve elle-meme.
Aucune n'etait une faute d'inattention : chacune avait ete ecrite de bonne foi, a un
moment ou le besoin semblait local. C'est le mode de panne de la duplication — pas
l'erreur, mais la DERIVE, invisible depuis l'interieur d'un fichier.
LA RESOLUTION UNIQUE. `inventory_rules` porte instance_courante(), inventaire_de(),
dossier_inventaire() et plan_de(). Trois niveaux de repli, dont le TROISIEME manquait a la
moitie des copies : un hosts.yml existant, puis un REPERTOIRE existant (instance neuve —
c'est ce qui faisait echouer `make instancier` sur le modele public), puis le defaut.
Vingt-huit modules y sont branches.
CE QUI REND CE REFACTOR SUR : avant de toucher quoi que ce soit, chaque module a ete
interroge sur ce qu'il resolvait, pour les DEUX ecosystemes. Apres refactor, meme mesure :
17 modules x 2 instances, diff VIDE. Aucune resolution n'a change — prouve, pas suppose.
P41 echoue des qu'un module reintroduit une copie. Eprouvee en negatif : une copie
replacee dans genome.py est signalee avec son numero de ligne. Trois exemptions nommees :
instances.py et inventory_gui.py manipulent le SYMLINK lui-meme (bascule d'instance), et
devis_opnsense lit deliberement quelle instance est ACTIVE. Elles parlent du lien, pas de
la resolution.
make verifier 41 OK, 0 echec, 0 saute ; make ci idem ; lint vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:19:55 -04:00
INSTANCE = instance_courante ( )
2026-06-30 16:08:25 -04:00
def _inventaire ( instance : Path , * noms : str ) - > Path :
""" Inventaire de l ' instance, détection rétro-compatible (premier existant). """
for nom in noms :
p = instance / " inventories " / nom / " hosts.yml "
if p . exists ( ) :
return p
Mise en conformité prouvable : registre d'affirmations + make prouver
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>
2026-07-20 19:53:18 -04:00
# Aucun hosts.yml encore généré : viser le répertoire d'inventaire déjà présent
# (ses group_vars y sont), sinon le premier nom par défaut.
for nom in noms :
if ( instance / " inventories " / nom ) . is_dir ( ) :
return instance / " inventories " / nom / " hosts.yml "
2026-06-30 16:08:25 -04:00
return instance / " inventories " / noms [ 0 ] / " hosts.yml "
# Un seul inventaire par instance (modèle « séparation par instance ») ; on accepte
# l'ancien découpage lab/production en rétro-compatibilité. SETOPS_INVENTAIRE force.
_force = os . environ . get ( " SETOPS_INVENTAIRE " )
INVENTAIRE_DEFAUT = Path ( _force ) if _force else _inventaire ( INSTANCE , " principal " , " production " )
INVENTAIRE_PRODUCTION = INVENTAIRE_DEFAUT . resolve ( )
# Inventaire qui porte la config Proxmox / le clonage (modèle) : lab > principal > production.
INVENTAIRE_MODELE = _inventaire ( INSTANCE , " lab " , " principal " , " production " )
2026-06-24 20:17:46 -04:00
DOSSIER_PLAYBOOKS_GROUPES = RACINE / " playbooks/groupes "
FICHIER_DEPENDANCES = RACINE / " docs/dependances-groupes.yml "
FICHIER_NOMENCLATURE = INSTANCE / " plan/nomenclature.yml "
FICHIER_BASES = INSTANCE / " plan/bases-donnees.yml "
FICHIER_APPLICATIONS = INSTANCE / " plan/applications.yml "
FICHIER_DOMAINES = INSTANCE / " plan/domaines.yml "
FICHIER_SERVEURS = INSTANCE / " plan/serveurs.yml "
2026-06-30 10:07:17 -04:00
# Intrants de base (panneau GUI). Cibles d'ecriture possedees par le GUI.
2026-06-30 16:31:42 -04:00
# Identite : group_vars/all/10-intrants.yml de l'inventaire. Selon l'instance c'est
# un fichier reel (modele « par instance ») ou un symlink vers une source partagee
# (ecrire suit le symlink). Proxmox : group_vars de l'inventaire modele. Les SECRETS
# n'y entrent jamais.
INTRANTS_IDENTITE = INVENTAIRE_DEFAUT . parent / " group_vars/all/10-intrants.yml "
2026-06-30 16:08:25 -04:00
INTRANTS_PROXMOX = INVENTAIRE_MODELE . parent / " group_vars/proxmox.yml "
Adressage derive du seul seed index (rupture, mode compact retire)
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>
2026-07-23 02:58:15 -04:00
# Le seed reseau (`index`) vit dans la nomenclature (le plan reseau) — d'ou tout
# l'adressage se derive. Le panneau Intrants le lit/ecrit la, chirurgicalement, sans
# reformater le fichier (cf. _ecrire_index_nomenclature).
frontière nord/sud : devis dérivé, lien de transit et les deux routes
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>
2026-08-01 19:32:04 -04:00
# Frontiere nord/sud (OPNsense) : parametres NON sensibles du pare-feu de bordure.
# La cle/secret d'API n'y entrent JAMAIS — voute uniquement (cf. INTRANTS_CLES_INTERDITES).
underlay : monté par symlink depuis le dépôt de l'hébergeur
Il vivait gitignoré à la racine du moteur : consommé par deux générateurs,
validé par P23, versionné nulle part. La dizaine de modifications de la
journée n'était récupérable d'aucune façon, et un clone frais repartait du
gabarit.
Il appartient à l'hébergeur — ses switches, ses câbles — pas au moteur, qui
est générique, ni à un tenant, qui n'en possède pas. Chezlepro est ici
hébergeur ET tenant, d'où la confusion initiale.
Le moteur le monte par symlink comme il monte le plan par `instance/`. Ce
lien NE SUIT PAS `make instance-utiliser` : la fabric reste celle de
l'hébergeur quel que soit le tenant actif.
Vérifié : les deux devis sortent identiques octet pour octet avant et après,
P23 verte, 24 preuves. Symlink brisé (clone sans dépôt frère) : dégrade
proprement, sections omises au lieu d'échouer. Cas exercé.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:05:12 -04:00
# Fabric physique : `underlay.yml`, monte par symlink a la racine du moteur depuis le
# depot de l'HEBERGEUR (ses switches, ses cables). Distinct de l'instance : il ne suit
# pas `make instance-utiliser`. Imbrique sous `underlay:` et riche en commentaires :
# on l'ecrit chirurgicalement, jamais par un safe_dump qui les effacerait.
GUI : section « Fabric » — l'underlay se règle depuis la console
Tout le modèle d'underlay bâti aujourd'hui s'éditait à la main dans un YAML,
pendant que la doctrine dit qu'un sysadmin doit exploiter l'outil sans IA.
La frontière avait eu sa section ; l'underlay, non.
La section couvre les valeurs plates : switch routeur, dialecte de CLI, mode
et topologie de spanning-tree. Les listes de tables (`reseaux`, `hotes`, donc
les ports) restent hors de portée du panneau — elles demandent une vue
dédiée, comme celle des serveurs.
Le dialecte devient un intrant déclaré : il ne vivait que dans
SETOPS_DIALECTE. C'est une propriété du matériel, donc de la fabric.
Précédence : `--dialecte` > environnement > intrant déclaré > cisco.
Écriture chirurgicale plutôt que safe_dump : underlay.yml porte 23 lignes de
commentaires qui expliquent des décisions d'architecture, et un dump les
aurait effacées comme c'est arrivé à plan/applications.yml. Vérifié : trois
valeurs modifiées, 73 lignes et 23 commentaires avant comme après. Une clef
absente du fichier est refusée plutôt qu'inventée à un endroit arbitraire.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:01:00 -04:00
FICHIER_UNDERLAY = Path ( os . environ . get ( " SETOPS_UNDERLAY " ) or ( RACINE / " underlay.yml " ) )
2026-08-02 17:17:10 -04:00
def _depot_hebergeur ( ) - > Path | None :
""" Depot de l ' HEBERGEUR, derive du symlink `underlay.yml` qui le designe deja.
Un hebergeur sert PLUSIEURS tenants et n ' a qu ' une frontiere : ses intrants ne
peuvent pas vivre chez le tenant actif , sinon basculer sur un invite les ferait
disparaitre du panneau . None si aucun underlay n ' est monte.
"""
if not FICHIER_UNDERLAY . exists ( ) :
return None
return FICHIER_UNDERLAY . resolve ( ) . parent
2026-09-16 14:36:59 -04:00
# --- CE QUE CETTE CONSOLE PEUT, DERIVE DE CE QUE SA MACHINE PORTE --------------------
#
# LE DEFAUT QUI A RENDU CECI NECESSAIRE (2026-09-16). Servie par le runner d'un SITE,
# cette console affichait ZERO serveur, ZERO application, ZERO base — sans une erreur.
# `serveur_ops` RETIRE le lien `instance` sur un runner d'hebergeur (a juste titre : un
# lien perime vers un tenant serait pire), `charger_yaml` rend `{"all": {"children": {}}}`
# sur un fichier absent, et la page dessinait ce vide comme un plan vide.
#
# C'est la forme exacte que P79 garde partout ailleurs : une derivation qui ne trouve rien
# ne se distingue pas d'une derivation qui n'a rien a trouver. Ici elle avait le pire
# visage possible — celui d'un ecosysteme qu'on croit sans machines.
#
# ON DERIVE, ON NE DECLARE PAS. Un `serveur_ops_gui_mode: site|tenant` au plan serait une
# SECONDE liste, et une liste qui suit une autre prend du retard : il suffirait de
# reconfigurer un runner sans y penser. Les deux symlinks, eux, SONT le pouvoir :
#
# instance/ -> je configure CET ecosysteme (j'ai sa voute)
# underlay.yml -> je materialise sur CETTE fabric (j'ai celle du site)
#
# C'est la meme coupure que `roles/serveur_ops` exige deja dans son assertion, et que
# `docs/responsabilites-locataire-hebergeur.md` nomme : calculer, configurer, materialiser.
def contexte ( ) - > dict :
console : un tronc, deux branches — assembler deux fois est ce qui fait deriver
Il y avait deux assembleurs de charge, un par sorte de console. Ils ont derive deux fois le
meme jour : en forme (serveurs valait [] d'un cote et {} de l'autre, et charger() levait),
puis en contenu (cinq registres servis vides alors que le site a SON plan — 9 serveurs,
21 applications, 2 bases, 1 domaine, tous invisibles).
Console assemble, une seule fois, et fixe les clefs et leurs formes. Les branches ne
decident que de ce qui leur appartient : d'ou vient le plan, d'ou vient l'inventaire, quels
pouvoirs elles portent. ConsoleLocataire configure, ConsoleSite materialise et sert
desormais son propre plan, ConsolePoste herite du locataire et sait en plus sur quelle
fabric poser.
Le role et la portee ne se confondent pas : le role est une propriete de la classe, la
portee se calcule depuis les pouvoirs. Un poste prive de la voute du site reste l'atelier du
mainteneur et n'engendre pourtant rien. Le decoupage a montre un trou aussitot : ConsolePoste
heritait du refus d'un locataire — « elle ne sait pas sur quelle fabric poser » — alors qu'il
monte la carte ; ce qui lui manque est la voute, et accuser la mauvaise absence fait chercher
au mauvais endroit.
Le jugement des assistants remonte dans le tronc : il etait ecrit dans la route qui liste ET
dans celle qui execute, et celle qui se trompe est toujours celle qui execute.
Valide : make test a 0 echec, 8 tests de rendu sous node dont deux neufs, console lancee pour
de vrai (13 serveurs, 24 applications, 17 runbooks, 127 etapes, 0 ecart). make verifier a
aussi attrape une faute que j'avais laissee passer sur depots_perimes.yml — risky-shell-pipe,
corrige. P02 et P60 restent, et P60 demande une republication du wiki.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:55:49 -04:00
""" La portee de cette console — DELEGUEE a la classe qui la sert.
2026-09-16 14:36:59 -04:00
console : un tronc, deux branches — assembler deux fois est ce qui fait deriver
Il y avait deux assembleurs de charge, un par sorte de console. Ils ont derive deux fois le
meme jour : en forme (serveurs valait [] d'un cote et {} de l'autre, et charger() levait),
puis en contenu (cinq registres servis vides alors que le site a SON plan — 9 serveurs,
21 applications, 2 bases, 1 domaine, tous invisibles).
Console assemble, une seule fois, et fixe les clefs et leurs formes. Les branches ne
decident que de ce qui leur appartient : d'ou vient le plan, d'ou vient l'inventaire, quels
pouvoirs elles portent. ConsoleLocataire configure, ConsoleSite materialise et sert
desormais son propre plan, ConsolePoste herite du locataire et sait en plus sur quelle
fabric poser.
Le role et la portee ne se confondent pas : le role est une propriete de la classe, la
portee se calcule depuis les pouvoirs. Un poste prive de la voute du site reste l'atelier du
mainteneur et n'engendre pourtant rien. Le decoupage a montre un trou aussitot : ConsolePoste
heritait du refus d'un locataire — « elle ne sait pas sur quelle fabric poser » — alors qu'il
monte la carte ; ce qui lui manque est la voute, et accuser la mauvaise absence fait chercher
au mauvais endroit.
Le jugement des assistants remonte dans le tronc : il etait ecrit dans la route qui liste ET
dans celle qui execute, et celle qui se trompe est toujours celle qui execute.
Valide : make test a 0 echec, 8 tests de rendu sous node dont deux neufs, console lancee pour
de vrai (13 serveurs, 24 applications, 17 runbooks, 127 etapes, 0 ecart). make verifier a
aussi attrape une faute que j'avais laissee passer sur depots_perimes.yml — risky-shell-pipe,
corrige. P02 et P60 restent, et P60 demande une republication du wiki.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:55:49 -04:00
Ce que cette fonction faisait a la main vit desormais dans ` Console ` et ses deux
branches . Elle reste parce que le harnais et les preuves l ' appellent par ce nom, et
parce qu ' un appelant n ' a pas a savoir laquelle des deux consoles lui repond .
2026-09-16 14:36:59 -04:00
"""
console : un tronc, deux branches — assembler deux fois est ce qui fait deriver
Il y avait deux assembleurs de charge, un par sorte de console. Ils ont derive deux fois le
meme jour : en forme (serveurs valait [] d'un cote et {} de l'autre, et charger() levait),
puis en contenu (cinq registres servis vides alors que le site a SON plan — 9 serveurs,
21 applications, 2 bases, 1 domaine, tous invisibles).
Console assemble, une seule fois, et fixe les clefs et leurs formes. Les branches ne
decident que de ce qui leur appartient : d'ou vient le plan, d'ou vient l'inventaire, quels
pouvoirs elles portent. ConsoleLocataire configure, ConsoleSite materialise et sert
desormais son propre plan, ConsolePoste herite du locataire et sait en plus sur quelle
fabric poser.
Le role et la portee ne se confondent pas : le role est une propriete de la classe, la
portee se calcule depuis les pouvoirs. Un poste prive de la voute du site reste l'atelier du
mainteneur et n'engendre pourtant rien. Le decoupage a montre un trou aussitot : ConsolePoste
heritait du refus d'un locataire — « elle ne sait pas sur quelle fabric poser » — alors qu'il
monte la carte ; ce qui lui manque est la voute, et accuser la mauvaise absence fait chercher
au mauvais endroit.
Le jugement des assistants remonte dans le tronc : il etait ecrit dans la route qui liste ET
dans celle qui execute, et celle qui se trompe est toujours celle qui execute.
Valide : make test a 0 echec, 8 tests de rendu sous node dont deux neufs, console lancee pour
de vrai (13 serveurs, 24 applications, 17 runbooks, 127 etapes, 0 ecart). make verifier a
aussi attrape une faute que j'avais laissee passer sur depots_perimes.yml — risky-shell-pipe,
corrige. P02 et P60 restent, et P60 demande une republication du wiki.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:55:49 -04:00
return console ( ) . contexte ( )
2026-09-16 14:36:59 -04:00
def inventaire_du_site ( ) - > dict :
""" Les machines du SITE, mises dans la forme d ' un `hosts.yml`.
L ' inventaire d ' un site est un SCRIPT ( ` site_inventaire . py ` ) : on l ' EXECUTE — en
l ' important, sans sous-processus a chaque page — et l ' on traduit son resultat dans la
forme que le reste de ce fichier sait lire . Juger sa source reviendrait a relire le
raisonnement au lieu du resultat .
"""
import site_inventaire
brut = site_inventaire . inventaire ( ) or { }
hostvars = ( ( brut . get ( " _meta " ) or { } ) . get ( " hostvars " ) or { } )
enfants_site : dict = { }
for groupe , valeur in brut . items ( ) :
if groupe == " _meta " or not isinstance ( valeur , dict ) :
continue
hotes = valeur . get ( " hosts " ) or [ ]
if not hotes :
continue
enfants_site [ groupe ] = { " hosts " : { h : dict ( hostvars . get ( h ) or { } ) for h in hotes } }
return { " all " : { " children " : enfants_site } }
def sonder_donnees ( data : dict ) - > dict :
""" Meme sonde que `sonder_hotes`, sur un inventaire DEJA charge (le site). """
cibles = [ ( h . get ( " nom " ) , h . get ( " adresse_ip " ) ) for h in liste_hotes ( data ) ]
with ThreadPoolExecutor ( max_workers = 16 ) as executeur :
resultats = executeur . map ( lambda c : ( c [ 0 ] , _joignable ( c [ 1 ] ) ) , cibles )
return { nom : ok for nom , ok in resultats if nom }
# CE QUE CHAQUE POINT D'ECRITURE EXIGE. Toute route POST y figure — P81 refuse qu'une
# route nouvelle echappe a la garde en silence, parce qu'une garde qui suit une liste
# prend du retard sur elle.
POUVOIR_REQUIS = {
" /api/inventaire " : " editer_le_plan " ,
" /api/bases " : " editer_le_plan " ,
" /api/applications " : " editer_le_plan " ,
" /api/serveurs " : " editer_le_plan " ,
" /api/nomenclature " : " editer_le_plan " ,
" /api/domaines " : " editer_le_plan " ,
" /api/intrants " : " editer_le_plan " ,
" /api/instancier " : " editer_le_plan " ,
" /api/instance-creer " : " editer_le_plan " ,
" /api/instance-utiliser " : " editer_le_plan " ,
" /api/verifier " : " configurer " ,
" /api/deployer " : " configurer " ,
" /api/pousser " : " configurer " ,
" /api/creer " : " materialiser " ,
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
# LA GARDE DE CETTE ROUTE EST PLUS FINE QUE LES AUTRES, ET C'EST VOULU. Un runbook de
# site et un runbook de locataire n'exigent pas le meme pouvoir : un seul mot ici
# serait soit trop laxiste (un site lancerait un geste de locataire), soit trop
# severe (un locataire ne pourrait plus rien). La portee est DECLAREE au registre,
# et c'est elle qu'on resout — jamais celle que le navigateur annonce.
" /api/runbook-etape " : " selon-le-runbook " ,
2026-09-16 14:36:59 -04:00
}
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
# CE QU'UNE PORTEE DE RUNBOOK EXIGE DE LA CONSOLE. Les deux pouvoirs de `contexte()`, et
# rien d'autre : `poste` porte les deux liens, il doit donc les deux.
POUVOIR_PAR_PORTEE = {
" tenant " : ( " configurer " , ) ,
" site " : ( " materialiser " , ) ,
" poste " : ( " configurer " , " materialiser " ) ,
" toute " : ( ) ,
}
# Ce qu'une valeur d'etape a le droit d'etre. Les commandes partent en LISTE (jamais par
# un shell), donc l'injection n'est pas le risque ; ce qui l'est, c'est qu'une valeur se
# fasse lire par `make` comme une option. On refuse donc ce qui commence par un tiret.
MOTIF_VALEUR = re . compile ( r " ^[^- \ s][ \ w .@:/+=,-] { 0,199}$ " , re . UNICODE )
2026-08-02 17:17:10 -04:00
def _fichier_frontiere ( ) - > Path :
2026-08-23 00:47:32 -04:00
""" Les intrants de la frontiere — a la RACINE du depot de site depuis le 2026-08-22.
La frontiere est un objet du MONDE PHYSIQUE : ses reglages vivent avec ` underlay . yml `
et ` proxmox - hebergeur . yml ` , pas dans les group_vars d ' un tenant. Range chez un
locataire , ce fichier a garde une adresse d ' API perimee sans que personne la voie —
et l ' applicateur restait suspendu dessus, sans message.
Les anciens emplacements restent lus , pour un site pas encore migre .
"""
2026-08-02 17:17:10 -04:00
base = _depot_hebergeur ( )
if base :
2026-08-23 00:47:32 -04:00
racine = base / " opnsense.yml "
if racine . is_file ( ) :
return racine
2026-08-02 17:17:10 -04:00
for nom in ( " principal " , " production " , " lab " ) :
p = base / " inventories " / nom / " group_vars " / " opnsense.yml "
if p . is_file ( ) :
return p
2026-08-23 00:47:32 -04:00
return racine # a creer la, pas ailleurs
2026-08-02 17:17:10 -04:00
return INVENTAIRE_DEFAUT . parent / " group_vars/opnsense.yml "
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
def _fichier_proxmox_hebergeur ( ) - > Path :
""" proxmox-hebergeur.yml de l ' HEBERGEUR ; repli sur le proxmox.yml du tenant.
Meme raisonnement que la frontiere et l ' underlay (D-14) : un cluster appartient a
l ' hebergeur, pas au tenant qui y loge. Recopie chez chaque tenant, l ' inventaire du
cluster avait deja diverge — deux listes de stockages contradictoires pour le meme
materiel . Le repli garde un depot sans underlay monte parfaitement utilisable .
"""
base = _depot_hebergeur ( )
if base :
return base / " proxmox-hebergeur.yml "
return INTRANTS_PROXMOX
2026-08-02 17:17:10 -04:00
INTRANTS_FRONTIERE = _fichier_frontiere ( )
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
INTRANTS_PROXMOX_HEBERGEUR = _fichier_proxmox_hebergeur ( )
Adressage derive du seul seed index (rupture, mode compact retire)
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>
2026-07-23 02:58:15 -04:00
FICHIERS_INTRANTS = { " identite " : INTRANTS_IDENTITE , " proxmox " : INTRANTS_PROXMOX ,
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
" proxmox_hebergeur " : INTRANTS_PROXMOX_HEBERGEUR ,
GUI : section « Fabric » — l'underlay se règle depuis la console
Tout le modèle d'underlay bâti aujourd'hui s'éditait à la main dans un YAML,
pendant que la doctrine dit qu'un sysadmin doit exploiter l'outil sans IA.
La frontière avait eu sa section ; l'underlay, non.
La section couvre les valeurs plates : switch routeur, dialecte de CLI, mode
et topologie de spanning-tree. Les listes de tables (`reseaux`, `hotes`, donc
les ports) restent hors de portée du panneau — elles demandent une vue
dédiée, comme celle des serveurs.
Le dialecte devient un intrant déclaré : il ne vivait que dans
SETOPS_DIALECTE. C'est une propriété du matériel, donc de la fabric.
Précédence : `--dialecte` > environnement > intrant déclaré > cisco.
Écriture chirurgicale plutôt que safe_dump : underlay.yml porte 23 lignes de
commentaires qui expliquent des décisions d'architecture, et un dump les
aurait effacées comme c'est arrivé à plan/applications.yml. Vérifié : trois
valeurs modifiées, 73 lignes et 23 commentaires avant comme après. Une clef
absente du fichier est refusée plutôt qu'inventée à un endroit arbitraire.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:01:00 -04:00
" reseau " : FICHIER_NOMENCLATURE , " frontiere " : INTRANTS_FRONTIERE ,
" fabric " : FICHIER_UNDERLAY }
# Intrants de fabric -> chemin dans underlay.yml. Seules les valeurs PLATES sont ici :
# `reseaux` et `hotes` sont des listes de tables, hors de portee du panneau (cf. la vue
# dediee, a venir). Un intrant absent d'ici s'edite encore dans le YAML.
CHEMINS_UNDERLAY = {
" underlay_routeur " : ( " routeur " , ) ,
" underlay_dialecte " : ( " dialecte " , ) ,
" underlay_stp_mode " : ( " stp " , " mode " ) ,
" underlay_stp_topologie " : ( " stp " , " topologie " ) ,
}
2026-06-30 10:07:17 -04:00
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
# (variable, fichier, classe, section, libelle, type, proprietaire)
#
# `proprietaire` repond a une question que le panneau posait sans y repondre : en
# ouvrant l'ecran on trouvait, cote a cote et sans distinction, des valeurs du TENANT
# (son domaine, son realm, son modele) et des valeurs de l'HEBERGEUR (son cluster, sa
# frontiere, sa fabric). Ce sont deux proprietaires, deux depots, deux cycles de vie.
2026-06-30 10:07:17 -04:00
INTRANTS_SCHEMA = [
adressage : le decalage de +10 est retire, l'index se lit dans l'adresse
supernet_de rendait 10.(10+index).0.0/16. Personne ne savait plus pourquoi : ni le
commentaire de la constante, ni le wiki, ni le commit fondateur 36a882b ne le
justifiaient. Trois endroits consultes, zero raison ecrite.
Ses deux effets constates :
- il reservait 10.0-10.9 sous la plage tenant. Utile tant que l'underlay vivait
la — mais D-77 l'a fait entrer dans la bande basse de son propre /16, ce qui a
vide cette reserve de son role la veille ;
- il eloignait le premier tenant de 10.0.0.0/16, la plage la plus repandue en
reseau domestique. Ce risque revient donc aux index bas, et c'est ASSUME.
En echange l'index se lit directement dans l'adresse (17 -> 10.17.x.x) et le
plafond passe de 245 a 255 ecosystemes federes.
Chezlepro (17) 10.27.0.0/16 -> 10.17.0.0/16
Technolibre (11) 10.21.0.0/16 -> 10.11.0.0/16
lab (1) 10.11.0.0/16 -> 10.1.0.0/16
Doc alignee : les trois pages du wiki, multi-instances.md (plafond et exemple
devenus faux arithmetiquement), sdn-evpn.md, le libelle de la GUI, la docstring
d'underlay.py, D-77, et le document de preparation d'un site hebergeur. Les
CONSTATS DE TERRAIN dates sont laisses tels quels : ce sont des mesures.
CE COMMIT NE RENUMEROTE RIEN. Il change ce que le plan DERIVE ; l'inventaire
applique porte toujours 10.27.x.x et les quatorze VM tournent dessus. Appliquer
sans reconstruire rendrait la flotte injoignable — le renumerotage est une
operation a part, a mener a froid.
Au passage, retire un debris : une copie de conflit Nextcloud de
serveur_powerdns/defaults/main.yml, IDENTIQUE a l'original, commitee par accident
dans 1295eea et jamais chargee par Ansible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:16:08 -04:00
( " index " , " reseau " , " constante " , " Réseau " , " Index d ' instance dans la fédération (seed : dérive supernet 10.<index>, VLAN 1000+index× 10+zone, VMID) " , " int " , " tenant " ) ,
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
( " domaine_interne " , " identite " , " constante " , " Identité " , " Domaine DNS interne " , " str " , " tenant " ) ,
( " fuseau_horaire " , " identite " , " defaut " , " Identité " , " Fuseau horaire " , " str " , " tenant " ) ,
( " organisation " , " identite " , " defaut " , " Identité " , " Organisation (annuaire LDAP, certificats) " , " str " , " tenant " ) ,
( " identite_realm " , " identite " , " defaut " , " Identité " , " Realm SSO (Keycloak) " , " str " , " tenant " ) ,
( " nftables_admin_ssh " , " identite " , " catalogue " , " Sécurité " , " Sources SSH d ' administration toujours autorisées (garde anti-lockout du pare-feu) " , " liste " , " tenant " ) ,
( " proxmox_api_host " , " proxmox_hebergeur " , " constante " , " Cluster " , " Hôte API Proxmox " , " str " , " hebergeur " ) ,
( " proxmox_api_user " , " proxmox_hebergeur " , " constante " , " Cluster " , " Utilisateur API " , " str " , " hebergeur " ) ,
( " proxmox_api_port " , " proxmox_hebergeur " , " constante " , " Cluster " , " Port API " , " str " , " hebergeur " ) ,
( " proxmox_validate_certs " , " proxmox_hebergeur " , " constante " , " Cluster " , " Valider les certificats TLS " , " bool " , " hebergeur " ) ,
( " proxmox_clone_source_nom " , " proxmox " , " constante " , " Proxmox " , " Nom du golden template " , " str " , " tenant " ) ,
( " proxmox_clone_vmid_modele " , " proxmox " , " constante " , " Proxmox " , " VMID du golden template " , " int " , " tenant " ) ,
( " proxmox_clone_noeud " , " proxmox " , " defaut " , " Proxmox " , " Nœud Proxmox (défaut) " , " str " , " tenant " ) ,
( " proxmox_clone_stockage " , " proxmox " , " defaut " , " Proxmox " , " Stockage (défaut) " , " str " , " tenant " ) ,
GUI : le « pont reseau » n'est pas un reglage de la flotte — l'intitule le dit
Question de l'exploitant apres la decouverte de vmbr3 : a quoi sert cet intrant ?
Mesure : a presque rien.
proxmox_pont 14 occurrences dans l'inventaire -> DERIVE par hote (VNet de zone)
proxmox_noeud 0 -> proxmox_clone_noeud est la vraie valeur
proxmox_stockage 0 -> proxmox_clone_stockage est la vraie valeur
instancier pose le VNet de chaque zone dans proxmox_pont, et l'hote l'emporte sur
le defaut. proxmox_clone_pont n'est donc consulte que par un `make cloner-vm`
manuel, hors flotte. C'est exactement pourquoi vmbr3 a pu y etre faux dix jours.
C'est le pire genre d'intrant : visible dans le GUI, on le corrige, on redeploie,
rien ne change. L'intitule dit desormais sa portee.
D-80 CORRIGEE : j'y avais ecrit « trois cles : noeud, stockage, pont ». Faux pour
le pont. La liaison de placement reelle est noeud, stockage et gabarit ; le pont
se derive comme le reste.
Verifier avant d'enumerer : j'avais liste les cles en lisant le fichier du tenant,
sans regarder lesquelles sont reellement consultees. Deux le sont, une ne l'est
pas — et c'est celle qui etait fausse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 11:31:41 -04:00
# REPLI, pas un reglage de la flotte. Chaque VM du plan recoit le VNet DERIVE de sa
# zone (`instancier` pose `proxmox_pont` par hote) ; cette valeur n'est consultee que
# si l'hote n'en porte pas — c'est-a-dire jamais dans le flux normal. L'intitule le
# dit, sans quoi on la corrige, on redeploie, et rien ne change : `vmbr3` y est reste
# faux dix jours sans que rien ne bronche (2026-08-13).
( " proxmox_clone_pont " , " proxmox " , " defaut " , " Proxmox " ,
" Pont réseau — clones manuels seulement (les VM du plan reçoivent le VNet de leur zone) " ,
" str " , " tenant " ) ,
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
( " proxmox_noeuds " , " proxmox_hebergeur " , " catalogue " , " Cluster " , " Nœuds disponibles (liste) " , " liste " , " hebergeur " ) ,
( " proxmox_stockages " , " proxmox_hebergeur " , " catalogue " , " Cluster " , " Stockages disponibles (liste) " , " liste " , " hebergeur " ) ,
( " proxmox_ponts " , " proxmox_hebergeur " , " catalogue " , " Cluster " , " Ponts réseau disponibles (liste) " , " liste " , " hebergeur " ) ,
( " opnsense_api_url " , " frontiere " , " constante " , " Frontière " , " URL de gestion de la frontière OPNsense (API REST) " , " str " , " hebergeur " ) ,
( " opnsense_api_verifier_certs " , " frontiere " , " defaut " , " Frontière " , " Valider le certificat TLS de la frontière " , " bool " , " hebergeur " ) ,
( " opnsense_wan_ip " , " frontiere " , " constante " , " Frontière " , " Adresse publique du WAN de la frontière (face Internet de l ' écosystème) " , " str " , " hebergeur " ) ,
( " opnsense_if_wan " , " frontiere " , " defaut " , " Frontière " , " Interface WAN — IDENTIFIANT OPNsense (wan), pas le libellé ni le périphérique " , " str " , " hebergeur " ) ,
( " opnsense_if_transit " , " frontiere " , " defaut " , " Frontière " , " Interface de transit — IDENTIFIANT OPNsense (opt1, opt2…), pas « igb1 » ni le libellé affiché " , " str " , " hebergeur " ) ,
2026-08-06 14:39:51 -04:00
( " opnsense_if_gestion " , " frontiere " , " defaut " , " Frontière " , " Interface où arrivent les plans d ' administration attachés à la frontière — IDENTIFIANT OPNsense (lan). Une règle SSH posée ailleurs ne peut jamais correspondre " , " str " , " hebergeur " ) ,
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
( " underlay_routeur " , " fabric " , " constante " , " Fabric " , " Switch qui porte le routage (SVI, ACL, routes) — les autres restent en L2 pur " , " str " , " hebergeur " ) ,
( " underlay_dialecte " , " fabric " , " constante " , " Fabric " , " Dialecte de CLI du commutateur (cisco | binardat) " , " str " , " hebergeur " ) ,
( " underlay_stp_mode " , " fabric " , " defaut " , " Fabric " , " Mode de spanning-tree (rstp | mstp | pvst) " , " str " , " hebergeur " ) ,
( " underlay_stp_topologie " , " fabric " , " defaut " , " Fabric " , " Topologie de câblage (etoile | anneau | maille) " , " str " , " hebergeur " ) ,
2026-08-01 19:38:17 -04:00
# `opnsense_prochain_saut` n'est PLUS un intrant : il derive du reseau de transit de
# l'underlay (`passerelle` du reseau portant `passerelle_sortie`). Le saisir ici en
# doublon ouvrirait la porte a deux valeurs contradictoires pour un seul lien.
2026-06-30 10:07:17 -04:00
]
# Cles secretes : interdites a l'ecriture par le GUI (garde-fou).
INTRANTS_CLES_INTERDITES = {
" proxmox_api_token_id " , " proxmox_api_token_secret " , " ansible_become_pass " , " ansible_password " ,
frontière nord/sud : devis dérivé, lien de transit et les deux routes
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>
2026-08-01 19:32:04 -04:00
# Frontiere : les identifiants d'API ne transitent jamais par le panneau.
" opnsense_api_key " , " opnsense_api_secret " ,
2026-06-30 10:07:17 -04:00
}
# Rappel en lecture seule (jamais de valeur) : secrets a fournir via Ansible Vault.
2026-08-01 22:25:41 -04:00
def secrets_attendus ( ) - > list [ str ] :
""" Noms des secrets a fournir par la voute — DERIVES, jamais tenus a la main.
` voute . secrets_exiges ( ) ` les recense depuis le plan , les roles des groupes actifs et
les group_vars de l ' instance ; `voute.SECRETS_HORS_MOTIF` ajoute ceux qui ne portent
pas le prefixe ` vault_ ` ( les jetons Proxmox ) . C ' est la MEME source que la preuve P18.
Une liste ecrite a la main vivait ici et avait diverge : elle annoncait
2026-08-01 22:37:27 -04:00
` vault_ldap_sssd ` , qu ' aucun role ne consomme. Un operateur qui suivait le panneau
creait donc un secret inutile . Le recensement par motif textuel a lui aussi sa
limite : il a un temps reclame ` vault_step_ca_fingerprint ` a cause d ' un defaut mort
dans ` client_pki ` , qui derive en realite l ' empreinte a chaud depuis l ' AC .
2026-08-01 22:25:41 -04:00
Depot public nu ( aucune instance ) : liste vide plutot qu ' une erreur d ' affichage .
"""
try :
import voute
return sorted ( set ( voute . secrets_exiges ( ) ) | voute . SECRETS_HORS_MOTIF )
except Exception :
return [ ]
2026-06-30 10:07:17 -04:00
2026-06-24 20:17:46 -04:00
# Garde-fous des executions (verifier / deployer) depuis l'interface.
JETON = secrets . token_urlsafe ( 18 )
VERROU = threading . Lock ( )
VERIF_OK : dict [ str , bool ] = { }
MOTIF_HOTE = re . compile ( r " ^[A-Za-z0-9._-] { 1,63}$ " )
# Champs de provisioning persistes comme variables d'hote dans l'inventaire.
# (cle_gui, variable_inventaire, type)
CHAMPS_PROVISION = [
( " adresse_ip " , " ansible_host " , " str " ) ,
( " utilisateur_ansible " , " ansible_user " , " str " ) ,
( " cidr " , " proxmox_cidr " , " int " ) ,
( " passerelle " , " proxmox_passerelle " , " str " ) ,
( " vlan " , " proxmox_vlan " , " int " ) ,
( " pont " , " proxmox_pont " , " str " ) ,
( " dns " , " proxmox_dns " , " str " ) ,
( " vmid " , " proxmox_vmid " , " int " ) ,
( " noeud " , " proxmox_noeud " , " str " ) ,
( " stockage " , " proxmox_stockage " , " str " ) ,
( " disque_taille " , " proxmox_disque_taille " , " str " ) ,
( " memoire " , " proxmox_memoire " , " int " ) ,
( " coeurs " , " proxmox_coeurs " , " int " ) ,
]
def charger_yaml ( path : Path ) - > dict :
if not path . exists ( ) :
return { " all " : { " children " : { } } }
with path . open ( " r " , encoding = " utf-8 " ) as fichier :
data = yaml . safe_load ( fichier ) or { }
if not isinstance ( data , dict ) :
raise ValueError ( f " { path } ne contient pas une table YAML. " )
data . setdefault ( " all " , { } )
data [ " all " ] . setdefault ( " children " , { } )
return data
def ecrire_yaml ( path : Path , data : dict ) - > None :
plan : l ecriture des registres devient atomique — tout, ou rien
`path.open("w")` TRONQUE avant d ecrire : entre les deux, le fichier est vide.
Une exception dans yaml.safe_dump, un disque plein, un Ctrl-C, et
instance/plan/serveurs.yml reste mutile.
L asymetrie fait la gravite : hosts.yml se regenere d un
make instancier-appliquer, le PLAN ne se regenere de rien. C est la source
unique de verite. Git est le filet, mais encore faut-il savoir qu on est tombe.
TREIZE SITES, UNE SEULE FONCTION
Le defaut n etait pas dans le GUI seul : douze sites dans sept fichiers, dont
les miroirs CLI des MEMES registres. Corriger le GUI seul aurait recree la
divergence que P41 garde depuis les neuf resolutions d instance. La fonction
vit donc dans inventory_rules.py, que les sept importaient deja. Une source,
pas douze.
TROIS DETAILS QUI FONT LA DIFFERENCE ENTRE « CA MARCHE » ET « CA TIENT »
temporaire dans le MEME dossier os.replace n est atomique qu au sein d un
meme systeme de fichiers ; un /tmp sur une
autre partition casserait la garantie sans
rien dire
fsync AVANT le rename sinon le renommage peut atteindre le disque
avant le contenu : au retour d une coupure
brutale, un fichier neuf et VIDE — le defaut
qu on ferme, deplace d un cran
report des droits mkstemp cree en 0600, le plan est en 0664 et
doit rester lisible par le groupe sur les
runners
LE TEST PORTE SON PROPRE CONTROLE NEGATIF
scripts/tests/test_ecriture_atomique.py rejoue D ABORD l ancienne forme et
verifie qu elle DETRUIT. Sans ce controle, « le fichier est intact » ne
prouverait rien — il pourrait l etre parce que rien n a ete ecrit du tout. Une
garantie qu on n a jamais vue echouer n est pas une garantie, c est une
habitude.
Branche sur P02, dont le titre annoncait « inventory_host » alors qu il lance
maintenant trois tests. Corrige au passage.
LA VOUTE DU GUI : VERIFIEE, PAS DE DEFAUT
Le soupcon etait qu executer_flux pose ANSIBLE_VAULT_PASSWORD_FILE (un seul mot
de passe) alors que creer une VM ouvre DEUX voutes depuis « une voute, une cle ».
Eprouve contre deux voutes JETABLES a mots de passe distincts — jamais les
vraies. Les deux variables se CUMULENT : Ansible essaie tous les secrets, et un
PASSWORD_FILE errone n empeche rien. Confirme en sondant l environnement qu une
recette make recoit reellement : le mot de passe saisi ET les cinq cles
calculees par voutes.py.
Ce qui sauve ce chemin n est donc pas le mot de passe saisi, c est
l IDENTITY_LIST que make pose par-dessus. Chacun couvre ce que l autre ne
couvre pas — le PASSWORD_FILE sert le runner qui n a que sa cle, l IDENTITY_LIST
le poste qui les a toutes. Ecrit au-dessus du code, pour que personne ne
« simplifie » en retirant l un des deux.
make prouver : CONFORME, 59 OK, 0 echec, 1 saute.
make instancier : DIFF VIDE, quatre registres relus, droits 664 preserves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 14:57:57 -04:00
with ecriture_atomique ( path ) as fichier :
2026-06-24 20:17:46 -04:00
yaml . safe_dump ( data , fichier , default_flow_style = False , sort_keys = False , allow_unicode = True )
def enfants ( data : dict ) - > dict :
return data . setdefault ( " all " , { } ) . setdefault ( " children " , { } )
def groupes_disponibles ( data : dict ) - > list [ str ] :
return groupes_operationnels_connus ( enfants ( data ) , DOSSIER_PLAYBOOKS_GROUPES )
def roles_des_groupes ( ) - > dict :
""" Pour chaque playbook de groupe, extrait les roles appliques (chaine groupe -> roles). """
resultat : dict = { }
for chemin in sorted ( DOSSIER_PLAYBOOKS_GROUPES . glob ( " *.yml " ) ) :
roles : list [ str ] = [ ]
try :
with chemin . open ( " r " , encoding = " utf-8 " ) as fichier :
docs = yaml . safe_load ( fichier ) or [ ]
plays = docs if isinstance ( docs , list ) else [ docs ]
for play in plays :
if not isinstance ( play , dict ) :
continue
for role in play . get ( " roles " , [ ] ) or [ ] :
if isinstance ( role , str ) :
roles . append ( role )
elif isinstance ( role , dict ) :
nom = role . get ( " role " ) or role . get ( " name " )
if nom :
roles . append ( str ( nom ) )
except ( OSError , yaml . YAMLError ) :
pass
resultat [ chemin . stem ] = { " roles " : roles , " stub " : not roles }
return resultat
def hotes_actifs_fichier ( path : Path ) - > set [ str ] :
data = charger_yaml ( path )
return set ( enfants ( data ) . get ( GROUPE_HOTES_ACTIFS , { } ) . get ( " hosts " , { } ) or { } )
def variables_hote ( data : dict , hote : str ) - > dict :
resultat : dict = { }
for groupe_data in enfants ( data ) . values ( ) :
hosts = groupe_data . get ( " hosts " , { } )
valeurs = hosts . get ( hote )
if isinstance ( valeurs , dict ) :
resultat . update ( valeurs )
return resultat
def liste_hotes ( data : dict ) - > list [ dict ] :
noms = set ( )
for groupe_data in enfants ( data ) . values ( ) :
noms . update ( groupe_data . get ( " hosts " , { } ) )
hotes = [ ]
for nom in sorted ( noms ) :
groupes = [ ]
etat = " planifie "
visible = False
for groupe , groupe_data in enfants ( data ) . items ( ) :
if nom not in groupe_data . get ( " hosts " , { } ) :
continue
if groupe == GROUPE_HOTES_ACTIFS :
etat = " actif "
visible = True
elif groupe in GROUPES_ETAT_HOTE :
visible = True
continue
elif est_groupe_operationnel ( groupe ) :
groupes . append ( groupe )
visible = True
if not visible :
continue
variables = variables_hote ( data , nom )
hote = { " nom " : nom , " etat " : etat , " groupes " : sorted ( groupes ) }
for cle , var , _type in CHAMPS_PROVISION :
valeur = variables . get ( var , " " )
hote [ cle ] = " " if valeur is None else valeur
if not str ( hote . get ( " utilisateur_ansible " , " " ) ) . strip ( ) :
hote [ " utilisateur_ansible " ] = " ansible "
hotes . append ( hote )
return hotes
2026-07-01 17:39:44 -04:00
def _joignable ( ip : str , port : int = 22 , timeout : float = 1.5 ) - > bool :
if not ip :
return False
try :
with socket . create_connection ( ( ip , port ) , timeout = timeout ) :
return True
except OSError :
return False
def sonder_hotes ( path : Path ) - > dict :
""" Etat REEL (vs plan declaratif) : la VM de chaque hote repond-elle sur SSH ? """
cibles = [ ( h . get ( " nom " ) , h . get ( " adresse_ip " ) ) for h in liste_hotes ( charger_yaml ( path ) ) ]
with ThreadPoolExecutor ( max_workers = 16 ) as executeur :
resultats = executeur . map ( lambda c : ( c [ 0 ] , _joignable ( c [ 1 ] ) ) , cibles )
return { nom : ok for nom , ok in resultats if nom }
def marquer_serveur_actif ( hote : str ) - > None :
""" Auto-actif : un hote materialise (clone/deploye) passe ' actif ' dans le plan. """
registre = charger_serveurs ( FICHIER_SERVEURS )
serveurs = registre . get ( " serveurs " ) or { }
if hote in serveurs and serveurs [ hote ] . get ( " etat " ) != " actif " :
serveurs [ hote ] [ " etat " ] = " actif "
ecrire_serveurs ( FICHIER_SERVEURS , registre )
2026-06-24 20:17:46 -04:00
def serveurs_reconcilies ( data : dict ) - > list :
""" Serveurs du plan + valeurs derivees + statut de reconciliation (lecture). """
registre = charger_serveurs ( FICHIER_SERVEURS )
nomenclature = charger_nomenclature ( FICHIER_NOMENCLATURE )
hotes = { h [ " nom " ] : h for h in liste_hotes ( data ) if h . get ( " nom " ) }
resultat = [ ]
for nom , srv in ( registre . get ( " serveurs " ) or { } ) . items ( ) :
rec = reconcilier_serveur ( nom , srv , hotes . get ( nom ) , nomenclature )
d = rec [ " derive " ]
statut = " absent " if rec [ " absent_inventaire " ] else ( " divergence " if rec [ " divergences " ] else " reconcilie " )
resultat . append ( {
" nom " : nom , " fonction " : srv . get ( " fonction " , " " ) , " etat " : srv . get ( " etat " , " " ) ,
" noeud " : srv . get ( " noeud " , " " ) , " stockage " : srv . get ( " stockage " , " " ) ,
" disque " : srv . get ( " disque " , " " ) , " memoire " : srv . get ( " memoire " , " " ) , " coeurs " : srv . get ( " coeurs " , " " ) ,
" integrations " : srv . get ( " integrations " , [ ] ) ,
" vmid " : d . get ( " vmid " , " " ) , " adresse_ip " : d . get ( " adresse_ip " , " " ) , " vlan " : d . get ( " vlan " , " " ) ,
" statut " : statut , " divergences " : rec [ " divergences " ] ,
} )
return resultat
2026-07-07 11:34:55 -04:00
def flux_api ( ) - > list :
""" Matrice des flux réseau (meta/flux.yml de tous les rôles) — lecture seule. """
try :
import resoudre_flux
flux = resoudre_flux . charger_flux ( )
except Exception :
return [ ]
lignes = [ ]
for role in sorted ( flux ) :
for fl in flux [ role ] :
pair = fl . get ( " pair " )
pair = " , " . join ( str ( p ) for p in pair ) if isinstance ( pair , list ) else str ( pair )
port = fl . get ( " port " )
port = " , " . join ( str ( p ) for p in port ) if isinstance ( port , list ) else str ( port )
lignes . append ( {
" role " : role , " sens " : fl . get ( " sens " , " " ) , " port " : port ,
" protocole " : fl . get ( " protocole " , " " ) , " pair " : pair ,
" chiffrement " : fl . get ( " chiffrement " , " " ) , " raison " : fl . get ( " raison " , " " ) ,
} )
return lignes
def couches_api ( ) - > list :
""" Ordre de déploiement en couches (couches-deploiement.yml + graphe) — lecture seule. """
try :
import orchestrer
return [ { " nom " : nom , " groupes " : list ( groupes ) } for nom , groupes in orchestrer . ordre_deploiement ( ) ]
except Exception :
return [ ]
console : un tronc, deux branches — assembler deux fois est ce qui fait deriver
Il y avait deux assembleurs de charge, un par sorte de console. Ils ont derive deux fois le
meme jour : en forme (serveurs valait [] d'un cote et {} de l'autre, et charger() levait),
puis en contenu (cinq registres servis vides alors que le site a SON plan — 9 serveurs,
21 applications, 2 bases, 1 domaine, tous invisibles).
Console assemble, une seule fois, et fixe les clefs et leurs formes. Les branches ne
decident que de ce qui leur appartient : d'ou vient le plan, d'ou vient l'inventaire, quels
pouvoirs elles portent. ConsoleLocataire configure, ConsoleSite materialise et sert
desormais son propre plan, ConsolePoste herite du locataire et sait en plus sur quelle
fabric poser.
Le role et la portee ne se confondent pas : le role est une propriete de la classe, la
portee se calcule depuis les pouvoirs. Un poste prive de la voute du site reste l'atelier du
mainteneur et n'engendre pourtant rien. Le decoupage a montre un trou aussitot : ConsolePoste
heritait du refus d'un locataire — « elle ne sait pas sur quelle fabric poser » — alors qu'il
monte la carte ; ce qui lui manque est la voute, et accuser la mauvaise absence fait chercher
au mauvais endroit.
Le jugement des assistants remonte dans le tronc : il etait ecrit dans la route qui liste ET
dans celle qui execute, et celle qui se trompe est toujours celle qui execute.
Valide : make test a 0 echec, 8 tests de rendu sous node dont deux neufs, console lancee pour
de vrai (13 serveurs, 24 applications, 17 runbooks, 127 etapes, 0 ecart). make verifier a
aussi attrape une faute que j'avais laissee passer sur depots_perimes.yml — risky-shell-pipe,
corrige. P02 et P60 restent, et P60 demande une republication du wiki.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:55:49 -04:00
# ═══ LES DEUX CONSOLES, ET LE TRONC QU'ELLES PARTAGENT ═══════════════════════════════
#
# POURQUOI DEUX CLASSES, ET PAS DEUX FONCTIONS (2026-09-20).
#
# Il y avait deux fonctions : `inventaire_api` pour un locataire, `inventaire_api_du_site`
# pour un site. Elles assemblaient LA MEME charge, chacune de son cote — et elles ont
# derive, deux fois le meme jour :
#
# en FORME `serveurs` valait `[]` d'un cote et `{}` de l'autre. Les deux « vides »,
# et la page ne les lit pas pareil : `charger()` LEVAIT, et la console d'un
# site mourait avant sa premiere vue.
# en CONTENU cinq registres etaient servis vides « pour ne pas fabriquer un faux plan
# de tenant » — alors que le site a SON plan, au meme format, que les
# lecteurs du moteur lisent sans broncher : 9 serveurs, 21 applications.
#
# Deux fonctions qui doivent rendre la meme forme finissent toujours par ne plus la
# rendre. Un tronc qui assemble, et des branches qui ne decident que de CE QUI LEUR
# APPARTIENT, rendent cette derive impossible : il n'y a plus qu'un seul endroit ou la
# charge se compose.
#
# CE QUE CHAQUE BRANCHE EST, au sens de `docs/responsabilites-locataire-hebergeur.md` :
#
# ConsoleLocataire configure SES services — voute du tenant, plan derive d'un seed
# ConsoleSite materialise le terrain — voute du site, plan DECLARE (ip, vmid)
# ConsoleOrpheline ne pilote rien, et c'est ce qu'elle doit dire
#
# Aucune n'est omnipotente, et la classe le porte au lieu d'un `if portee == ...` seme
# dans le code.
class Console :
""" Le tronc : ce qu ' une console EST, quelle que soit la machine qui la sert. """
# LE ROLE est une propriete de la CLASSE — ce pour quoi cette console existe.
# LA PORTEE se CALCULE — ce qu'elle peut vraiment, ici et maintenant. Les deux ne se
# confondent pas : un poste prive de la voute du site reste l'atelier du mainteneur,
# et n'engendre pourtant rien. Figer la portee sur la classe aurait rendu a ce
# poste-la un pouvoir que la mesure lui refuse.
role = " aucun "
@property
def portee ( self ) - > str :
p = self . pouvoirs ( )
return ( " poste " if p [ " configurer " ] and p [ " materialiser " ] else
" tenant " if p [ " configurer " ] else
" site " if p [ " materialiser " ] else " orphelin " )
@property
def titre ( self ) - > str :
return {
" poste " : f " Poste du mainteneur — { self . ecosysteme ( ) } sur { self . fabric ( ) } " ,
" tenant " : f " Console du locataire — { self . ecosysteme ( ) } " ,
" site " : f " Console du SITE — { self . fabric ( ) } " ,
} . get ( self . portee , " Console sans pouvoir — elle ne pilote rien " )
# LES REGISTRES DU PLAN, declares UNE fois : leur nom a l'ecran, leur fichier, leur
# lecteur. Une branche qui n'a pas de plan les rend vides — de la BONNE forme, parce
# que c'est le tronc qui decide de la forme, pas la branche.
REGISTRES = (
( " nomenclature " , " nomenclature.yml " , charger_nomenclature ) ,
( " bases " , " bases-donnees.yml " , charger_bases_donnees ) ,
( " applications " , " applications.yml " , charger_applications ) ,
( " domaines " , " domaines.yml " , charger_domaines ) ,
)
# --- CE QUE CHAQUE BRANCHE DECIDE POUR ELLE ---------------------------------------
def racine_du_plan ( self ) - > Path | None :
""" Le dossier `plan/` que cette console lit. None = elle n ' en a pas. """
return None
def inventaire_brut ( self ) - > dict :
""" L ' inventaire, dans la forme d ' un `hosts.yml`. Vide par defaut. """
return { " all " : { " children " : { } } }
def nom_inventaire ( self ) - > str :
return " "
def est_production ( self ) - > bool :
return False
def serveurs ( self , data : dict ) - > list :
""" Les serveurs, TOUJOURS en liste. D ' ou ils viennent regarde la branche. """
return [ ]
def intrants ( self ) :
return None
def pouvoirs ( self ) - > dict :
return { " editer_le_plan " : False , " configurer " : False ,
" materialiser " : False , " fabric " : False }
def raisons ( self ) - > dict :
return {
" configurer " : " Cette console ne porte la voute d ' aucun ecosysteme. " ,
" editer_le_plan " : " Aucun plan n ' est monte ici. " ,
" materialiser " : " Cette console ne sait sur quelle fabric poser une VM. " ,
" fabric " : " Aucune fabric n ' est montee ici. " ,
}
def ecosysteme ( self ) - > str | None :
return None
def fabric ( self ) - > str | None :
return None
2026-10-11 17:31:13 -04:00
# LES DEUX COTES DU CONTRAT (lot 2, 2026-10-11). Un locataire lit ce qu'il publie a son
# site et ce que le site lui a depose ; un site lit ce que chacun de ses locataires lui
# a publie. None = cette console n'a pas ce cote-la, et la page ne l'offre pas.
def lien_avec_le_site ( self ) - > dict | None :
return None
def locataires_heberges ( self ) - > dict | None :
return None
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
def a_traiter ( self ) - > list | None :
return None
console : un tronc, deux branches — assembler deux fois est ce qui fait deriver
Il y avait deux assembleurs de charge, un par sorte de console. Ils ont derive deux fois le
meme jour : en forme (serveurs valait [] d'un cote et {} de l'autre, et charger() levait),
puis en contenu (cinq registres servis vides alors que le site a SON plan — 9 serveurs,
21 applications, 2 bases, 1 domaine, tous invisibles).
Console assemble, une seule fois, et fixe les clefs et leurs formes. Les branches ne
decident que de ce qui leur appartient : d'ou vient le plan, d'ou vient l'inventaire, quels
pouvoirs elles portent. ConsoleLocataire configure, ConsoleSite materialise et sert
desormais son propre plan, ConsolePoste herite du locataire et sait en plus sur quelle
fabric poser.
Le role et la portee ne se confondent pas : le role est une propriete de la classe, la
portee se calcule depuis les pouvoirs. Un poste prive de la voute du site reste l'atelier du
mainteneur et n'engendre pourtant rien. Le decoupage a montre un trou aussitot : ConsolePoste
heritait du refus d'un locataire — « elle ne sait pas sur quelle fabric poser » — alors qu'il
monte la carte ; ce qui lui manque est la voute, et accuser la mauvaise absence fait chercher
au mauvais endroit.
Le jugement des assistants remonte dans le tronc : il etait ecrit dans la route qui liste ET
dans celle qui execute, et celle qui se trompe est toujours celle qui execute.
Valide : make test a 0 echec, 8 tests de rendu sous node dont deux neufs, console lancee pour
de vrai (13 serveurs, 24 applications, 17 runbooks, 127 etapes, 0 ecart). make verifier a
aussi attrape une faute que j'avais laissee passer sur depots_perimes.yml — risky-shell-pipe,
corrige. P02 et P60 restent, et P60 demande une republication du wiki.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:55:49 -04:00
# --- LE TRONC : UN SEUL ENDROIT OU LA CHARGE SE COMPOSE ---------------------------
def contexte ( self ) - > dict :
return {
" portee " : self . portee ,
" role " : self . role ,
" titre " : self . titre ,
" ecosysteme " : self . ecosysteme ( ) ,
" fabric " : self . fabric ( ) ,
" peut " : self . pouvoirs ( ) ,
" raison " : self . raisons ( ) ,
# QUELLE SOURCE D'INVENTAIRE : un site n'a pas de `hosts.yml`, son inventaire
# est un SCRIPT. Une console qui a quelque chose a piloter le NOMME.
" inventaire " : self . nom_inventaire ( ) or None ,
2026-10-11 17:31:13 -04:00
# LES COTES DU CONTRAT QUE CETTE CONSOLE LIT : la page n'offre que ceux-la.
" contrat " : {
" lien_avec_le_site " : type ( self ) . lien_avec_le_site is not Console . lien_avec_le_site ,
" locataires_heberges " : type ( self ) . locataires_heberges is not Console . locataires_heberges ,
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
" a_traiter " : type ( self ) . a_traiter is not Console . a_traiter ,
2026-10-11 17:31:13 -04:00
} ,
console : un tronc, deux branches — assembler deux fois est ce qui fait deriver
Il y avait deux assembleurs de charge, un par sorte de console. Ils ont derive deux fois le
meme jour : en forme (serveurs valait [] d'un cote et {} de l'autre, et charger() levait),
puis en contenu (cinq registres servis vides alors que le site a SON plan — 9 serveurs,
21 applications, 2 bases, 1 domaine, tous invisibles).
Console assemble, une seule fois, et fixe les clefs et leurs formes. Les branches ne
decident que de ce qui leur appartient : d'ou vient le plan, d'ou vient l'inventaire, quels
pouvoirs elles portent. ConsoleLocataire configure, ConsoleSite materialise et sert
desormais son propre plan, ConsolePoste herite du locataire et sait en plus sur quelle
fabric poser.
Le role et la portee ne se confondent pas : le role est une propriete de la classe, la
portee se calcule depuis les pouvoirs. Un poste prive de la voute du site reste l'atelier du
mainteneur et n'engendre pourtant rien. Le decoupage a montre un trou aussitot : ConsolePoste
heritait du refus d'un locataire — « elle ne sait pas sur quelle fabric poser » — alors qu'il
monte la carte ; ce qui lui manque est la voute, et accuser la mauvaise absence fait chercher
au mauvais endroit.
Le jugement des assistants remonte dans le tronc : il etait ecrit dans la route qui liste ET
dans celle qui execute, et celle qui se trompe est toujours celle qui execute.
Valide : make test a 0 echec, 8 tests de rendu sous node dont deux neufs, console lancee pour
de vrai (13 serveurs, 24 applications, 17 runbooks, 127 etapes, 0 ecart). make verifier a
aussi attrape une faute que j'avais laissee passer sur depots_perimes.yml — risky-shell-pipe,
corrige. P02 et P60 restent, et P60 demande une republication du wiki.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:55:49 -04:00
}
def peut_conduire ( self , portee_etape : str ) - > tuple [ bool , str ] :
""" Cette console peut-elle conduire une etape de CETTE portee — et sinon, pourquoi ?
LE JUGEMENT VIT ICI , PAS DANS LES ROUTES . Il etait ecrit deux fois : dans la route
qui LISTE les assistants et dans celle qui EXECUTE une etape . Deux copies d ' une
meme regle finissent par ne plus dire la meme chose — et celle qui se trompe est
toujours celle qui execute , parce que c ' est la moins relue.
"""
peut = self . pouvoirs ( )
manquants = [ p for p in POUVOIR_PAR_PORTEE . get ( portee_etape , ( ) )
if not peut . get ( p ) ]
if not manquants :
return True , " "
return False , self . raisons ( ) . get ( manquants [ 0 ] ,
" Geste hors de la portee de cette console. " )
def charge ( self ) - > dict :
""" LA charge servie a la page — la seule, pour les deux consoles.
Les clefs et leurs FORMES sont fixees ici . Une branche choisit ce qu ' elle met
dedans , jamais la forme de ce qu ' elle met : c ' est exactement ce qui a manque .
"""
data = self . inventaire_brut ( )
plan = self . racine_du_plan ( )
registres = { }
for clef , fichier , lecteur in self . REGISTRES :
chemin = ( plan / fichier ) if plan else None
registres [ clef ] = lecteur ( chemin ) if ( chemin and chemin . is_file ( ) ) else { }
return {
" flux " : flux_api ( ) ,
" couches " : couches_api ( ) ,
" inventaire " : self . nom_inventaire ( ) ,
" production " : self . est_production ( ) ,
" groupes " : groupes_disponibles ( data ) ,
" dependances " : charger_dependances ( FICHIER_DEPENDANCES ) ,
" chaine " : roles_des_groupes ( ) ,
* * registres ,
" serveurs " : self . serveurs ( data ) ,
" hotes " : liste_hotes ( data ) ,
" intrants " : self . intrants ( ) ,
" integrations_disponibles " : integrations_disponibles ( ) ,
" integrations_universelles " : integrations_universelles ( RACINE ) ,
" liens_acceptes " : catalogue_liens ( RACINE ) ,
# LE SCHEMA VOYAGE AVEC LES DONNEES. Le navigateur genere ses formulaires
# depuis lui plutot que de les porter en dur. Genere par `make schema`, P61.
" schema " : schema_du_plan ( ) ,
" contexte " : self . contexte ( ) ,
}
class ConsoleLocataire ( Console ) :
""" Elle CONFIGURE : des roles sur ses machines, avec la voute du tenant.
Son plan DERIVE tout d ' un seed `index` — VMID, adresse, VLAN. Elle edite donc le
plan , et l ' inventaire qu ' elle lit est un artefact genere depuis lui .
"""
role = " locataire "
def __init__ ( self , instance : Path , inventaire : Path , a_voute : bool ) :
self . _instance = instance
self . _inventaire = inventaire
self . _a_voute = a_voute
def ecosysteme ( self ) :
return self . _instance . resolve ( ) . name
def racine_du_plan ( self ) :
return self . _instance / " plan "
def inventaire_brut ( self ) :
return charger_yaml ( self . _inventaire )
def nom_inventaire ( self ) :
return os . path . relpath ( self . _inventaire , RACINE )
def est_production ( self ) :
return instance_est_production ( self . _inventaire )
def serveurs ( self , data ) :
return serveurs_reconcilies ( data )
def intrants ( self ) :
return intrants_api ( )
2026-10-11 17:31:13 -04:00
def lien_avec_le_site ( self ) :
return lien_du_locataire ( self . _instance . resolve ( ) )
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
def a_traiter ( self ) :
return veille_locataire ( self . _instance . resolve ( ) )
console : un tronc, deux branches — assembler deux fois est ce qui fait deriver
Il y avait deux assembleurs de charge, un par sorte de console. Ils ont derive deux fois le
meme jour : en forme (serveurs valait [] d'un cote et {} de l'autre, et charger() levait),
puis en contenu (cinq registres servis vides alors que le site a SON plan — 9 serveurs,
21 applications, 2 bases, 1 domaine, tous invisibles).
Console assemble, une seule fois, et fixe les clefs et leurs formes. Les branches ne
decident que de ce qui leur appartient : d'ou vient le plan, d'ou vient l'inventaire, quels
pouvoirs elles portent. ConsoleLocataire configure, ConsoleSite materialise et sert
desormais son propre plan, ConsolePoste herite du locataire et sait en plus sur quelle
fabric poser.
Le role et la portee ne se confondent pas : le role est une propriete de la classe, la
portee se calcule depuis les pouvoirs. Un poste prive de la voute du site reste l'atelier du
mainteneur et n'engendre pourtant rien. Le decoupage a montre un trou aussitot : ConsolePoste
heritait du refus d'un locataire — « elle ne sait pas sur quelle fabric poser » — alors qu'il
monte la carte ; ce qui lui manque est la voute, et accuser la mauvaise absence fait chercher
au mauvais endroit.
Le jugement des assistants remonte dans le tronc : il etait ecrit dans la route qui liste ET
dans celle qui execute, et celle qui se trompe est toujours celle qui execute.
Valide : make test a 0 echec, 8 tests de rendu sous node dont deux neufs, console lancee pour
de vrai (13 serveurs, 24 applications, 17 runbooks, 127 etapes, 0 ecart). make verifier a
aussi attrape une faute que j'avais laissee passer sur depots_perimes.yml — risky-shell-pipe,
corrige. P02 et P60 restent, et P60 demande une republication du wiki.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:55:49 -04:00
def pouvoirs ( self ) :
return { " editer_le_plan " : True , " configurer " : self . _a_voute ,
" materialiser " : False , " fabric " : False }
def raisons ( self ) :
r = super ( ) . raisons ( )
r [ " materialiser " ] = ( " Cette console configure un ecosysteme ; elle ne sait pas "
" sur quelle fabric poser une VM, et c ' est voulu : exploiter "
" n ' est pas engendrer. " )
r [ " configurer " ] = ( " Un ecosysteme est monte, mais sa voute n ' est pas sur cette "
" machine : calculer un inventaire ne demande aucun secret, "
" configurer un service si. " )
r [ " fabric " ] = ( " La fabric appartient a l ' hebergeur. Cette console n ' en monte "
" aucune carte. " )
return r
class ConsoleSite ( Console ) :
""" Elle MATERIALISE : cree et detruit des VM, pose le reseau, avec la voute du site.
adressage : le site est une flotte, et P20 le regarde enfin
L'exploitant a demande si l'adressage derive ne valait pas pour toute flotte, site compris.
Oui — decision du 12 septembre, index 37 pour le site. J'avais affirme le contraire a
l'entree (11) du CHANGELOG, en recopiant un commentaire perime de huit jours au lieu de
mesurer. Un commentaire perime se propage : celui-la s'etait aussi loge dans la docstring de
ConsoleSite.
P20 s'appelle « Adressage 100% derive du seed (aucun stocke) » et sa portee etait
« l'instance liee + les modeles » : elle ne regardait jamais le site, qui stockait
sous_reseau et passerelle pour ses sept zones. Elle juge desormais les deux natures d'un
underlay et refuse une entree qui ne declare pas la sienne — on ne peut pas juger ce qu'on
ne sait pas lire. Controle negatif joue : une zone remise a 10.36.31.0/24 est refusee, en
nommant l'index dont elle aurait du descendre.
Le loader derive sous_reseau et passerelle a la lecture, pour que rien ne change chez les
consommateurs. Mesure : 14 valeurs retirees du fichier, zero ecart sur ce qu'ils lisent, et
make underlay, devis-sdn et devis-reseau passent.
Valide : make test a 0 echec, P20 verte sur 2 nomenclatures et 7 zones de site. P02 et P60
restent, pour les raisons deja consignees.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 22:46:43 -04:00
SES ZONES DERIVENT , SES MACHINES SONT ENCORE ECRITES . Un site a son propre index
depuis le 2026 - 09 - 12 — le 37 pour celui - ci — et ses zones en descendent comme celles
d ' un locataire descendent du sien (P20 le garde). Ce qui reste ecrit dans son plan,
ce sont les ` ip ` , ` vmid ` et ` noeud ` de ses MACHINES : une dette nommee , pas une
doctrine . La docstring disait le contraire : elle recopiait un commentaire perime de
huit jours au lieu de mesurer . Son inventaire , lui , n ' est pas un fichier mais un SCRIPT.
console : un tronc, deux branches — assembler deux fois est ce qui fait deriver
Il y avait deux assembleurs de charge, un par sorte de console. Ils ont derive deux fois le
meme jour : en forme (serveurs valait [] d'un cote et {} de l'autre, et charger() levait),
puis en contenu (cinq registres servis vides alors que le site a SON plan — 9 serveurs,
21 applications, 2 bases, 1 domaine, tous invisibles).
Console assemble, une seule fois, et fixe les clefs et leurs formes. Les branches ne
decident que de ce qui leur appartient : d'ou vient le plan, d'ou vient l'inventaire, quels
pouvoirs elles portent. ConsoleLocataire configure, ConsoleSite materialise et sert
desormais son propre plan, ConsolePoste herite du locataire et sait en plus sur quelle
fabric poser.
Le role et la portee ne se confondent pas : le role est une propriete de la classe, la
portee se calcule depuis les pouvoirs. Un poste prive de la voute du site reste l'atelier du
mainteneur et n'engendre pourtant rien. Le decoupage a montre un trou aussitot : ConsolePoste
heritait du refus d'un locataire — « elle ne sait pas sur quelle fabric poser » — alors qu'il
monte la carte ; ce qui lui manque est la voute, et accuser la mauvaise absence fait chercher
au mauvais endroit.
Le jugement des assistants remonte dans le tronc : il etait ecrit dans la route qui liste ET
dans celle qui execute, et celle qui se trompe est toujours celle qui execute.
Valide : make test a 0 echec, 8 tests de rendu sous node dont deux neufs, console lancee pour
de vrai (13 serveurs, 24 applications, 17 runbooks, 127 etapes, 0 ecart). make verifier a
aussi attrape une faute que j'avais laissee passer sur depots_perimes.yml — risky-shell-pipe,
corrige. P02 et P60 restent, et P60 demande une republication du wiki.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:55:49 -04:00
ELLE A UN PLAN , ET ON LE LUI SERT . La charge du site servait cinq registres vides
« pour ne pas fabriquer un faux plan de tenant » . Le site n ' a pas de faux plan : il a
LE SIEN , au meme format , que les lecteurs du moteur lisent sans broncher — mesure du
2026 - 09 - 20 : 9 serveurs , 21 applications , 2 bases , 1 domaine , tous invisibles .
"""
role = " site "
def __init__ ( self , depot : Path , a_voute : bool ) :
self . _depot = depot
self . _a_voute = a_voute
def fabric ( self ) :
return self . _depot . name
def racine_du_plan ( self ) :
return self . _depot / " plan "
def inventaire_brut ( self ) :
return inventaire_du_site ( )
def nom_inventaire ( self ) :
return f " { self . fabric ( ) } (inventaire dynamique) "
def est_production ( self ) :
return True
2026-10-11 17:31:13 -04:00
def locataires_heberges ( self ) :
return locataires_du_site ( self . _depot )
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
def a_traiter ( self ) :
return veille_site ( self . _depot )
console : un tronc, deux branches — assembler deux fois est ce qui fait deriver
Il y avait deux assembleurs de charge, un par sorte de console. Ils ont derive deux fois le
meme jour : en forme (serveurs valait [] d'un cote et {} de l'autre, et charger() levait),
puis en contenu (cinq registres servis vides alors que le site a SON plan — 9 serveurs,
21 applications, 2 bases, 1 domaine, tous invisibles).
Console assemble, une seule fois, et fixe les clefs et leurs formes. Les branches ne
decident que de ce qui leur appartient : d'ou vient le plan, d'ou vient l'inventaire, quels
pouvoirs elles portent. ConsoleLocataire configure, ConsoleSite materialise et sert
desormais son propre plan, ConsolePoste herite du locataire et sait en plus sur quelle
fabric poser.
Le role et la portee ne se confondent pas : le role est une propriete de la classe, la
portee se calcule depuis les pouvoirs. Un poste prive de la voute du site reste l'atelier du
mainteneur et n'engendre pourtant rien. Le decoupage a montre un trou aussitot : ConsolePoste
heritait du refus d'un locataire — « elle ne sait pas sur quelle fabric poser » — alors qu'il
monte la carte ; ce qui lui manque est la voute, et accuser la mauvaise absence fait chercher
au mauvais endroit.
Le jugement des assistants remonte dans le tronc : il etait ecrit dans la route qui liste ET
dans celle qui execute, et celle qui se trompe est toujours celle qui execute.
Valide : make test a 0 echec, 8 tests de rendu sous node dont deux neufs, console lancee pour
de vrai (13 serveurs, 24 applications, 17 runbooks, 127 etapes, 0 ecart). make verifier a
aussi attrape une faute que j'avais laissee passer sur depots_perimes.yml — risky-shell-pipe,
corrige. P02 et P60 restent, et P60 demande une republication du wiki.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:55:49 -04:00
def serveurs ( self , data ) :
""" Les serveurs DECLARES du site, dans la forme que la page sait lire.
On ne les reconcilie pas contre une nomenclature : un site n ' en a pas, ses
valeurs sont ecrites . Le statut se lit donc contre l ' inventaire REEL — declare
ici , present la - bas , ou pas .
"""
chemin = self . racine_du_plan ( ) / " serveurs.yml "
if not chemin . is_file ( ) :
return [ ]
registre = ( charger_serveurs ( chemin ) or { } ) . get ( " serveurs " ) or { }
vivants = { h . get ( " nom " ) for h in liste_hotes ( data ) }
sortie = [ ]
for nom , srv in registre . items ( ) :
gabarit = srv . get ( " gabarit " ) or { }
sortie . append ( {
" nom " : nom ,
# Un site ne derive d'aucune fonction : il nomme le RESEAU qui le porte.
" fonction " : srv . get ( " reseau " , " " ) ,
" etat " : srv . get ( " etat " , " " ) ,
" noeud " : srv . get ( " noeud " , " " ) ,
" stockage " : srv . get ( " stockage " , " " ) ,
" disque " : gabarit . get ( " disque " , " " ) ,
" memoire " : gabarit . get ( " memoire " , " " ) ,
" coeurs " : gabarit . get ( " vcpu " , " " ) ,
" integrations " : srv . get ( " integrations " , [ ] ) ,
" vmid " : srv . get ( " vmid " , " " ) ,
" adresse_ip " : srv . get ( " ip " , " " ) ,
" vlan " : " " ,
" statut " : " reconcilie " if nom in vivants else " absent " ,
" divergences " : [ ] ,
} )
return sortie
def pouvoirs ( self ) :
# EDITER : pas encore. Les formulaires sont generes depuis le schema du plan d'un
# LOCATAIRE, ou l'adressage se derive ; le plan d'un site l'ECRIT. Offrir un
# formulaire qui ecrirait de travers serait pire que ne rien offrir. On LIT.
return { " editer_le_plan " : False , " configurer " : False ,
" materialiser " : self . _a_voute , " fabric " : True }
def raisons ( self ) :
r = super ( ) . raisons ( )
r [ " configurer " ] = ( " Cette console materialise le terrain ; elle n ' a la voute "
" d ' aucun locataire, donc elle ne configure aucun de leurs "
" services. " )
r [ " editer_le_plan " ] = ( " Le plan de ce site se LIT ici. Ses adresses sont ECRITES, "
" pas derivees d ' un seed, et les formulaires sont generes "
" depuis le schema d ' un plan de locataire : ils ecriraient "
" de travers. " )
r [ " materialiser " ] = ( " Cette console LIT la carte d ' une fabric dont elle n ' a pas "
" la voute. Creer ou detruire une machine demande le secret "
" du site, et il n ' est pas ici. " )
r [ " fabric " ] = ( " La fabric appartient a l ' hebergeur. Ce qui s ' affiche ici vient "
" d ' une copie locale, et une copie avait deja diverge. " )
return r
class ConsolePoste ( ConsoleLocataire ) :
""" L ' atelier du mainteneur : les deux voutes, donc les deux pouvoirs.
Elle EST une console de locataire — meme plan , meme inventaire — qui sait en plus sur
quelle fabric poser une VM . L ' heritage dit exactement ca, et evite une troisieme
copie de la meme charge .
"""
role = " poste "
def __init__ ( self , instance , inventaire , depot , a_voute_tenant , a_voute_site ) :
super ( ) . __init__ ( instance , inventaire , a_voute_tenant )
self . _depot = depot
self . _a_voute_site = a_voute_site
def fabric ( self ) :
return self . _depot . name
2026-10-11 17:31:13 -04:00
def locataires_heberges ( self ) :
# Le poste monte la carte du site : il voit les deux cotes du contrat.
return locataires_du_site ( self . _depot )
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
def a_traiter ( self ) :
# Les deux veilles ; le moteur n'est compte qu'une fois.
vus , out = set ( ) , [ ]
for e in veille_locataire ( self . _instance . resolve ( ) ) + veille_site ( self . _depot ) :
cle = ( e [ " source " ] , e [ " titre " ] , e [ " detail " ] )
if cle not in vus :
vus . add ( cle )
out . append ( e )
return out
console : un tronc, deux branches — assembler deux fois est ce qui fait deriver
Il y avait deux assembleurs de charge, un par sorte de console. Ils ont derive deux fois le
meme jour : en forme (serveurs valait [] d'un cote et {} de l'autre, et charger() levait),
puis en contenu (cinq registres servis vides alors que le site a SON plan — 9 serveurs,
21 applications, 2 bases, 1 domaine, tous invisibles).
Console assemble, une seule fois, et fixe les clefs et leurs formes. Les branches ne
decident que de ce qui leur appartient : d'ou vient le plan, d'ou vient l'inventaire, quels
pouvoirs elles portent. ConsoleLocataire configure, ConsoleSite materialise et sert
desormais son propre plan, ConsolePoste herite du locataire et sait en plus sur quelle
fabric poser.
Le role et la portee ne se confondent pas : le role est une propriete de la classe, la
portee se calcule depuis les pouvoirs. Un poste prive de la voute du site reste l'atelier du
mainteneur et n'engendre pourtant rien. Le decoupage a montre un trou aussitot : ConsolePoste
heritait du refus d'un locataire — « elle ne sait pas sur quelle fabric poser » — alors qu'il
monte la carte ; ce qui lui manque est la voute, et accuser la mauvaise absence fait chercher
au mauvais endroit.
Le jugement des assistants remonte dans le tronc : il etait ecrit dans la route qui liste ET
dans celle qui execute, et celle qui se trompe est toujours celle qui execute.
Valide : make test a 0 echec, 8 tests de rendu sous node dont deux neufs, console lancee pour
de vrai (13 serveurs, 24 applications, 17 runbooks, 127 etapes, 0 ecart). make verifier a
aussi attrape une faute que j'avais laissee passer sur depots_perimes.yml — risky-shell-pipe,
corrige. P02 et P60 restent, et P60 demande une republication du wiki.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:55:49 -04:00
def pouvoirs ( self ) :
p = super ( ) . pouvoirs ( )
p [ " materialiser " ] = self . _a_voute_site
p [ " fabric " ] = True
return p
def raisons ( self ) :
# CE POSTE SAIT SUR QUELLE FABRIC POSER — il en monte la carte. Ce qui lui manque,
# le cas echeant, c'est la VOUTE. Heriter du refus d'un locataire (« elle ne sait
# pas sur quelle fabric ») enverrait chercher une carte qui est pourtant la, et
# ferait perdre le temps qu'un message juste economise.
r = super ( ) . raisons ( )
r [ " materialiser " ] = ( " Cette console LIT la carte d ' une fabric dont elle n ' a pas "
" la voute. Creer ou detruire une machine demande le secret "
" du site, et il n ' est pas ici. " )
r [ " fabric " ] = ( " La fabric appartient a l ' hebergeur. Ce qui s ' affiche ici vient "
" d ' une copie locale, et une copie avait deja diverge. " )
return r
2026-10-11 17:31:13 -04:00
# --- LE CONTRAT SITE <-> LOCATAIRE, EN LECTURE (lot 2, 2026-10-11) -----------------------
#
# Entre un site et un locataire passent deux fichiers, et seulement eux : la face reseau
# (`face-reseau.yml`, le locataire la publie) et la fiche du site (`fiche-site.yml`, le site
# la depose). Ces deux lectures les montrent a chaque bout, avec leur commit — c'est le
# commit qui dit ce que l'autre a pu lire. Rien n'est sonde ici : ce qui n'est pas dans
# ces fichiers n'est pas affirme, et la page le dit au lieu de le deviner.
def _trace_git ( depot : Path , fichier : str ) - > dict :
""" Le dernier commit qui touche ce fichier, et s ' il porte des changements non commites.
Un fichier modifie mais non commite n ' a pas encore ete transmis : l ' autre bout lit
le commit , pas le disque . """
try :
r = subprocess . run ( [ " git " , " -C " , str ( depot ) , " log " , " -1 " , " --format= %h% x09 %c I " , " -- " , fichier ] ,
capture_output = True , text = True , timeout = 5 )
s = subprocess . run ( [ " git " , " -C " , str ( depot ) , " status " , " --porcelain " , " -- " , fichier ] ,
capture_output = True , text = True , timeout = 5 )
except ( OSError , subprocess . TimeoutExpired ) as exc :
return { " commit " : None , " date " : None , " modifie " : False , " erreur " : str ( exc ) }
if r . returncode != 0 :
return { " commit " : None , " date " : None , " modifie " : False ,
" erreur " : ( ( r . stderr or " " ) . strip ( ) . splitlines ( ) or [ " git refuse " ] ) [ - 1 ] }
commit , _ , date = r . stdout . strip ( ) . partition ( " \t " )
return { " commit " : commit or None , " date " : date or None ,
" modifie " : bool ( s . stdout . strip ( ) ) if s . returncode == 0 else False }
def resume_face ( face : dict ) - > dict :
""" Ce qu ' une face reseau demande, compte et liste pour un humain. Les parametres de
clonage et les sources des flux internes restent dans le fichier . """
machines = face . get ( " machines " ) or { }
vlans = sorted ( { int ( v ) for m in machines . values ( )
for v in [ ( m . get ( " materialisation " ) or { } ) . get ( " proxmox_vlan " ) ]
if str ( v or " " ) . isdigit ( ) } )
sorties , expositions = [ ] , [ ]
for h , d in sorted ( ( face . get ( " flux " ) or { } ) . items ( ) ) :
# Le ICMP « frag-needed » est sur chaque machine (decouverte du MTU) : il n'apprend
# rien a qui lit, et noierait les lignes qui comptent.
for s in d . get ( " sorties_externes " ) or [ ] :
if s . get ( " proto " ) != " icmp " :
sorties . append ( { " hote " : h , " proto " : s . get ( " proto " ) , " port " : str ( s . get ( " port " ) ) ,
" role " : s . get ( " role " ) } )
for e in d . get ( " entrees " ) or [ ] :
if " 0.0.0.0/0 " in ( e . get ( " sources " ) or [ ] ) and e . get ( " proto " ) != " icmp " :
expositions . append ( { " hote " : h , " proto " : e . get ( " proto " ) , " port " : str ( e . get ( " port " ) ) ,
" role " : e . get ( " role " ) } )
adm = face . get ( " administration " ) or { }
return {
" index " : face . get ( " index " ) ,
" hebergeur " : face . get ( " hebergeur " ) ,
" machines " : len ( machines ) ,
" actives " : sum ( 1 for m in machines . values ( ) if m . get ( " etat " ) == " actif " ) ,
" planifiees " : sorted ( h for h , m in machines . items ( ) if m . get ( " etat " ) != " actif " ) ,
2026-10-11 22:47:06 -04:00
" adresses_planifiees " : { h : m . get ( " ip " ) for h , m in machines . items ( ) if m . get ( " etat " ) != " actif " } ,
2026-10-11 17:31:13 -04:00
" zones " : len ( face . get ( " zones " ) or [ ] ) ,
" vlans " : [ vlans [ 0 ] , vlans [ - 1 ] ] if vlans else [ ] ,
" sorties " : sorties ,
" expositions " : expositions ,
" dns_public " : ( face . get ( " dns_public " ) or { } ) . get ( " zones " ) or [ ] ,
" administration " : { " reseaux " : adm . get ( " intrant " ) or [ ] , " tunnel " : adm . get ( " tunnel " ) or [ ] ,
" pairs " : len ( adm . get ( " pairs " ) or { } ) } ,
" sauvegarde " : bool ( ( face . get ( " sauvegarde " ) or { } ) . get ( " cle_publique " ) ) ,
2026-10-11 22:47:06 -04:00
" genome_depot " : str ( ( face . get ( " genome " ) or { } ) . get ( " depot " ) or " " ) ,
2026-10-11 17:31:13 -04:00
}
def lien_du_locataire ( instance : Path ) - > dict :
""" LE COTE LOCATAIRE : ce que je publie a mon site, et ce qu ' il m ' a depose.
« A jour » se mesure ici , chez le locataire qui seul sait calculer sa face ( P92 la
meme regle ) . La fiche , elle , ne se juge que si la carte du site est montee ( poste ) ;
sur le runner d ' un locataire, elle se lit sans pouvoir se juger, et la page le dit.
"""
import contexte as _ctx
loc = _ctx . Locataire ( instance )
face = loc . face_publiee ( )
fiche = loc . fiche_du_site ( ) or { }
try :
site = loc . site ( )
except _ctx . ContexteInvalide :
site = None
2026-10-11 18:43:58 -04:00
# LA FACE SE JUGE PARTOUT CHEZ LE LOCATAIRE. Elle a porte un temps la cle du runner du
# site, lue dans le plan du site : le runner du locataire la recalculait sans elle et la
# disait perimee (mesure du 2026-10-11 sur Technolibre). Le site pose maintenant cette
# cle lui-meme en clonant (`contexte.clonage_rendu`) : la face ne lit plus rien du site.
2026-10-11 18:22:31 -04:00
ecarts_face = None
2026-10-11 18:43:58 -04:00
if face is not None :
2026-10-11 18:22:31 -04:00
try :
ecarts_face = _ctx . verifier_face_publiee ( loc )
except Exception as exc : # une face incalculable ne doit pas masquer la publiee
ecarts_face = [ f " face incalculable ici : { exc } " ]
2026-10-11 17:31:13 -04:00
ecarts_fiche = None
if site is not None and fiche :
try :
ecarts_fiche = _ctx . verifier_fiche_deposee ( site , loc )
except Exception as exc :
ecarts_fiche = [ f " fiche injugeable ici : { exc } " ]
return {
" locataire " : loc . nom ,
" hebergeur " : loc . nom_hebergeur ( ) ,
" face " : { " publiee " : face is not None ,
" trace " : _trace_git ( loc . depot , _ctx . NOM_FICHIER_FACE_RESEAU ) ,
" ecarts " : ecarts_face ,
" resume " : resume_face ( face ) if face else None } ,
" fiche " : { " presente " : bool ( fiche ) ,
" trace " : _trace_git ( loc . depot , _ctx . NOM_FICHIER_FICHE_SITE ) if fiche else None ,
" site " : fiche . get ( " site " ) ,
" attribue " : fiche . get ( " attribue " ) or { } ,
" offre " : fiche . get ( " offre " ) or { } ,
" generation " : fiche . get ( " generation " ) or { } ,
" racine " : ( fiche . get ( " racine_confiance " ) or { } ) . get ( " sha256 " , " " ) [ : 12 ] ,
" jugeable " : site is not None ,
" ecarts " : ecarts_fiche } ,
# Le site ne rapporte pas encore quand il a pose cette face a sa frontiere. Le dire
# vaut mieux qu'un « applique » que rien ne mesure.
" retour_du_site " : None ,
2026-10-11 22:47:06 -04:00
# OU DEMANDER AU SITE (lot 3b) : un ticket dans SON depot prive, sur la forge du site,
# ecrit avec le compte du mainteneur. La console n'a pas de jeton : elle donne le lien.
" demandes " : _ou_demander ( fiche , face ) ,
2026-10-11 17:31:13 -04:00
}
2026-10-11 22:47:06 -04:00
def _ou_demander ( fiche : dict , face : dict | None ) - > dict :
base = url_forge_du_site ( str ( ( fiche . get ( " offre " ) or { } ) . get ( " domaine_site " ) or " " ) )
depot = str ( ( ( face or { } ) . get ( " genome " ) or { } ) . get ( " depot " ) or " " )
if not ( base and depot ) :
return { }
return { " liste " : f " { base } /genome/ { depot } /issues " , " nouvelle " : f " { base } /genome/ { depot } /issues/new " ,
" depot " : depot }
2026-10-11 17:31:13 -04:00
def locataires_du_site ( depot : Path ) - > dict :
""" LE COTE SITE : ce que chacun de mes locataires m ' a publie, et ce que je lui ai depose.
Le site ne lit de son locataire que sa face publiee — le commit present ici est donc ce
que ses gestes ( ` locataire - creer ` , ` creer - vm ` , la frontiere ) liront . La fiche se juge
contre ce que le site calcule aujourd ' hui (P91 la meme regle).
"""
import contexte as _ctx
site = _ctx . Site ( depot )
allocations = site . allocations ( )
sortie = [ ]
for loc in site . locataires ( ) :
face = loc . face_publiee ( )
try :
attribue = site . fiche_pour ( loc ) . get ( " attribue " ) or { }
except Exception as exc :
attribue = { " erreur " : str ( exc ) }
# UN LOCATAIRE PEUT FIGURER CHEZ DEUX SITES : chez l'hebergeur que sa face nomme,
# et chez un site de reprise. Ce n'est pas le meme engagement.
relation = ( " actif " if ( face or { } ) . get ( " hebergeur " ) == site . nom else
" reprise " if face else " inconnue " )
# LA FICHE QU'IL PORTE VIENT DE SON HEBERGEUR ACTIF. Un site de reprise qui la jugeait
# contre la sienne la declarait perimee a chaque lecture (mesure du 2026-10-11 sur
# SITE-Technolibre) : un faux ecart, qui aurait appris a ne plus regarder celui-ci.
if relation == " actif " :
try :
ecarts_fiche = _ctx . verifier_fiche_deposee ( site , loc )
except Exception as exc :
ecarts_fiche = [ f " fiche injugeable ici : { exc } " ]
else :
ecarts_fiche = None
resume = resume_face ( face ) if face else None
sortie . append ( {
" nom " : loc . nom ,
" index_attribue " : allocations . get ( loc . nom ) ,
" ip_publique " : attribue . get ( " ip_publique " ) ,
" relation " : relation ,
" face " : { " publiee " : face is not None ,
" trace " : _trace_git ( loc . depot , _ctx . NOM_FICHIER_FACE_RESEAU ) ,
" resume " : resume } ,
" fiche " : { " deposee " : loc . fiche_du_site ( ) is not None ,
" trace " : _trace_git ( loc . depot , _ctx . NOM_FICHIER_FICHE_SITE ) ,
" ecarts " : ecarts_fiche } ,
} )
return { " site " : site . nom , " index " : site . index , " locataires " : sortie ,
" absents " : site . locataires_absents ( ) }
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
# --- A TRAITER (lot 3a, 2026-10-11) -----------------------------------------------------
#
# Ce que chaque console sait DEJA, rassemble au meme endroit, du plus pressant au moins
# pressant. Ni secret ni forge : les demandes (3b) et la supervision viendront ensuite.
# Chaque element dit sa SOURCE, sa GRAVITE (agir / savoir / ok), ce qu'il touche, et le
# geste qui le traite. Les verifications sans rien a faire sont rendues aussi, en « ok » :
# un ecran vide ne dit pas si l'on a regarde.
def _git_lecture ( depot : Path , * args : str , delai : int = 8 ) - > tuple [ int , str ] :
try :
r = subprocess . run ( [ " git " , " -C " , str ( depot ) , * args ] , capture_output = True , text = True ,
timeout = delai )
return r . returncode , ( r . stdout or r . stderr ) . strip ( )
except ( OSError , subprocess . TimeoutExpired ) as exc :
return 124 , str ( exc )
def etat_forge ( depot : Path ) - > dict :
""" Ce depot local est-il au niveau de sa forge — SANS `git fetch`.
` verifier_genome_a_jour . py ` fetche avant de juger : juste avant un deploiement . Une page
qui fetcherait a chaque affichage ecrirait dans le depot a chaque lecture . ` ls - remote `
lit la tete de la forge , et le reste se juge sur les objets deja presents .
"""
if not ( depot / " .git " ) . exists ( ) :
return { " verdict " : " inconnu " , " detail " : " pas un dépôt git " }
rc , amont = _git_lecture ( depot , " rev-parse " , " --abbrev-ref " , " --symbolic-full-name " , " @ {u} " )
if rc != 0 or " / " not in amont :
return { " verdict " : " inconnu " , " detail " : " aucune branche de suivi " }
distant , _ , branche = amont . partition ( " / " )
rc , local = _git_lecture ( depot , " rev-parse " , " HEAD " )
rc2 , sortie = _git_lecture ( depot , " ls-remote " , distant , f " refs/heads/ { branche } " )
if rc2 != 0 or not sortie . split ( ) :
return { " verdict " : " inconnu " , " detail " : " forge injoignable " , " local " : local [ : 7 ] }
forge = sortie . split ( ) [ 0 ]
base = { " local " : local [ : 7 ] , " forge " : forge [ : 7 ] }
if forge == local :
return { " verdict " : " a-jour " , " detail " : amont , * * base }
if _git_lecture ( depot , " cat-file " , " -e " , f " { forge } ^ {{ commit }} " ) [ 0 ] != 0 :
return { " verdict " : " retard " , " detail " : f " la forge porte { forge [ : 7 ] } , pas encore tiré ici " , * * base }
if _git_lecture ( depot , " merge-base " , " --is-ancestor " , forge , " HEAD " ) [ 0 ] == 0 :
n = _git_lecture ( depot , " rev-list " , " --count " , f " { forge } ..HEAD " ) [ 1 ]
return { " verdict " : " avance " , " detail " : f " { n } commit(s) devant la forge " , * * base }
if _git_lecture ( depot , " merge-base " , " --is-ancestor " , " HEAD " , forge ) [ 0 ] == 0 :
n = _git_lecture ( depot , " rev-list " , " --count " , f " HEAD.. { forge } " ) [ 1 ]
return { " verdict " : " retard " , " detail " : f " { n } commit(s) derrière la forge " , * * base }
return { " verdict " : " diverge " , " detail " : " l ' historique local et celui de la forge ont divergé " , * * base }
def _element ( source : str , gravite : str , titre : str , impact : str = " " , detail : str = " " ,
geste : dict | None = None ) - > dict :
return { " source " : source , " gravite " : gravite , " titre " : titre , " impact " : impact ,
" detail " : detail , " geste " : geste }
def _veille_genome ( depots : list [ tuple [ str , Path , str ] ] , impact_retard : str ) - > list [ dict ] :
""" `depots` : (nom, chemin, commande qui le remet au niveau, lancee depuis le poste). """
out = [ ]
for nom , depot , commande in depots :
e = etat_forge ( depot )
v = e [ " verdict " ]
detail = f " { depot . name } · ici { e . get ( ' local ' , ' ? ' ) } · forge { e . get ( ' forge ' , ' ? ' ) } · { e [ ' detail ' ] } "
if v == " retard " :
out . append ( _element ( " GÉNOME " , " agir " , f " { nom } : en retard sur la forge " , impact_retard ,
detail , { " libelle " : " Depuis le poste : le runner ne tire que ce qu ' on lui fait tirer. " ,
" commande " : commande } ) )
elif v == " diverge " :
out . append ( _element ( " GÉNOME " , " agir " , f " { nom } : a divergé de la forge " ,
" Le runner ne suit plus la forge ; c ' est à un humain de trancher. " , detail ) )
elif v == " avance " :
out . append ( _element ( " GÉNOME " , " savoir " , f " { nom } : en avance sur la forge " ,
" Normal sur le poste d ' un mainteneur qui n ' a pas encore publié. " , detail ,
{ " libelle " : " Publier " , " commande " : " make publier " } ) )
elif v == " inconnu " :
out . append ( _element ( " GÉNOME " , " savoir " , f " { nom } : niveau de la forge inconnu " ,
" La comparaison n ' a pas pu se faire ; rien n ' est affirmé. " , detail ) )
else :
out . append ( _element ( " GÉNOME " , " ok " , f " { nom } : au niveau de la forge " , " " , detail ) )
return out
def veille_locataire ( instance : Path ) - > list [ dict ] :
""" CE QUI ATTEND LE MAINTENEUR : son genome, sa face, la fiche du site, son plan. """
import contexte as _ctx
lien = lien_du_locataire ( instance )
loc = _ctx . Locataire ( instance )
rejouer = f " SETOPS_INSTANCE=../ { loc . nom } make appliquer GROUPE=serveur_ops "
out = _veille_genome ( [ ( " Le moteur " , RACINE , rejouer ) , ( f " Le dépôt { loc . nom } " , loc . depot , rejouer ) ] ,
" Un déploiement d ' ici poserait un état dépassé ; il le refuse "
" (`verifier_genome_a_jour`) tant que le runner n ' a pas tiré. " )
F , Fi = lien [ " face " ] , lien [ " fiche " ]
face_geste = { " libelle " : " Assistant : matérialiser (republier la face) " , " runbook " : " locataire-materialiser " }
if not F [ " publiee " ] :
out . append ( _element ( " FACE " , " agir " , " Face réseau jamais publiée " ,
" Le site ne peut rien créer ni ouvrir pour ce locataire. " ,
" make face-reseau-publier, puis commiter face-reseau.yml " , face_geste ) )
elif F [ " ecarts " ] :
out . append ( _element ( " FACE " , " agir " , " Face réseau à republier " ,
" Le site lit une face qui ne dit plus ce que le plan demande. " ,
" · " . join ( F [ " ecarts " ] ) , face_geste ) )
elif ( F [ " trace " ] or { } ) . get ( " modifie " ) :
out . append ( _element ( " FACE " , " agir " , " Face réseau modifiée, pas encore commitée " ,
" Le site lit le commit : il ne voit pas encore ce changement. " ,
" commiter face-reseau.yml puis `make publier` " ) )
else :
out . append ( _element ( " FACE " , " ok " , " Face réseau publiée et à jour " , " " ,
f " commit { ( F [ ' trace ' ] or { } ) . get ( ' commit ' ) } " , { " libelle " : " Lien avec le site " , " vue " : " lien " } ) )
if not Fi [ " presente " ] :
out . append ( _element ( " FICHE " , " agir " , " Aucune fiche du site " ,
" L ' inventaire se génère d ' après elle ; c ' est au site de la déposer. " ,
" make fiches-site-deposer (chez le site) " ) )
elif Fi [ " jugeable " ] and Fi [ " ecarts " ] :
out . append ( _element ( " FICHE " , " agir " , " Fiche du site périmée " ,
" L ' inventaire se génère sur d ' anciennes valeurs du site. " ,
" · " . join ( Fi [ " ecarts " ] ) , { " libelle " : " Lien avec le site " , " vue " : " lien " } ) )
elif Fi [ " jugeable " ] :
out . append ( _element ( " FICHE " , " ok " , " Fiche du site conforme " , " " , f " commit { ( Fi [ ' trace ' ] or { } ) . get ( ' commit ' ) } " ) )
else :
out . append ( _element ( " FICHE " , " savoir " , " Fiche du site lue, non jugeable d ' ici " ,
" Seuls le site ou le poste du mainteneur peuvent dire si elle est à jour. " ,
f " commit { ( Fi [ ' trace ' ] or { } ) . get ( ' commit ' ) } " ) )
# LE PLAN FACE A L'INVENTAIRE : un serveur declare mais absent, ou dont les valeurs
# derivees ont bouge, attend « Appliquer le plan ».
try :
srv = serveurs_reconcilies ( charger_yaml ( loc . inventaire ( ) ) )
except Exception as exc :
srv = [ ]
out . append ( _element ( " PLAN " , " savoir " , " Plan non comparé à l ' inventaire " , " " , str ( exc ) ) )
absents = [ s [ " nom " ] for s in srv if s . get ( " statut " ) == " absent " ]
diverg = [ s [ " nom " ] for s in srv if s . get ( " statut " ) == " divergence " ]
if absents or diverg :
out . append ( _element ( " PLAN " , " agir " , " Le plan a changé depuis le dernier « Appliquer le plan » " ,
" L ' inventaire, la face et les déploiements suivent encore l ' ancien plan. " ,
" · " . join ( ( [ f " absents de l ' inventaire : { ' , ' . join ( absents ) } " ] if absents else [ ] )
+ ( [ f " valeurs dérivées changées : { ' , ' . join ( diverg ) } " ] if diverg else [ ] ) ) ,
{ " libelle " : " Parc " , " vue " : " serveurs " } ) )
elif srv :
out . append ( _element ( " PLAN " , " ok " , " Plan et inventaire concordent " , " " , f " { len ( srv ) } serveurs " ) )
prevues = ( F . get ( " resume " ) or { } ) . get ( " planifiees " ) or [ ]
if prevues :
out . append ( _element ( " MACHINES " , " savoir " , f " { len ( prevues ) } machine(s) prévues, pas encore actives " ,
" Le site ne les crée que sur demande. " , " , " . join ( prevues ) ) )
return out
2026-10-11 22:47:06 -04:00
# --- LES DEMANDES AU SITE (lot 3b, 2026-10-11) -------------------------------------------
#
# Un mainteneur demande au site par un TICKET dans le depot prive de son locataire, sur la
# forge du site, avec son propre compte. La console du site les lit avec un compte qui ne peut
# que lire (`make forge-comptes`), dont le jeton vit sur le runner du site, hors de tout depot.
# La console d'un locataire n'en a pas encore : elle offre le lien, pre-rempli.
JETON_CONSOLE_SITE = Path . home ( ) / " .config " / " setops-forge-jeton-console "
def url_forge_du_site ( domaine_site : str ) - > str :
return f " https://forge. { domaine_site } " if domaine_site else " "
def demandes_ouvertes ( base : str , depot : str , jeton : str , organisation : str = " genome " ) - > list [ dict ] :
""" Les tickets ouverts d ' un depot, en lecture seule. Leve si la forge ne repond pas. """
import ssl
import urllib . request
req = urllib . request . Request (
f " { base } /api/v1/repos/ { organisation } / { depot } /issues?state=open&type=issues&limit=50 " )
req . add_header ( " Authorization " , f " token { jeton } " )
with urllib . request . urlopen ( req , timeout = 10 , context = ssl . create_default_context ( ) ) as r :
tickets = json . loads ( r . read ( ) . decode ( ) or " [] " )
return [ { " numero " : t . get ( " number " ) , " titre " : t . get ( " title " , " " ) , " url " : t . get ( " html_url " , " " ) ,
" auteur " : ( t . get ( " user " ) or { } ) . get ( " login " , " " ) , " creee " : t . get ( " created_at " , " " ) ,
" commentaires " : t . get ( " comments " , 0 ) } for t in tickets ]
def _demandes_des_locataires ( site_depot : Path , locataires : list [ dict ] ) - > tuple [ dict , str ] :
""" (locataire -> demandes ouvertes, raison si illisible). """
dom = str ( ( yaml . safe_load ( ( site_depot / " plan " / " 10-intrants.yml " ) . read_text ( encoding = " utf-8 " ) )
or { } ) . get ( " domaine_interne " ) or " " ) if ( site_depot / " plan " / " 10-intrants.yml " ) . is_file ( ) else " "
base = url_forge_du_site ( dom )
if not JETON_CONSOLE_SITE . is_file ( ) :
return { } , " jeton de lecture absent sur cette machine (`make forge-comptes CONFIRMER=true` au site) "
jeton = JETON_CONSOLE_SITE . read_text ( encoding = " utf-8 " ) . strip ( )
out = { }
for l in locataires :
depot = ( ( l . get ( " face " ) or { } ) . get ( " resume " ) or { } ) . get ( " genome_depot " )
if not depot :
continue
try :
out [ l [ " nom " ] ] = demandes_ouvertes ( base , depot , jeton )
except Exception as exc :
return out , f " la forge ne repond pas ( { type ( exc ) . __name__ } : { exc } ) "
return out , " "
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
def veille_site ( depot : Path ) - > list [ dict ] :
""" CE QUI ATTEND L ' HEBERGEUR : son genome, et ce que ses locataires lui ont publie. """
charge = locataires_du_site ( depot )
import contexte as _ctx
site = _ctx . Site ( depot )
rejouer = " make site-appliquer GROUPE=serveur_ops ARGS= \" --limit ' serveur_ops:&hotes_actifs ' \" "
depots = [ ( " Le moteur " , RACINE , rejouer ) , ( f " Le dépôt { site . nom } " , site . depot , rejouer ) ]
# Les depots des locataires arrivent sur le runner du site par `make publier`.
depots + = [ ( f " Le dépôt { l . nom } " , l . depot , " make publier " ) for l in site . locataires ( ) ]
out = _veille_genome ( depots , " Le site lirait un moteur, une carte ou une face dépassés : "
" `make publier` et le rôle serveur_ops le remettent au niveau. " )
2026-10-11 22:47:06 -04:00
# LES DEMANDES DES MAINTENEURS, d'abord : c'est quelqu'un qui attend.
demandes , raison = _demandes_des_locataires ( depot , charge [ " locataires " ] )
if raison :
out . append ( _element ( " DEMANDES " , " savoir " , " Demandes des locataires non lues " , raison , " " ,
{ " libelle " : " Créer le compte de lecture de la console " ,
" commande " : " make forge-comptes CONFIRMER=true " } ) )
else :
for nom , liste in sorted ( demandes . items ( ) ) :
for t in liste :
out . append ( _element ( nom , " agir " , f " Demande : { t [ ' titre ' ] } " ,
f " De son mainteneur ( { t [ ' auteur ' ] } ), ouverte le { t [ ' creee ' ] [ : 10 ] } "
+ ( f " · { t [ ' commentaires ' ] } commentaire(s) " if t [ " commentaires " ] else " " ) ,
f " # { t [ ' numero ' ] } · { t [ ' url ' ] } " ,
{ " libelle " : " Ouvrir la demande sur la forge " , " url " : t [ " url " ] } ) )
if not any ( demandes . values ( ) ) :
out . append ( _element ( " DEMANDES " , " ok " , " Aucune demande ouverte par les locataires " , " " ,
" , " . join ( sorted ( demandes ) ) or " aucun dépôt lu " ) )
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
for nom in charge [ " absents " ] :
out . append ( _element ( nom , " agir " , " Locataire déclaré sans dépôt ici " ,
" Le site ne peut rien lire de lui : ni face, ni fiche. " ,
" underlay.yml le déclare ; son dépôt manque sur ce runner " ) )
for l in charge [ " locataires " ] :
R = ( l [ " face " ] or { } ) . get ( " resume " )
if not l [ " face " ] [ " publiee " ] :
out . append ( _element ( l [ " nom " ] , " agir " , " Aucune face réseau publiée " ,
" Le site ne sait rien créer ni ouvrir pour lui. " , " " ,
{ " libelle " : " Locataires hébergés " , " vue " : " locataires " } ) )
continue
if R and R . get ( " index " ) != l [ " index_attribue " ] :
out . append ( _element ( l [ " nom " ] , " agir " , " Index publié différent de l ' index attribué " ,
" Son adressage ne correspond plus à ce que le site lui a donné. " ,
f " attribué { l [ ' index_attribue ' ] } · publié { R . get ( ' index ' ) } " ,
{ " libelle " : " Locataires hébergés " , " vue " : " locataires " } ) )
if l [ " relation " ] == " actif " and l [ " fiche " ] [ " ecarts " ] :
out . append ( _element ( l [ " nom " ] , " agir " , " Sa fiche est à redéposer " ,
" Son inventaire se génère sur d ' anciennes valeurs du site. " ,
" · " . join ( l [ " fiche " ] [ " ecarts " ] ) ,
{ " libelle " : " Assistant : tenir un site en état " , " runbook " : " site-tenir " } ) )
elif l [ " relation " ] == " actif " :
out . append ( _element ( l [ " nom " ] , " ok " , " Face lue, fiche à jour " , " " ,
f " face { ( l [ ' face ' ] [ ' trace ' ] or { } ) . get ( ' commit ' ) } · fiche { ( l [ ' fiche ' ] [ ' trace ' ] or { } ) . get ( ' commit ' ) } " ) )
if R and R . get ( " planifiees " ) :
out . append ( _element ( l [ " nom " ] , " savoir " , f " { len ( R [ ' planifiees ' ] ) } machine(s) prévues, pas encore actives " ,
" À créer seulement sur demande de son mainteneur (creer-vm). " ,
" , " . join ( R [ " planifiees " ] ) ) )
return out
console : un tronc, deux branches — assembler deux fois est ce qui fait deriver
Il y avait deux assembleurs de charge, un par sorte de console. Ils ont derive deux fois le
meme jour : en forme (serveurs valait [] d'un cote et {} de l'autre, et charger() levait),
puis en contenu (cinq registres servis vides alors que le site a SON plan — 9 serveurs,
21 applications, 2 bases, 1 domaine, tous invisibles).
Console assemble, une seule fois, et fixe les clefs et leurs formes. Les branches ne
decident que de ce qui leur appartient : d'ou vient le plan, d'ou vient l'inventaire, quels
pouvoirs elles portent. ConsoleLocataire configure, ConsoleSite materialise et sert
desormais son propre plan, ConsolePoste herite du locataire et sait en plus sur quelle
fabric poser.
Le role et la portee ne se confondent pas : le role est une propriete de la classe, la
portee se calcule depuis les pouvoirs. Un poste prive de la voute du site reste l'atelier du
mainteneur et n'engendre pourtant rien. Le decoupage a montre un trou aussitot : ConsolePoste
heritait du refus d'un locataire — « elle ne sait pas sur quelle fabric poser » — alors qu'il
monte la carte ; ce qui lui manque est la voute, et accuser la mauvaise absence fait chercher
au mauvais endroit.
Le jugement des assistants remonte dans le tronc : il etait ecrit dans la route qui liste ET
dans celle qui execute, et celle qui se trompe est toujours celle qui execute.
Valide : make test a 0 echec, 8 tests de rendu sous node dont deux neufs, console lancee pour
de vrai (13 serveurs, 24 applications, 17 runbooks, 127 etapes, 0 ecart). make verifier a
aussi attrape une faute que j'avais laissee passer sur depots_perimes.yml — risky-shell-pipe,
corrige. P02 et P60 restent, et P60 demande une republication du wiki.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:55:49 -04:00
def console ( inventaire : Path | None = None ) - > Console :
""" QUELLE console sert cette machine — derive de ce qu ' elle peut OUVRIR.
` docs / responsabilites - locataire - hebergeur . md ` attache chaque pouvoir a une voute :
calculer n ' en demande aucune, configurer celle du tenant, materialiser celle du site.
On ne prouve pas qu ' une voute s ' ouvre — le mot de passe se tape a l ' execution — mais
son ABSENCE est decisive et se mesure sans rien ouvrir .
"""
a_instance = INSTANCE . exists ( )
depot = _depot_hebergeur ( )
inv = inventaire or INVENTAIRE_DEFAUT
voute_tenant = ( INSTANCE / " inventories " / " principal " / " group_vars " / " all " /
" vault.yml " ) if a_instance else None
a_voute_tenant = bool ( voute_tenant and voute_tenant . is_file ( ) )
a_voute_site = bool ( depot and ( depot / " underlay.vault.yml " ) . is_file ( ) )
# LA BRANCHE SUIT LES LIENS — ce que cette machine est faite pour faire. La PORTEE,
# elle, suivra les voutes : une console de locataire sans sa voute reste une console
# de locataire (elle calcule son inventaire), et se dira « orpheline » parce qu'elle
# ne peut rien configurer. Le role et le pouvoir sont deux questions distinctes.
if a_instance and depot :
return ConsolePoste ( INSTANCE , inv , depot , a_voute_tenant , a_voute_site )
if a_instance :
return ConsoleLocataire ( INSTANCE , inv , a_voute_tenant )
if depot :
return ConsoleSite ( depot , a_voute_site )
return Console ( )
2026-06-24 20:17:46 -04:00
def inventaire_api ( path : Path ) - > dict :
console : un tronc, deux branches — assembler deux fois est ce qui fait deriver
Il y avait deux assembleurs de charge, un par sorte de console. Ils ont derive deux fois le
meme jour : en forme (serveurs valait [] d'un cote et {} de l'autre, et charger() levait),
puis en contenu (cinq registres servis vides alors que le site a SON plan — 9 serveurs,
21 applications, 2 bases, 1 domaine, tous invisibles).
Console assemble, une seule fois, et fixe les clefs et leurs formes. Les branches ne
decident que de ce qui leur appartient : d'ou vient le plan, d'ou vient l'inventaire, quels
pouvoirs elles portent. ConsoleLocataire configure, ConsoleSite materialise et sert
desormais son propre plan, ConsolePoste herite du locataire et sait en plus sur quelle
fabric poser.
Le role et la portee ne se confondent pas : le role est une propriete de la classe, la
portee se calcule depuis les pouvoirs. Un poste prive de la voute du site reste l'atelier du
mainteneur et n'engendre pourtant rien. Le decoupage a montre un trou aussitot : ConsolePoste
heritait du refus d'un locataire — « elle ne sait pas sur quelle fabric poser » — alors qu'il
monte la carte ; ce qui lui manque est la voute, et accuser la mauvaise absence fait chercher
au mauvais endroit.
Le jugement des assistants remonte dans le tronc : il etait ecrit dans la route qui liste ET
dans celle qui execute, et celle qui se trompe est toujours celle qui execute.
Valide : make test a 0 echec, 8 tests de rendu sous node dont deux neufs, console lancee pour
de vrai (13 serveurs, 24 applications, 17 runbooks, 127 etapes, 0 ecart). make verifier a
aussi attrape une faute que j'avais laissee passer sur depots_perimes.yml — risky-shell-pipe,
corrige. P02 et P60 restent, et P60 demande une republication du wiki.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:55:49 -04:00
""" La charge d ' une console de LOCATAIRE — deleguee au tronc.
Elle reste parce que le harnais et les tests l ' appellent par ce nom. Elle n ' assemble
plus rien : assembler deux fois est exactement ce qui a fait deriver la charge du
site de celle du locataire .
"""
return ConsoleLocataire ( INSTANCE , path , True ) . charge ( )
2026-06-30 10:07:17 -04:00
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
def schema_du_plan ( ) - > dict :
""" Le JSON Schema des registres, tel que `make schema` l ' a ecrit.
Lu depuis le fichier plutot que recalcule : c ' est l ' artefact VERSIONNE qui fait foi ,
et P61 refuse qu ' il soit perime. Recalculer ici laisserait l ' ecran afficher une forme
que personne n ' a relue.
"""
f = RACINE / " docs " / " audit " / " schema-plan.json "
if not f . is_file ( ) :
return { }
try :
return json . loads ( f . read_text ( encoding = " utf-8 " ) ) . get ( " registres " , { } )
except ( OSError , json . JSONDecodeError ) :
return { }
2026-06-30 10:07:17 -04:00
def _lire_yaml_dict ( path : Path ) - > dict :
if not path . exists ( ) :
return { }
with path . open ( " r " , encoding = " utf-8 " ) as fichier :
data = yaml . safe_load ( fichier ) or { }
return data if isinstance ( data , dict ) else { }
2026-06-30 16:20:18 -04:00
def instance_est_production ( path : Path ) - > bool :
""" Cette instance est-elle la PRODUCTION ? (garde-fou de prudence, pas un blocage)
2026-06-30 15:59:37 -04:00
2026-06-30 16:20:18 -04:00
Le déploiement est permis sur toute instance ( lab compris , vers son infra ) ;
ce drapeau ne sert qu ' à exiger une confirmation renforcée côté GUI quand c ' est
la prod . Repère explicite : ` setops_production ` dans ` < inventaire > / group_vars / all / ` .
Rétro - compatibilité : à défaut , ancien repère ( inventaire nommé « production » ) .
2026-06-30 15:59:37 -04:00
"""
base = path . parent / " group_vars " / " all "
fichiers = sorted ( base . glob ( " *.yml " ) ) if base . is_dir ( ) else [ ]
plat = path . parent / " group_vars " / " all.yml "
if plat . exists ( ) :
fichiers . append ( plat )
for f in fichiers :
d = _lire_yaml_dict ( f )
if isinstance ( d , dict ) and " setops_production " in d :
return bool ( d [ " setops_production " ] )
return path . resolve ( ) == INVENTAIRE_PRODUCTION
GUI : section « Fabric » — l'underlay se règle depuis la console
Tout le modèle d'underlay bâti aujourd'hui s'éditait à la main dans un YAML,
pendant que la doctrine dit qu'un sysadmin doit exploiter l'outil sans IA.
La frontière avait eu sa section ; l'underlay, non.
La section couvre les valeurs plates : switch routeur, dialecte de CLI, mode
et topologie de spanning-tree. Les listes de tables (`reseaux`, `hotes`, donc
les ports) restent hors de portée du panneau — elles demandent une vue
dédiée, comme celle des serveurs.
Le dialecte devient un intrant déclaré : il ne vivait que dans
SETOPS_DIALECTE. C'est une propriété du matériel, donc de la fabric.
Précédence : `--dialecte` > environnement > intrant déclaré > cisco.
Écriture chirurgicale plutôt que safe_dump : underlay.yml porte 23 lignes de
commentaires qui expliquent des décisions d'architecture, et un dump les
aurait effacées comme c'est arrivé à plan/applications.yml. Vérifié : trois
valeurs modifiées, 73 lignes et 23 commentaires avant comme après. Une clef
absente du fichier est refusée plutôt qu'inventée à un endroit arbitraire.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:01:00 -04:00
def _lire_fabric_a_plat ( ) - > dict :
""" Les intrants PLATS de underlay.yml, aplatis dans l ' espace de noms du panneau. """
data = _lire_yaml_dict ( FICHIER_UNDERLAY ) . get ( " underlay " ) or { }
plat = { }
for variable , chemin in CHEMINS_UNDERLAY . items ( ) :
noeud = data
for clef in chemin :
noeud = ( noeud or { } ) . get ( clef ) if isinstance ( noeud , dict ) else None
plat [ variable ] = noeud
return plat
2026-06-30 10:07:17 -04:00
def intrants_courants ( ) - > dict :
cache : dict = { }
valeurs : dict = { }
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
for variable , fcle , _classe , _section , _libelle , typ , _prop in INTRANTS_SCHEMA :
2026-06-30 10:07:17 -04:00
if fcle not in cache :
GUI : section « Fabric » — l'underlay se règle depuis la console
Tout le modèle d'underlay bâti aujourd'hui s'éditait à la main dans un YAML,
pendant que la doctrine dit qu'un sysadmin doit exploiter l'outil sans IA.
La frontière avait eu sa section ; l'underlay, non.
La section couvre les valeurs plates : switch routeur, dialecte de CLI, mode
et topologie de spanning-tree. Les listes de tables (`reseaux`, `hotes`, donc
les ports) restent hors de portée du panneau — elles demandent une vue
dédiée, comme celle des serveurs.
Le dialecte devient un intrant déclaré : il ne vivait que dans
SETOPS_DIALECTE. C'est une propriété du matériel, donc de la fabric.
Précédence : `--dialecte` > environnement > intrant déclaré > cisco.
Écriture chirurgicale plutôt que safe_dump : underlay.yml porte 23 lignes de
commentaires qui expliquent des décisions d'architecture, et un dump les
aurait effacées comme c'est arrivé à plan/applications.yml. Vérifié : trois
valeurs modifiées, 73 lignes et 23 commentaires avant comme après. Une clef
absente du fichier est refusée plutôt qu'inventée à un endroit arbitraire.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:01:00 -04:00
cache [ fcle ] = ( _lire_fabric_a_plat ( ) if fcle == " fabric "
else _lire_yaml_dict ( FICHIERS_INTRANTS [ fcle ] ) )
2026-06-30 12:36:51 -04:00
v = cache [ fcle ] . get ( variable )
if typ == " liste " :
items = v if isinstance ( v , list ) else [ ]
valeurs [ variable ] = [ str ( x ) . strip ( ) for x in items if str ( x ) . strip ( ) ]
else :
valeurs [ variable ] = " " if v is None else v
2026-06-30 10:07:17 -04:00
return valeurs
2026-07-22 21:32:42 -04:00
# --- Couverture du schema du plan par le GUI -------------------------------------
#
# Ce que les fonctions de sauvegarde JS ecrivent REELLEMENT, registre par registre.
# Source unique verifiee par `scripts/couverture_gui.py` : tout champ present dans
# un plan reel (instance ou modele) mais absent d'ici signale un trou de l'interface
# — l'operateur devrait editer le YAML a la main, ce que la doctrine refuse.
#
# A TENIR A JOUR avec sauvegarderServeurs / sauvegarderApplications /
# sauvegarderBases / sauvegarderDomaines. Le verificateur controle aussi que chaque
# champ declare ici apparait bien dans le source du GUI (garde anti-derive).
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
# CE QUE LE GUI SAIT ECRIRE — ET D'OU IL LE TIENT (2026-09-08).
#
# Cette table etait tenue A LA MAIN : quelqu'un devait penser a l'allonger quand un champ
# arrivait au plan, et P19 confrontait la liste au reel. Une copie gardee reste une copie.
#
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
# LA TABLE EST VIDE, ET C'EST LE BUT. Les SIX registres ont leur formulaire GENERE depuis
# `docs/audit/schema-plan.json` (`make schema`) : la liste se DEDUIT du schema au lieu
# d'etre recopiee ici. `bases_donnees` est sortie la premiere, la nomenclature ensuite,
# puis `serveurs_bd`, `domaines_publics`, et enfin `serveurs` et `applications`.
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
#
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
# On la garde plutot que de la supprimer : le mecanisme doit pouvoir accueillir un champ
# qu'un formulaire ecrirait sans que le schema le decrive — et le jour ou ca arrive, il
# faudra l'ECRIRE ICI, visiblement, plutot que de le laisser vivre en silence.
#
# P19 lit `champs_ecrits_par_gui()` et non cette constante : elle garde un mecanisme, pas
# une liste.
CHAMPS_ECRITS_A_LA_MAIN : dict [ str , set [ str ] ] = { }
2026-07-22 21:32:42 -04:00
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
# Les registres dont le formulaire est GENERE : leurs champs viennent du schema.
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
REGISTRES_GENERES = { " bases_donnees " , " nomenclature " , " serveurs_bd " ,
" domaines_publics " , " serveurs " , " applications " }
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
def champs_ecrits_par_gui ( ) - > dict :
""" Ce que le GUI sait ecrire : declare a la main, plus derive du schema.
Une seule fonction repond a la question , pour que P19 n ' ait pas a savoir lequel des
deux mecanismes couvre quel registre .
"""
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
def _noms ( props : dict ) - > set :
""" Les champs decrits, TABLES ET BLOCS IMBRIQUES COMPRIS.
Les plans portent ` categorie ` et ` service ` a plat , sous la table ` fonctions ` .
Ne rendre que les proprietes de premier niveau ferait dire au GUI qu ' il ne sait
pas les ecrire , alors que le formulaire genere les edite .
"""
vus = set ( )
for nom , spec in ( props or { } ) . items ( ) :
vus . add ( nom )
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
for conteneur in ( spec , spec . get ( " additionalProperties " ) , spec . get ( " items " ) ) :
if isinstance ( conteneur , dict ) and isinstance ( conteneur . get ( " properties " ) , dict ) :
vus | = _noms ( conteneur [ " properties " ] )
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
return vus
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
resultat = { nom : set ( champs ) for nom , champs in CHAMPS_ECRITS_A_LA_MAIN . items ( ) }
for nom , reg in ( schema_du_plan ( ) or { } ) . items ( ) :
if nom in REGISTRES_GENERES :
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
resultat [ nom ] = _noms ( ( reg . get ( " entite " ) or { } ) . get ( " properties " ) or { } )
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
return resultat
2026-07-22 21:32:42 -04:00
2026-06-30 12:36:51 -04:00
def integrations_disponibles ( ) - > list :
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
""" Roles client_* SELECTIONNABLES : ceux qui restent un choix.
Les universelles en sont exclues : on ne coche pas ce qui s ' applique a tout le
monde . Le GUI les affiche quand meme , en lecture seule ( voir / api / etat ) — sinon
l ' operateur lirait « aucune supervision » sur une flotte entierement supervisee.
"""
2026-06-30 12:36:51 -04:00
base = RACINE / " roles "
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
universelles = set ( integrations_universelles ( RACINE ) )
return sorted ( p . name for p in base . glob ( " client_* " )
if p . is_dir ( ) and p . name not in universelles )
2026-06-30 12:36:51 -04:00
2026-06-30 10:07:17 -04:00
def intrants_api ( ) - > dict :
schema = [ { " variable " : v , " fichier " : f , " classe " : c , " section " : s ,
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
" libelle " : l , " type " : t , " proprietaire " : p } for ( v , f , c , s , l , t , p ) in INTRANTS_SCHEMA ]
2026-06-30 10:07:17 -04:00
return {
" schema " : schema ,
" valeurs " : intrants_courants ( ) ,
2026-08-01 22:25:41 -04:00
" secrets_attendus " : secrets_attendus ( ) ,
2026-06-30 10:07:17 -04:00
" fichiers " : { k : os . path . relpath ( v , RACINE ) for k , v in FICHIERS_INTRANTS . items ( ) } ,
2026-06-24 20:17:46 -04:00
}
2026-06-30 10:07:17 -04:00
def _coercer_intrant ( typ : str , brut : object ) - > object :
2026-06-30 12:36:51 -04:00
if typ == " liste " :
items = brut if isinstance ( brut , list ) else str ( brut ) . replace ( " \n " , " , " ) . split ( " , " )
# dedoublonne en preservant l'ordre
vus , sortie = set ( ) , [ ]
for x in items :
s = str ( x ) . strip ( )
if s and s not in vus :
vus . add ( s ) ; sortie . append ( s )
return sortie
2026-06-30 10:07:17 -04:00
s = str ( brut ) . strip ( )
if typ == " bool " :
return s . lower ( ) in ( " true " , " 1 " , " oui " , " yes " , " on " )
if typ == " int " :
return int ( s ) if s . lstrip ( " - " ) . isdigit ( ) else s
return s
frontière nord/sud : devis dérivé, lien de transit et les deux routes
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>
2026-08-01 19:32:04 -04:00
_REFERENCE_VOUTE = re . compile ( r " ^ \ { \ { \ s*vault_[A-Za-z0-9_]+ \ s* \ } \ }$ " )
def _est_reference_voute ( valeur : object ) - > bool :
""" Vrai si la valeur est une simple REFERENCE de voute (` {{ vault_xxx }}`).
Un fichier d ' intrants peut porter, a cote de ses parametres anodins, des renvois
par NOM vers la voute ( cas de group_vars / opnsense . yml ) . Ce n ' est pas un secret :
c ' est un pointeur. La fusion « preserve les cles non gerees » les relit donc du
disque , et le garde - fou ne doit pas les confondre avec une valeur saisie .
"""
return bool ( _REFERENCE_VOUTE . match ( str ( valeur ) . strip ( ) ) )
GUI : le panneau d'intrants effacait la memoire ecrite du depot
Un enregistrement du panneau « Intrants de base », a 13:48, a emporte 121 lignes
d'explications dans quatre fichiers — dont celle qui disait POURQUOI la valeur qu'on
venait de changer avait ete choisie (le plan de gestion reste en 10.0.0.0/24 tant que la
frontiere ne sait pas classer un second CIDR, D-61). Ces phrases sont la seule trace de
raisonnements qu'aucun code ne redit. `safe_dump` les effacait toutes a chaque
sauvegarde, en retriant les clefs au passage.
LE DEPOT CONNAISSAIT DEJA LE GESTE JUSTE. `_ecrire_intrants_fabric` (underlay.yml) et
`_ecrire_index_nomenclature` remplacent LA LIGNE sans toucher au reste ; leur commentaire
dit meme « un safe_dump les effacerait toutes ». Les quatre fichiers d'intrants n'avaient
jamais recu ce traitement. Ce qui manquait pour l'etendre : savoir remplacer une valeur
de LISTE, qui tient sur plusieurs lignes.
`_fusion_chirurgicale`, trois regles :
- une clef dont la valeur ne change pas n'est PAS reecrite (zero bruit au diff) ;
- les commentaires internes a un bloc remplace sont conserves, jamais juges ;
- une clef absente du fichier est ajoutee a la fin, jamais inseree au hasard.
La deuxieme est assumee : une explication devenue fausse survit a la valeur qu'elle
explique. Corriger une phrase est un geste humain ; l'effacer parce qu'un champ a bouge,
non. Meme principe que la fusion des clefs posee le 2026-08-10.
EPROUVE SUR LE FICHIER REEL, pas sur un exemple : l'enregistrement de 13:48 rejoue sur la
version d'avant (tiree de git) rend 41 lignes -> 41, 30 commentaires -> 30, 0 perdu, et un
diff d'UNE ligne. Neuf tests dans scripts/tests/test_gui_intrants.py, branches sur
`make test`.
DEUX FOIS LE MEME GESTE DESTRUCTEUR, SUR LE MEME CHEMIN : le 10 aout ce panneau perdait
des CLEFS (dns_amorcage, amorcage_acces_courriel — une VM qui nait sans resolution), le
18 des COMMENTAIRES. La premiere fois avait valu une fusion, pas un test. C'est le test
qui manquait.
Non touche, et dit comme tel : les registres (bases, applications, serveurs, domaines)
passent toujours par safe_dump. Ils sont structures et n'ont pas perdu de commentaires au
meme enregistrement — a reprendre si l'un d'eux en porte un jour.
make test 9/9 sur le nouveau fichier ; prouver 37 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:05:12 -04:00
_CLE_RACINE = re . compile ( r " ^([A-Za-z_][A-Za-z0-9_]*): " )
def _rendre_cle ( cle : str , valeur : object ) - > list [ str ] :
""" Les lignes que PyYAML ecrirait pour cette seule clef (listes en bloc incluses). """
texte = yaml . safe_dump ( { cle : valeur } , default_flow_style = False ,
allow_unicode = True , sort_keys = False )
return texte . rstrip ( " \n " ) . split ( " \n " )
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
_ENTREE_TABLE = re . compile ( r " ^( \ s+)([^ \ s#:][^:]*):( \ s*)(.*)$ " )
def _fusion_table ( bloc : list [ str ] , entrees : dict ) - > list [ str ] :
""" Met a jour une table clef->mapping LIGNE A LIGNE, sans la reecrire.
POURQUOI PAS ` _fusion_chirurgicale ` SEULE ( mesure du 2026 - 09 - 08 ) . Elle remplace le
BLOC entier des qu ' une valeur change. Sur `fonctions:` de la nomenclature, ajouter
une seule entree a produit deux degats mesures :
- les quinze entrees compactes ` { categorie : 6 , service : 2 } ` sont devenues
quarante - deux lignes — le fichier cesse d ' etre une table lisible d ' un coup d ' oeil ;
- le commentaire « LE POSTE D ' EXPLOITATION », qui coiffait `ops:`, s ' est retrouve
en TETE du bloc . Un commentaire deplace ne se contente pas d ' etre laid : il
explique desormais la mauvaise ligne . Il affirmait que ` collab ` etait le poste
d ' exploitation.
Ici on ne touche qu ' aux LIGNES concernees. Une entree inchangee n ' est pas relue , un
commentaire garde son voisin , et le style compact survit parce qu ' on ne le refait pas.
"""
tete , corps = bloc [ 0 ] , bloc [ 1 : ]
sortie , vues = [ tete ] , set ( )
flow , colonne = 0 , 0
2026-09-28 13:17:52 -04:00
# SEULS LES ENFANTS DIRECTS SONT DES ENTREES (2026-09-28). Une ligne plus indentee
# appartient a une entree ; la tester contre `_ENTREE_TABLE` la prenait pour une entree
# a part : ` - {nom: "@", type: A, ...}` donnait la clef `- {nom`, dont le YAML est
# une LISTE — et `.get` echouait (P02, depuis la zone signee du 2026-09-16).
enfant = next ( ( m . group ( 1 ) for m in map ( _ENTREE_TABLE . match , corps ) if m ) , None )
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
i = 0
while i < len ( corps ) :
ligne = corps [ i ]
m = _ENTREE_TABLE . match ( ligne )
2026-09-28 13:17:52 -04:00
if not m or m . group ( 1 ) != enfant :
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
sortie . append ( ligne )
i + = 1
continue
indent , cle , ecart , reste = m . group ( 1 ) , m . group ( 2 ) . strip ( ) , m . group ( 3 ) , m . group ( 4 )
2026-09-28 13:17:52 -04:00
# L'entree s'etend sur les lignes PLUS indentees qui suivent — Y COMPRIS a travers
# une ligne vide ou un commentaire, tant que la prochaine ligne SIGNIFICATIVE est
# encore plus indentee. S'arreter au premier commentaire coupait `chezlepro.ca:` en
# deux, a l'endroit ou il explique sa signature DNSSEC. Un commentaire suivi de
# l'entree SUIVANTE, lui, reste avec elle : il la coiffe.
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
fin = i
j = i + 1
while j < len ( corps ) :
suite = corps [ j ]
if not suite . strip ( ) or suite . strip ( ) . startswith ( " # " ) :
2026-09-28 13:17:52 -04:00
k = j + 1
while k < len ( corps ) and ( not corps [ k ] . strip ( ) or corps [ k ] . strip ( ) . startswith ( " # " ) ) :
k + = 1
if k < len ( corps ) and len ( corps [ k ] ) - len ( corps [ k ] . lstrip ( ) ) > len ( indent ) :
j = k
continue
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
break
creux = len ( suite ) - len ( suite . lstrip ( ) )
if creux < = len ( indent ) :
break
fin = j
j + = 1
vues . add ( cle )
if reste . startswith ( " { " ) :
flow + = 1
colonne = max ( colonne , len ( indent ) + len ( cle ) + 1 + len ( ecart ) )
try :
ancien = ( yaml . safe_load ( " \n " . join ( l [ len ( indent ) : ] for l in corps [ i : fin + 1 ] ) ) or { } ) . get ( cle )
except yaml . YAMLError :
ancien = object ( ) # illisible : on la considere changee
if cle not in entrees :
i = fin + 1 # entree retiree : ses lignes disparaissent, pas ses voisines
continue
if ancien == entrees [ cle ] :
sortie . extend ( corps [ i : fin + 1 ] )
elif reste . startswith ( " { " ) :
sortie . append ( f " { indent } { cle } : { ecart } { _rendre_flow ( entrees [ cle ] ) } " )
else :
# L'entree tenait sur plusieurs lignes : on la refait en bloc, indentee
# comme ses voisines.
sortie . extend ( indent + l for l in _rendre_cle ( cle , entrees [ cle ] ) )
i = fin + 1
# Les entrees NEUVES vont a la fin du bloc, dans le style dominant.
neuves = [ c for c in entrees if c not in vues ]
if neuves :
indent = " "
for ligne in corps :
m = _ENTREE_TABLE . match ( ligne )
if m :
indent = m . group ( 1 )
break
while sortie and not sortie [ - 1 ] . strip ( ) :
sortie . pop ( )
for cle in neuves :
if flow :
ecart = " " * max ( 1 , colonne - len ( indent ) - len ( str ( cle ) ) - 1 )
sortie . append ( f " { indent } { cle } : { ecart } { _rendre_flow ( entrees [ cle ] ) } " )
else :
sortie . extend ( indent + l for l in _rendre_cle ( cle , entrees [ cle ] ) )
return sortie
def _rendre_flow ( valeur : object ) - > str :
""" La forme compacte ` { a: 1, b: 2 }` — celle que le fichier employait deja. """
rendu = yaml . safe_dump ( valeur , default_flow_style = True , allow_unicode = True ,
sort_keys = False , width = 10 * * 6 ) . strip ( )
if rendu . startswith ( " { " ) and rendu . endswith ( " } " ) :
rendu = " { " + rendu [ 1 : - 1 ] . strip ( ) + " } "
return rendu
def _fusion_chirurgicale ( texte : str , valeurs : dict , tables : set | None = None ) - > str :
GUI : le panneau d'intrants effacait la memoire ecrite du depot
Un enregistrement du panneau « Intrants de base », a 13:48, a emporte 121 lignes
d'explications dans quatre fichiers — dont celle qui disait POURQUOI la valeur qu'on
venait de changer avait ete choisie (le plan de gestion reste en 10.0.0.0/24 tant que la
frontiere ne sait pas classer un second CIDR, D-61). Ces phrases sont la seule trace de
raisonnements qu'aucun code ne redit. `safe_dump` les effacait toutes a chaque
sauvegarde, en retriant les clefs au passage.
LE DEPOT CONNAISSAIT DEJA LE GESTE JUSTE. `_ecrire_intrants_fabric` (underlay.yml) et
`_ecrire_index_nomenclature` remplacent LA LIGNE sans toucher au reste ; leur commentaire
dit meme « un safe_dump les effacerait toutes ». Les quatre fichiers d'intrants n'avaient
jamais recu ce traitement. Ce qui manquait pour l'etendre : savoir remplacer une valeur
de LISTE, qui tient sur plusieurs lignes.
`_fusion_chirurgicale`, trois regles :
- une clef dont la valeur ne change pas n'est PAS reecrite (zero bruit au diff) ;
- les commentaires internes a un bloc remplace sont conserves, jamais juges ;
- une clef absente du fichier est ajoutee a la fin, jamais inseree au hasard.
La deuxieme est assumee : une explication devenue fausse survit a la valeur qu'elle
explique. Corriger une phrase est un geste humain ; l'effacer parce qu'un champ a bouge,
non. Meme principe que la fusion des clefs posee le 2026-08-10.
EPROUVE SUR LE FICHIER REEL, pas sur un exemple : l'enregistrement de 13:48 rejoue sur la
version d'avant (tiree de git) rend 41 lignes -> 41, 30 commentaires -> 30, 0 perdu, et un
diff d'UNE ligne. Neuf tests dans scripts/tests/test_gui_intrants.py, branches sur
`make test`.
DEUX FOIS LE MEME GESTE DESTRUCTEUR, SUR LE MEME CHEMIN : le 10 aout ce panneau perdait
des CLEFS (dns_amorcage, amorcage_acces_courriel — une VM qui nait sans resolution), le
18 des COMMENTAIRES. La premiere fois avait valu une fusion, pas un test. C'est le test
qui manquait.
Non touche, et dit comme tel : les registres (bases, applications, serveurs, domaines)
passent toujours par safe_dump. Ils sont structures et n'ont pas perdu de commentaires au
meme enregistrement — a reprendre si l'un d'eux en porte un jour.
make test 9/9 sur le nouveau fichier ; prouver 37 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:05:12 -04:00
""" Pose les valeurs dans le texte existant SANS reformater ce qui n ' a pas change.
POURQUOI ( mesure du 2026 - 08 - 18 ) . Ces fichiers portent la memoire ECRITE des
decisions : pourquoi ` dns_amorcage ` pointe sur un resolveur public , pourquoi le
plan de gestion est reste en 10.0 .0 .0 / 24 , pourquoi la frontiere ecoute sur ` lan ` .
Chaque enregistrement du panneau les repassait au ` safe_dump ` : clefs retriees ,
et TOUS les commentaires effaces . Un enregistrement de 13 : 48 a ainsi emporte
2026-08-18 15:06:03 -04:00
94 lignes de commentaire dans quatre fichiers ( 129 - > 35 , le reste etant l ' entete
que le panneau reecrit ) — dont celle qui disait pourquoi la valeur qu ' on venait
de changer avait ete choisie .
GUI : le panneau d'intrants effacait la memoire ecrite du depot
Un enregistrement du panneau « Intrants de base », a 13:48, a emporte 121 lignes
d'explications dans quatre fichiers — dont celle qui disait POURQUOI la valeur qu'on
venait de changer avait ete choisie (le plan de gestion reste en 10.0.0.0/24 tant que la
frontiere ne sait pas classer un second CIDR, D-61). Ces phrases sont la seule trace de
raisonnements qu'aucun code ne redit. `safe_dump` les effacait toutes a chaque
sauvegarde, en retriant les clefs au passage.
LE DEPOT CONNAISSAIT DEJA LE GESTE JUSTE. `_ecrire_intrants_fabric` (underlay.yml) et
`_ecrire_index_nomenclature` remplacent LA LIGNE sans toucher au reste ; leur commentaire
dit meme « un safe_dump les effacerait toutes ». Les quatre fichiers d'intrants n'avaient
jamais recu ce traitement. Ce qui manquait pour l'etendre : savoir remplacer une valeur
de LISTE, qui tient sur plusieurs lignes.
`_fusion_chirurgicale`, trois regles :
- une clef dont la valeur ne change pas n'est PAS reecrite (zero bruit au diff) ;
- les commentaires internes a un bloc remplace sont conserves, jamais juges ;
- une clef absente du fichier est ajoutee a la fin, jamais inseree au hasard.
La deuxieme est assumee : une explication devenue fausse survit a la valeur qu'elle
explique. Corriger une phrase est un geste humain ; l'effacer parce qu'un champ a bouge,
non. Meme principe que la fusion des clefs posee le 2026-08-10.
EPROUVE SUR LE FICHIER REEL, pas sur un exemple : l'enregistrement de 13:48 rejoue sur la
version d'avant (tiree de git) rend 41 lignes -> 41, 30 commentaires -> 30, 0 perdu, et un
diff d'UNE ligne. Neuf tests dans scripts/tests/test_gui_intrants.py, branches sur
`make test`.
DEUX FOIS LE MEME GESTE DESTRUCTEUR, SUR LE MEME CHEMIN : le 10 aout ce panneau perdait
des CLEFS (dns_amorcage, amorcage_acces_courriel — une VM qui nait sans resolution), le
18 des COMMENTAIRES. La premiere fois avait valu une fusion, pas un test. C'est le test
qui manquait.
Non touche, et dit comme tel : les registres (bases, applications, serveurs, domaines)
passent toujours par safe_dump. Ils sont structures et n'ont pas perdu de commentaires au
meme enregistrement — a reprendre si l'un d'eux en porte un jour.
make test 9/9 sur le nouveau fichier ; prouver 37 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:05:12 -04:00
Le depot connaissait deja le geste juste : ` _ecrire_intrants_fabric ` et
` _ecrire_index_nomenclature ` remplacent la ligne sans toucher au reste . Ce qui
manquait ici , c ' est de savoir remplacer une valeur de LISTE, qui tient sur
plusieurs lignes .
Trois regles :
- une clef dont la valeur ne change pas n ' est PAS reecrite (zero bruit au diff) ;
- les commentaires INTERNES a un bloc remplace sont conserves , jamais juges : une
explication devenue fausse se corrige a la main , elle ne se supprime pas toute
seule ;
- une clef absente du fichier est ajoutee a la fin , jamais inseree au hasard .
"""
actuel = yaml . safe_load ( texte )
actuel = actuel if isinstance ( actuel , dict ) else { }
lignes = texte . split ( " \n " )
sortie : list [ str ] = [ ]
vues : set [ str ] = set ( )
i = 0
while i < len ( lignes ) :
m = _CLE_RACINE . match ( lignes [ i ] )
cle = m . group ( 1 ) if m else None
if cle is None or cle not in valeurs :
sortie . append ( lignes [ i ] )
i + = 1
continue
# Le bloc de valeur : la ligne de clef, puis tout ce qui est indente ou item de
# liste. Les commentaires et lignes vides qui SUIVENT la derniere ligne de valeur
# appartiennent a la clef suivante — on les laisse ou ils sont.
fin = i
j = i + 1
while j < len ( lignes ) :
l = lignes [ j ]
if l . startswith ( ( " " , " \t " , " - " ) ) or l == " - " :
fin = j
elif l . strip ( ) . startswith ( " # " ) or not l . strip ( ) :
pass # peut etre interne au bloc : on tranche avec `fin`
else :
break
j + = 1
vues . add ( cle )
if cle in actuel and actuel [ cle ] == valeurs [ cle ] :
sortie . extend ( lignes [ i : fin + 1 ] ) # inchangee : pas un octet touche
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
elif cle in ( tables or set ( ) ) and isinstance ( valeurs [ cle ] , dict ) :
# Table clef->mapping : on edite les lignes concernees, pas le bloc.
sortie . extend ( _fusion_table ( lignes [ i : fin + 1 ] , valeurs [ cle ] ) )
GUI : le panneau d'intrants effacait la memoire ecrite du depot
Un enregistrement du panneau « Intrants de base », a 13:48, a emporte 121 lignes
d'explications dans quatre fichiers — dont celle qui disait POURQUOI la valeur qu'on
venait de changer avait ete choisie (le plan de gestion reste en 10.0.0.0/24 tant que la
frontiere ne sait pas classer un second CIDR, D-61). Ces phrases sont la seule trace de
raisonnements qu'aucun code ne redit. `safe_dump` les effacait toutes a chaque
sauvegarde, en retriant les clefs au passage.
LE DEPOT CONNAISSAIT DEJA LE GESTE JUSTE. `_ecrire_intrants_fabric` (underlay.yml) et
`_ecrire_index_nomenclature` remplacent LA LIGNE sans toucher au reste ; leur commentaire
dit meme « un safe_dump les effacerait toutes ». Les quatre fichiers d'intrants n'avaient
jamais recu ce traitement. Ce qui manquait pour l'etendre : savoir remplacer une valeur
de LISTE, qui tient sur plusieurs lignes.
`_fusion_chirurgicale`, trois regles :
- une clef dont la valeur ne change pas n'est PAS reecrite (zero bruit au diff) ;
- les commentaires internes a un bloc remplace sont conserves, jamais juges ;
- une clef absente du fichier est ajoutee a la fin, jamais inseree au hasard.
La deuxieme est assumee : une explication devenue fausse survit a la valeur qu'elle
explique. Corriger une phrase est un geste humain ; l'effacer parce qu'un champ a bouge,
non. Meme principe que la fusion des clefs posee le 2026-08-10.
EPROUVE SUR LE FICHIER REEL, pas sur un exemple : l'enregistrement de 13:48 rejoue sur la
version d'avant (tiree de git) rend 41 lignes -> 41, 30 commentaires -> 30, 0 perdu, et un
diff d'UNE ligne. Neuf tests dans scripts/tests/test_gui_intrants.py, branches sur
`make test`.
DEUX FOIS LE MEME GESTE DESTRUCTEUR, SUR LE MEME CHEMIN : le 10 aout ce panneau perdait
des CLEFS (dns_amorcage, amorcage_acces_courriel — une VM qui nait sans resolution), le
18 des COMMENTAIRES. La premiere fois avait valu une fusion, pas un test. C'est le test
qui manquait.
Non touche, et dit comme tel : les registres (bases, applications, serveurs, domaines)
passent toujours par safe_dump. Ils sont structures et n'ont pas perdu de commentaires au
meme enregistrement — a reprendre si l'un d'eux en porte un jour.
make test 9/9 sur le nouveau fichier ; prouver 37 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:05:12 -04:00
else :
internes = [ l for l in lignes [ i + 1 : fin + 1 ] if l . strip ( ) . startswith ( " # " ) ]
rendu = _rendre_cle ( cle , valeurs [ cle ] )
sortie . extend ( [ rendu [ 0 ] ] + internes + rendu [ 1 : ] )
i = fin + 1
ajouts = [ c for c in valeurs if c not in vues ]
if ajouts :
while sortie and not sortie [ - 1 ] . strip ( ) :
sortie . pop ( )
for cle in ajouts :
sortie . extend ( _rendre_cle ( cle , valeurs [ cle ] ) )
sortie . append ( " " )
return " \n " . join ( sortie )
2026-06-30 10:07:17 -04:00
def _ecrire_intrants_fichier ( path : Path , valeurs : dict , entete : str ) - > None :
frontière nord/sud : devis dérivé, lien de transit et les deux routes
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>
2026-08-01 19:32:04 -04:00
# Une cle sensible n'est toleree que sous forme de reference de voute preservee.
# Toute VALEUR reelle sur une de ces cles fait echouer l'ecriture.
interdits = sorted ( cle for cle in set ( valeurs ) & INTRANTS_CLES_INTERDITES
if not _est_reference_voute ( valeurs [ cle ] ) )
2026-06-30 10:07:17 -04:00
if interdits :
raise ValueError ( f " Refus d ' ecrire des cles sensibles via le GUI: { ' , ' . join ( interdits ) } " )
path . parent . mkdir ( parents = True , exist_ok = True )
GUI : le panneau d'intrants effacait la memoire ecrite du depot
Un enregistrement du panneau « Intrants de base », a 13:48, a emporte 121 lignes
d'explications dans quatre fichiers — dont celle qui disait POURQUOI la valeur qu'on
venait de changer avait ete choisie (le plan de gestion reste en 10.0.0.0/24 tant que la
frontiere ne sait pas classer un second CIDR, D-61). Ces phrases sont la seule trace de
raisonnements qu'aucun code ne redit. `safe_dump` les effacait toutes a chaque
sauvegarde, en retriant les clefs au passage.
LE DEPOT CONNAISSAIT DEJA LE GESTE JUSTE. `_ecrire_intrants_fabric` (underlay.yml) et
`_ecrire_index_nomenclature` remplacent LA LIGNE sans toucher au reste ; leur commentaire
dit meme « un safe_dump les effacerait toutes ». Les quatre fichiers d'intrants n'avaient
jamais recu ce traitement. Ce qui manquait pour l'etendre : savoir remplacer une valeur
de LISTE, qui tient sur plusieurs lignes.
`_fusion_chirurgicale`, trois regles :
- une clef dont la valeur ne change pas n'est PAS reecrite (zero bruit au diff) ;
- les commentaires internes a un bloc remplace sont conserves, jamais juges ;
- une clef absente du fichier est ajoutee a la fin, jamais inseree au hasard.
La deuxieme est assumee : une explication devenue fausse survit a la valeur qu'elle
explique. Corriger une phrase est un geste humain ; l'effacer parce qu'un champ a bouge,
non. Meme principe que la fusion des clefs posee le 2026-08-10.
EPROUVE SUR LE FICHIER REEL, pas sur un exemple : l'enregistrement de 13:48 rejoue sur la
version d'avant (tiree de git) rend 41 lignes -> 41, 30 commentaires -> 30, 0 perdu, et un
diff d'UNE ligne. Neuf tests dans scripts/tests/test_gui_intrants.py, branches sur
`make test`.
DEUX FOIS LE MEME GESTE DESTRUCTEUR, SUR LE MEME CHEMIN : le 10 aout ce panneau perdait
des CLEFS (dns_amorcage, amorcage_acces_courriel — une VM qui nait sans resolution), le
18 des COMMENTAIRES. La premiere fois avait valu une fusion, pas un test. C'est le test
qui manquait.
Non touche, et dit comme tel : les registres (bases, applications, serveurs, domaines)
passent toujours par safe_dump. Ils sont structures et n'ont pas perdu de commentaires au
meme enregistrement — a reprendre si l'un d'eux en porte un jour.
make test 9/9 sur le nouveau fichier ; prouver 37 OK, 0 echec, 0 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:05:12 -04:00
if path . exists ( ) :
# Fichier existant : on pose les valeurs DANS son texte (commentaires preserves).
path . write_text ( _fusion_chirurgicale ( path . read_text ( encoding = " utf-8 " ) , valeurs ) ,
encoding = " utf-8 " )
return
plan : l ecriture des registres devient atomique — tout, ou rien
`path.open("w")` TRONQUE avant d ecrire : entre les deux, le fichier est vide.
Une exception dans yaml.safe_dump, un disque plein, un Ctrl-C, et
instance/plan/serveurs.yml reste mutile.
L asymetrie fait la gravite : hosts.yml se regenere d un
make instancier-appliquer, le PLAN ne se regenere de rien. C est la source
unique de verite. Git est le filet, mais encore faut-il savoir qu on est tombe.
TREIZE SITES, UNE SEULE FONCTION
Le defaut n etait pas dans le GUI seul : douze sites dans sept fichiers, dont
les miroirs CLI des MEMES registres. Corriger le GUI seul aurait recree la
divergence que P41 garde depuis les neuf resolutions d instance. La fonction
vit donc dans inventory_rules.py, que les sept importaient deja. Une source,
pas douze.
TROIS DETAILS QUI FONT LA DIFFERENCE ENTRE « CA MARCHE » ET « CA TIENT »
temporaire dans le MEME dossier os.replace n est atomique qu au sein d un
meme systeme de fichiers ; un /tmp sur une
autre partition casserait la garantie sans
rien dire
fsync AVANT le rename sinon le renommage peut atteindre le disque
avant le contenu : au retour d une coupure
brutale, un fichier neuf et VIDE — le defaut
qu on ferme, deplace d un cran
report des droits mkstemp cree en 0600, le plan est en 0664 et
doit rester lisible par le groupe sur les
runners
LE TEST PORTE SON PROPRE CONTROLE NEGATIF
scripts/tests/test_ecriture_atomique.py rejoue D ABORD l ancienne forme et
verifie qu elle DETRUIT. Sans ce controle, « le fichier est intact » ne
prouverait rien — il pourrait l etre parce que rien n a ete ecrit du tout. Une
garantie qu on n a jamais vue echouer n est pas une garantie, c est une
habitude.
Branche sur P02, dont le titre annoncait « inventory_host » alors qu il lance
maintenant trois tests. Corrige au passage.
LA VOUTE DU GUI : VERIFIEE, PAS DE DEFAUT
Le soupcon etait qu executer_flux pose ANSIBLE_VAULT_PASSWORD_FILE (un seul mot
de passe) alors que creer une VM ouvre DEUX voutes depuis « une voute, une cle ».
Eprouve contre deux voutes JETABLES a mots de passe distincts — jamais les
vraies. Les deux variables se CUMULENT : Ansible essaie tous les secrets, et un
PASSWORD_FILE errone n empeche rien. Confirme en sondant l environnement qu une
recette make recoit reellement : le mot de passe saisi ET les cinq cles
calculees par voutes.py.
Ce qui sauve ce chemin n est donc pas le mot de passe saisi, c est
l IDENTITY_LIST que make pose par-dessus. Chacun couvre ce que l autre ne
couvre pas — le PASSWORD_FILE sert le runner qui n a que sa cle, l IDENTITY_LIST
le poste qui les a toutes. Ecrit au-dessus du code, pour que personne ne
« simplifie » en retirant l un des deux.
make prouver : CONFORME, 59 OK, 0 echec, 1 saute.
make instancier : DIFF VIDE, quatre registres relus, droits 664 preserves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 14:57:57 -04:00
with ecriture_atomique ( path ) as fichier :
2026-06-30 10:07:17 -04:00
fichier . write ( entete )
yaml . safe_dump ( valeurs , fichier , default_flow_style = False , sort_keys = True , allow_unicode = True )
def ecrire_intrants ( payload : dict ) - > None :
valeurs = payload . get ( " valeurs " , { } ) or { }
frontière nord/sud : devis dérivé, lien de transit et les deux routes
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>
2026-08-01 19:32:04 -04:00
# Garde-fou en entree : le panneau n'affiche jamais ces cles, mais une requete
# forgee pourrait les soumettre. On refuse bruyamment plutot que d'ignorer.
soumis = sorted ( set ( valeurs ) & INTRANTS_CLES_INTERDITES )
if soumis :
raise ValueError ( f " Cles sensibles soumises au panneau (voute uniquement): { ' , ' . join ( soumis ) } " )
2026-06-30 10:07:17 -04:00
if not str ( valeurs . get ( " domaine_interne " , " " ) ) . strip ( ) :
raise ValueError ( " domaine_interne ne peut pas etre vide (cle de voute de l ' ecosysteme). " )
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
types = { v : t for ( v , f , c , s , l , t , p ) in INTRANTS_SCHEMA }
fichier_de = { v : f for ( v , f , c , s , l , t , p ) in INTRANTS_SCHEMA }
2026-06-30 10:07:17 -04:00
par_fichier : dict = { }
for variable , valeur in valeurs . items ( ) :
if variable not in fichier_de :
continue
par_fichier . setdefault ( fichier_de [ variable ] , { } ) [ variable ] = _coercer_intrant ( types [ variable ] , valeur )
if " identite " in par_fichier :
portabilite : monter un SECOND tenant revele trois defauts invisibles
Les deux reconstructions de la semaine rebatissaient Chezlepro sur son propre
materiel : une preuve de reproductibilite, pas de portabilite. La vraie
epreuve est un second tenant — Technolibre, index 11, plan distinct, voute
separee, meme cluster.
1. LE CLONAGE RESOLVAIT PAR NOM. proxmox_kvm cherche une VM portant le `name`
demande ; s'il en trouve une il rend `ok` et ne clone RIEN — aucune tache
cote cluster. Or les noms courts sont VOLONTAIREMENT identiques d'un tenant a
l'autre. `backup-01` de Technolibre tombait sur celui de Chezlepro. Mesure :
id-ldap-01 et sup-01 (noms absents chez Chezlepro) passaient du premier coup,
backup-01 echouait toujours. Le clonage cible desormais par VMID, via l'API.
Deux defauts de ce correctif, trouves en le mesurant : le corps assemble en
Jinja avec `>-` rendait une CHAINE et perdait le `pool` (VM nee hors de son
pool, sans un mot) ; et l'application du gabarit de calcul expirait a 5 s,
laissant la VM aux valeurs du gabarit — 2 coeurs/2 Go au lieu du plan, en
silence. Corps en mapping YAML avec omit ; six tentatives espacees.
Et `no_log` a masque la cause au moment ou elle servait. Les deux attentes
disent maintenant ce qu'elles ont constate.
2. LE GUI DETRUISAIT DES INTRANTS. Dans ecrire_intrants, `identite` etait la
seule branche sur quatre a ecrire par-dessus le disque au lieu de fusionner.
Un enregistrement a supprime dns_amorcage et amorcage_acces_courriel. Le meme
geste sur Chezlepro aurait mange les memes cles.
3. LE VERROU DE `raser` N'ETAIT PROUVE QUE POUR UN TENANT. Le faux cluster
codait en dur les VMID de Chezlepro ; monte ailleurs, le test rendait 0 au
lieu de 2. Il fabrique desormais la collision sur le plan courant.
Verifie : prouver.py 34 OK sur Technolibre, test_raser vert sur les deux
tenants, ansible-lint production sur le playbook de clonage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 11:18:14 -04:00
# Fusion, comme les trois autres fichiers. C'etait la SEULE branche a ecrire
# par-dessus le disque, et elle perdait donc en silence tout intrant absent du
# schema du panneau. Mesure du 2026-08-10, au pre-vol de l'epreuve de
# portabilite : un enregistrement du panneau a supprime `dns_amorcage` et
# `amorcage_acces_courriel` de Technolibre. Le premier laisse une VM naitre sans
# resolution — `apt` ne peut alors rien installer ; le second arrete le
# deploiement sur la garde de `amorcage_acces` (D-72).
#
# Le meme geste sur Chezlepro aurait mange les memes cles. Un panneau qui ne
# connait pas une valeur ne doit pas avoir le droit de la detruire.
fusion = _lire_yaml_dict ( INTRANTS_IDENTITE ) # preserve les cles non gerees
fusion . update ( par_fichier [ " identite " ] )
2026-06-30 10:07:17 -04:00
_ecrire_intrants_fichier (
portabilite : monter un SECOND tenant revele trois defauts invisibles
Les deux reconstructions de la semaine rebatissaient Chezlepro sur son propre
materiel : une preuve de reproductibilite, pas de portabilite. La vraie
epreuve est un second tenant — Technolibre, index 11, plan distinct, voute
separee, meme cluster.
1. LE CLONAGE RESOLVAIT PAR NOM. proxmox_kvm cherche une VM portant le `name`
demande ; s'il en trouve une il rend `ok` et ne clone RIEN — aucune tache
cote cluster. Or les noms courts sont VOLONTAIREMENT identiques d'un tenant a
l'autre. `backup-01` de Technolibre tombait sur celui de Chezlepro. Mesure :
id-ldap-01 et sup-01 (noms absents chez Chezlepro) passaient du premier coup,
backup-01 echouait toujours. Le clonage cible desormais par VMID, via l'API.
Deux defauts de ce correctif, trouves en le mesurant : le corps assemble en
Jinja avec `>-` rendait une CHAINE et perdait le `pool` (VM nee hors de son
pool, sans un mot) ; et l'application du gabarit de calcul expirait a 5 s,
laissant la VM aux valeurs du gabarit — 2 coeurs/2 Go au lieu du plan, en
silence. Corps en mapping YAML avec omit ; six tentatives espacees.
Et `no_log` a masque la cause au moment ou elle servait. Les deux attentes
disent maintenant ce qu'elles ont constate.
2. LE GUI DETRUISAIT DES INTRANTS. Dans ecrire_intrants, `identite` etait la
seule branche sur quatre a ecrire par-dessus le disque au lieu de fusionner.
Un enregistrement a supprime dns_amorcage et amorcage_acces_courriel. Le meme
geste sur Chezlepro aurait mange les memes cles.
3. LE VERROU DE `raser` N'ETAIT PROUVE QUE POUR UN TENANT. Le faux cluster
codait en dur les VMID de Chezlepro ; monte ailleurs, le test rendait 0 au
lieu de 2. Il fabrique desormais la collision sur le plan courant.
Verifie : prouver.py 34 OK sur Technolibre, test_raser vert sur les deux
tenants, ansible-lint production sur le playbook de clonage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 11:18:14 -04:00
INTRANTS_IDENTITE , fusion ,
2026-06-30 10:07:17 -04:00
" # Intrants d ' IDENTITE de l ' instance — SOURCE UNIQUE, partagee par tous les envs. \n "
" # Edite par le panneau « Intrants de base » du GUI (make inventaire-ui). \n "
" # Reference via symlink depuis inventories/<env>/group_vars/all/10-intrants.yml. \n --- \n " )
if " proxmox " in par_fichier :
fusion = _lire_yaml_dict ( INTRANTS_PROXMOX ) # preserve les cles non gerees
fusion . update ( par_fichier [ " proxmox " ] )
_ecrire_intrants_fichier (
INTRANTS_PROXMOX , fusion ,
frontière nord/sud : devis dérivé, lien de transit et les deux routes
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>
2026-08-01 19:32:04 -04:00
" # Parametres Proxmox non sensibles. Les secrets (jetons d ' API) vont dans la \n "
" # voute UNIQUE de l ' instance : group_vars/all/vault.yml. \n "
2026-06-30 10:07:17 -04:00
" # Cles d ' intrants editees par le panneau « Intrants de base » du GUI. \n --- \n " )
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
if " proxmox_hebergeur " in par_fichier :
fusion = _lire_yaml_dict ( INTRANTS_PROXMOX_HEBERGEUR )
fusion . update ( par_fichier [ " proxmox_hebergeur " ] )
_ecrire_intrants_fichier (
INTRANTS_PROXMOX_HEBERGEUR , fusion ,
" # Le CLUSTER, vu par l ' HEBERGEUR — pas par un tenant. \n "
" # \n "
" # API du cluster, noeuds, stockages, ponts : du materiel possede par \n "
" # l ' hebergeur. Recopiees dans le group_vars de chaque tenant, ces valeurs \n "
" # avaient deja diverge — deux inventaires contradictoires du meme cluster. \n "
" # \n "
" # Ce fichier vit dans le depot de l ' HEBERGEUR, a cote d ' underlay.yml (D-14), \n "
" # et se trouve par derivation du symlink qui designe deja l ' hebergeur (D-17). \n "
" # \n "
" # Chez le TENANT restent son golden template et ses defauts de placement : \n "
" # des CHOIX faits a l ' interieur de ce que l ' hebergeur offre ici. \n "
" # \n "
" # Les secrets (jeton d ' API) n ' entrent jamais ici : voute de l ' instance. \n --- \n " )
frontière nord/sud : devis dérivé, lien de transit et les deux routes
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>
2026-08-01 19:32:04 -04:00
if " frontiere " in par_fichier :
# Fusion (comme Proxmox) : preserve les cles non gerees par le panneau,
# en particulier les references de voute `{{ vault_opnsense_api_* }}`.
fusion = _lire_yaml_dict ( INTRANTS_FRONTIERE )
fusion . update ( par_fichier [ " frontiere " ] )
_ecrire_intrants_fichier (
INTRANTS_FRONTIERE , fusion ,
" # Frontiere nord/sud (OPNsense) — parametres NON sensibles. \n "
" # Cles d ' intrants editees par le panneau « Intrants de base » du GUI. \n "
" # \n "
" # Les identifiants d ' API ne figurent ici que par REFERENCE de nom : leurs \n "
" # valeurs vivent dans la voute unique de l ' instance (ansible-vault edit \n "
" # group_vars/all/vault.yml), sous vault_opnsense_api_key / _api_secret. \n "
" # Les obtenir dans OPNsense : System > Access > Users > <utilisateur> > \n "
" # API keys > « + ». Le secret n ' est affiche qu ' a la creation. \n "
" # \n "
" # Le boitier est HORS FLOTTE Ansible : on ne fait que piloter son API REST \n "
2026-08-02 13:02:27 -04:00
" # depuis le controleur. Cf. docs/frontiere-opnsense.md. \n "
" # \n "
" # Les interfaces se nomment par leur IDENTIFIANT OPNsense (wan, opt1...), celui \n "
" # que parle l ' API REST — ni le peripherique FreeBSD (igb1), ni le libelle affiche. \n "
" # `opnsense_prochain_saut` n ' est pas ici : il DERIVE du reseau de transit de \n "
" # l ' underlay (underlay.yml, cle `passerelle_sortie`). Source unique. \n --- \n " )
GUI : section « Fabric » — l'underlay se règle depuis la console
Tout le modèle d'underlay bâti aujourd'hui s'éditait à la main dans un YAML,
pendant que la doctrine dit qu'un sysadmin doit exploiter l'outil sans IA.
La frontière avait eu sa section ; l'underlay, non.
La section couvre les valeurs plates : switch routeur, dialecte de CLI, mode
et topologie de spanning-tree. Les listes de tables (`reseaux`, `hotes`, donc
les ports) restent hors de portée du panneau — elles demandent une vue
dédiée, comme celle des serveurs.
Le dialecte devient un intrant déclaré : il ne vivait que dans
SETOPS_DIALECTE. C'est une propriété du matériel, donc de la fabric.
Précédence : `--dialecte` > environnement > intrant déclaré > cisco.
Écriture chirurgicale plutôt que safe_dump : underlay.yml porte 23 lignes de
commentaires qui expliquent des décisions d'architecture, et un dump les
aurait effacées comme c'est arrivé à plan/applications.yml. Vérifié : trois
valeurs modifiées, 73 lignes et 23 commentaires avant comme après. Une clef
absente du fichier est refusée plutôt qu'inventée à un endroit arbitraire.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:01:00 -04:00
if " fabric " in par_fichier :
_ecrire_intrants_fabric ( par_fichier [ " fabric " ] )
Adressage derive du seul seed index (rupture, mode compact retire)
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>
2026-07-23 02:58:15 -04:00
if " reseau " in par_fichier and " index " in par_fichier [ " reseau " ] :
_ecrire_index_nomenclature ( int ( par_fichier [ " reseau " ] [ " index " ] ) )
GUI : section « Fabric » — l'underlay se règle depuis la console
Tout le modèle d'underlay bâti aujourd'hui s'éditait à la main dans un YAML,
pendant que la doctrine dit qu'un sysadmin doit exploiter l'outil sans IA.
La frontière avait eu sa section ; l'underlay, non.
La section couvre les valeurs plates : switch routeur, dialecte de CLI, mode
et topologie de spanning-tree. Les listes de tables (`reseaux`, `hotes`, donc
les ports) restent hors de portée du panneau — elles demandent une vue
dédiée, comme celle des serveurs.
Le dialecte devient un intrant déclaré : il ne vivait que dans
SETOPS_DIALECTE. C'est une propriété du matériel, donc de la fabric.
Précédence : `--dialecte` > environnement > intrant déclaré > cisco.
Écriture chirurgicale plutôt que safe_dump : underlay.yml porte 23 lignes de
commentaires qui expliquent des décisions d'architecture, et un dump les
aurait effacées comme c'est arrivé à plan/applications.yml. Vérifié : trois
valeurs modifiées, 73 lignes et 23 commentaires avant comme après. Une clef
absente du fichier est refusée plutôt qu'inventée à un endroit arbitraire.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:01:00 -04:00
def _ecrire_intrants_fabric ( valeurs : dict ) - > None :
""" Met a jour les cles PLATES de underlay.yml SANS reformater le fichier.
Meme motif que ` _ecrire_index_nomenclature ` : ce fichier porte des commentaires qui
expliquent des decisions d ' architecture (pourquoi un seul routeur, pourquoi le
transit vit dans l ' underlay...). Un `safe_dump` les effacerait toutes. On remplace
donc la ligne existante , en respectant son indentation , et on n ' invente rien : une
clef absente du fichier n ' est PAS creee — elle n ' a pas de place evidente ou aller .
"""
if not FICHIER_UNDERLAY . exists ( ) :
return
texte = FICHIER_UNDERLAY . read_text ( encoding = " utf-8 " )
for variable , chemin in CHEMINS_UNDERLAY . items ( ) :
if variable not in valeurs :
continue
val = str ( valeurs [ variable ] ) . strip ( )
if not val :
continue
feuille = chemin [ - 1 ]
# Indentation attendue : 2 espaces a la racine du bloc `underlay`, 4 si imbrique.
indent = " " * len ( chemin )
motif = re . compile ( rf " (?m)^ { indent } { re . escape ( feuille ) } :[ \ t]* \ S.*$ " )
remplacement = f " { indent } { feuille } : { val } "
texte , n = motif . subn ( remplacement , texte , count = 1 )
if not n :
raise ValueError ( f " underlay.yml : clef ' { feuille } ' introuvable — "
f " l ' ajouter au fichier avant de la regler ici. " )
FICHIER_UNDERLAY . write_text ( texte , encoding = " utf-8 " )
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
def valider_nomenclature ( donnees : dict , serveurs : dict ) - > None :
""" Refuse une nomenclature qui casserait la derivation, AVANT d ' ecrire.
La nomenclature n ' est pas un registre comme les autres : elle ne decrit pas des
objets , elle decrit la REGLE dont VMID , VLAN , adresse et passerelle se derivent .
Une fonction retiree ne laisse pas un trou local — elle prive de tout adressage
chaque VM qui la porte , et ` deriver_nomenclature ` rend ` None ` SANS RIEN DIRE .
Trois refus , dans l ' ordre du degat :
- une fonction encore portee par une VM ne se retire pas ;
- une fonction ne peut designer qu ' une zone declaree ;
- ` index ` ne s ' ecrit pas ici (il est ALLOUE par le site, cf. l ' entete du fichier ) .
"""
categories = donnees . get ( " categories " ) or { }
fonctions = donnees . get ( " fonctions " ) or { }
if not isinstance ( categories , dict ) or not isinstance ( fonctions , dict ) :
raise ValueError ( " `categories` et `fonctions` doivent etre des tables. " )
connues = { str ( c ) for c in categories }
for nom , f in fonctions . items ( ) :
if not isinstance ( f , dict ) :
raise ValueError ( f " Fonction ' { nom } ' : entree invalide. " )
cat = f . get ( " categorie " )
if cat in ( None , " " ) :
raise ValueError ( f " Fonction ' { nom } ' : ' categorie ' requise. " )
if str ( cat ) not in connues :
raise ValueError ( f " Fonction ' { nom } ' : zone { cat } non declaree dans `categories` "
f " — la VM n ' aurait ni VLAN ni adresse. " )
if f . get ( " service " ) in ( None , " " ) :
raise ValueError ( f " Fonction ' { nom } ' : ' service ' requis. " )
portees = { }
for nom_vm , srv in ( serveurs or { } ) . items ( ) :
if isinstance ( srv , dict ) and srv . get ( " fonction " ) :
portees . setdefault ( str ( srv [ " fonction " ] ) , [ ] ) . append ( nom_vm )
for fonction , vms in sorted ( portees . items ( ) ) :
if fonction not in fonctions :
raise ValueError ( f " Fonction ' { fonction } ' retiree, mais encore portee par "
f " { len ( vms ) } VM ( { ' , ' . join ( sorted ( vms ) [ : 4 ] ) } ) — "
f " deplacer ces VM d ' abord. " )
zones_utilisees = { str ( f . get ( " categorie " ) ) for f in fonctions . values ( ) if isinstance ( f , dict ) }
for zone in sorted ( zones_utilisees - connues ) :
raise ValueError ( f " Zone { zone } utilisee mais non declaree. " )
def ecrire_nomenclature ( path : Path , donnees : dict ) - > None :
""" Ecrit la nomenclature SANS effacer ses commentaires.
POURQUOI PAS ` safe_dump ` . Ce fichier porte dix - neuf lignes qui disent pourquoi
` index ` EST RECU ET PAS DECIDE , et pourquoi le modele des zones est commun a toute
la flotte . Un ` safe_dump ` les emporterait toutes — c ' est exactement l ' incident du
2026 - 08 - 18 ( 94 lignes de commentaire perdues en un enregistrement ) . On repasse donc
par ` _fusion_chirurgicale ` , qui pose les valeurs dans le texte existant .
` index ` n ' est jamais du lot : il s ' ecrit par ` _ecrire_index_nomenclature ` , depuis le
panneau Intrants , parce qu ' il vient du site et non de cette vue.
"""
texte = path . read_text ( encoding = " utf-8 " ) if path . exists ( ) else " --- \n "
valeurs = { cle : donnees [ cle ] for cle in ( " cidr_hote " , " reservations " , " categories " , " fonctions " )
if cle in donnees }
with ecriture_atomique ( path ) as fichier :
fichier . write ( _fusion_chirurgicale ( texte , valeurs ,
tables = { " categories " , " fonctions " } ) )
Adressage derive du seul seed index (rupture, mode compact retire)
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>
2026-07-23 02:58:15 -04:00
def _ecrire_index_nomenclature ( index : int ) - > None :
""" Met a jour `index:` dans plan/nomenclature.yml SANS reformater le reste.
Le fichier garde ses commentaires et son formatage compact ( l ' adressage se derive
de ce seul seed ) . Remplacement de la ligne existante , ou insertion en tete a defaut .
"""
path = FICHIER_NOMENCLATURE
texte = path . read_text ( encoding = " utf-8 " ) if path . exists ( ) else " --- \n "
nouveau , remplace = re . subn ( r " (?m)^index:.*$ " , f " index: { index } " , texte )
if not remplace :
nouveau = re . sub ( r " (?m)^(--- \ n) " , rf " \ g<1>index: { index } \ n " , texte , count = 1 ) or texte
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
# Atomique comme les autres : cette ecriture-ci avait ete oubliee au passage du
# 2026-09-06, et une coupure ici laisserait le seed a moitie ecrit — c'est-a-dire
# tout l'adressage de l'ecosysteme.
with ecriture_atomique ( path ) as fichier :
fichier . write ( nouveau )
2026-06-30 10:07:17 -04:00
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
def _ecrire_registre ( path : Path , entete : str , contenu : dict ) - > None :
""" Ecrit un registre du plan SANS effacer les commentaires qu ' il porte.
MESURE DU 2026 - 09 - 08 , sur les fichiers reels de cet ecosysteme :
domaines . yml 6 lignes de commentaire - > 3 ( PERD 3 )
applications . yml 27 - > 5 ( PERD 22 )
serveurs . yml 18 - > 3 ( PERD 15 )
bases - donnees . yml 4 - > 4 ( intact , il n ' en a pas)
Quarante lignes , detruites par n ' importe quel « Sauvegarder » depuis les vues
Serveurs , Applications ou Domaines . Parmi elles , celle qui explique pourquoi
` backup - 01 ` a ete RETIRE : « une supervision creuse est pire qu ' aucune — elle est
verte » . Et celle qui dit dans quel ORDRE les deux roles du runner s ' appliquent.
C ' est l ' incident du 2026 - 08 - 18 ( 94 lignes perdues dans les fichiers d ' intrants),
jamais corrige pour les registres du plan : ` _fusion_chirurgicale ` avait ete ecrite
pour les intrants seuls , et les quatre ` ecrire_ * ` sont restes au ` safe_dump ` .
Les registres du plan ont tous la meme forme — une clef racine , une table d ' entites —
donc chaque clef racine est declaree comme TABLE : ` _fusion_table ` edite alors les
lignes concernees , une entite retiree disparait , et le reste n ' est pas touche.
Un fichier qui n ' existe pas encore recoit son entete et un `safe_dump` : il n ' y a
rien a preserver .
"""
if path . exists ( ) :
texte = path . read_text ( encoding = " utf-8 " )
if texte . strip ( ) :
with ecriture_atomique ( path ) as fichier :
fichier . write ( _fusion_chirurgicale ( texte , contenu , tables = set ( contenu ) ) )
return
with ecriture_atomique ( path ) as fichier :
fichier . write ( entete )
yaml . safe_dump ( contenu , fichier , default_flow_style = False ,
sort_keys = False , allow_unicode = True )
2026-06-24 20:17:46 -04:00
def ecrire_bases ( path : Path , registre : dict ) - > None :
entete = (
" # Registre des bases de donnees Set-OPS (serveurs_bd + bases_donnees). \n "
" # Edite par make inventaire-ui ou scripts/bases_donnees.py. \n "
" # ' portee ' (application|groupe|hote) interprete ' consommateur ' ; defaut groupe. \n "
" # ' secret ' nomme une variable Ansible Vault (jamais le mot de passe). \n "
" --- \n "
)
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
_ecrire_registre ( path , entete , {
2026-06-24 20:17:46 -04:00
" serveurs_bd " : registre . get ( " serveurs_bd " , { } ) or { } ,
" bases_donnees " : registre . get ( " bases_donnees " , { } ) or { } ,
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
} )
2026-06-24 20:17:46 -04:00
def ecrire_applications ( path : Path , registre : dict ) - > None :
entete = (
" # Registre des applications Set-OPS (application = entite pivot). \n "
" # Edite par make inventaire-ui ou scripts/applications.py. \n "
" # Une VM peut porter plusieurs applications ; une base se lie a une application. \n "
2026-07-22 21:32:42 -04:00
" # ' liens ' = bindings declaratifs [ { vers, role}] resolus par scripts/instancier.py ; \n "
" # les roles acceptes sont ceux de roles/<groupe>/meta/liens.yml. \n "
2026-06-24 20:17:46 -04:00
" --- \n "
)
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
_ecrire_registre ( path , entete , { " applications " : registre . get ( " applications " , { } ) or { } } )
2026-06-24 20:17:46 -04:00
def ecrire_serveurs ( path : Path , registre : dict ) - > None :
entete = (
" # Registre des serveurs (VM) du plan Set-OPS. \n "
" # Edite par make inventaire-ui ou scripts/serveurs.py. \n "
" # VMID/IP/VLAN/passerelle sont DERIVES de la fonction via instance/plan/nomenclature.yml. \n "
" --- \n "
)
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
_ecrire_registre ( path , entete , { " serveurs " : registre . get ( " serveurs " , { } ) or { } } )
2026-06-24 20:17:46 -04:00
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
def ecrire_domaines ( path : Path , registre : dict ) - > None :
entete = (
" # Registre des domaines publics (zones DNS de l ' instance). \n "
" # Edite par make inventaire-ui ou scripts/domaines.py. \n "
" # autorite : primaire-cache | auto-heberge | delegue (label descriptif). \n "
" --- \n "
)
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
_ecrire_registre ( path , entete , { " domaines_publics " : registre . get ( " domaines_publics " , { } ) or { } } )
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
2026-07-07 13:31:02 -04:00
def _extraire_echec ( ligne : str , tache : str ) - > dict | None :
""" D ' une ligne Ansible fatal/unreachable, tire un échec en LANGAGE CLAIR
( tâche · hôte · type · message ) — pour communiquer entre humains . """
m = re . search ( r ' (?:fatal|failed): \ s* \ [([^ \ ]]+) \ ]: \ s*(FAILED!|UNREACHABLE!)? \ s*=> \ s*( \ { .* \ }) ' , ligne )
if not m :
return None
hote = m . group ( 1 )
typ = " injoignable (SSH) " if m . group ( 2 ) == " UNREACHABLE! " else " échec "
detail : dict = { }
try :
detail = json . loads ( m . group ( 3 ) )
except Exception :
pass
if detail . get ( " censored " ) :
message = " sortie masquée (no_log) — souvent une tâche qui manipule un secret "
else :
msg = str ( detail . get ( " msg " , " " ) ) . strip ( )
# Pour un échec de commande, msg est générique ; la vraie cause est dans stderr.
stderr = " "
for cle in ( " stderr " , " module_stderr " , " stdout " ) :
valeur = str ( detail . get ( cle , " " ) ) . strip ( )
if valeur :
lignes = [ x for x in valeur . splitlines ( ) if x . strip ( ) ]
stderr = lignes [ 0 ] if lignes else valeur # l'erreur mène, l'indice suit
break
if msg and msg . lower ( ) not in ( " non-zero return code " , " module failure " , " " ) :
message = msg
elif stderr :
message = stderr
else :
message = msg or " échec sans message explicite (voir le journal complet) "
message = " " . join ( str ( message ) . split ( ) ) # une ligne propre
return { " tache " : ( tache or " ? " ) . strip ( ) , " hote " : hote , " type " : typ , " message " : message [ : 400 ] }
2026-06-24 20:17:46 -04:00
def donnees_hote ( hote : dict ) - > dict :
variables : dict = { }
for cle , var , typ in CHAMPS_PROVISION :
brut = str ( hote . get ( cle , " " ) ) . strip ( )
if not brut :
continue
if typ == " int " and brut . lstrip ( " - " ) . isdigit ( ) :
variables [ var ] = int ( brut )
else :
variables [ var ] = brut
return variables
def construire_inventaire ( payload : dict , groupes_connus : list [ str ] ) - > dict :
enfants_data : dict = {
" modeles_vm " : { " hosts " : { } } ,
GROUPE_HOTES_ACTIFS : { " hosts " : { } } ,
GROUPE_HOTES_PLANIFIES : { " hosts " : { } } ,
}
for groupe in groupes_connus :
enfants_data . setdefault ( groupe , { " hosts " : { } } )
enfants_data . setdefault ( " hotes_proxmox " , { " hosts " : { } } )
for hote in payload . get ( " hotes " , [ ] ) :
nom = str ( hote . get ( " nom " , " " ) ) . strip ( )
if not nom :
continue
variables = donnees_hote ( hote )
groupe_etat = GROUPE_HOTES_ACTIFS if hote . get ( " etat " ) == " actif " else GROUPE_HOTES_PLANIFIES
enfants_data [ groupe_etat ] [ " hosts " ] [ nom ] = variables
for groupe in hote . get ( " groupes " , [ ] ) :
if groupe not in groupes_connus :
continue
enfants_data [ groupe ] [ " hosts " ] [ nom ] = { }
return { " all " : { " children " : enfants_data } }
def valider_dependances_payload ( payload : dict , dependencies : dict ) - > None :
hotes = payload . get ( " hotes " , [ ] )
active_groups : set [ str ] = set ( )
for hote in hotes :
if hote . get ( " etat " ) != " actif " :
continue
active_groups . update ( hote . get ( " groupes " , [ ] ) )
missing : list [ str ] = [ ]
for hote in hotes :
if hote . get ( " etat " ) != " actif " :
continue
nom = str ( hote . get ( " nom " , " " ) ) . strip ( )
for groupe in hote . get ( " groupes " , [ ] ) :
for required_group in dependencies . get ( groupe , { } ) . get ( " requiert_groupes_actifs " , [ ] ) :
if required_group not in active_groups :
missing . append ( f " { nom } : { groupe } requiert { required_group } actif " )
if missing :
raise ValueError ( " Dependances manquantes: " + " ; " . join ( missing ) )
def valider_payload ( payload : dict , groupes_connus : list [ str ] , dependencies : dict ) - > None :
noms : set [ str ] = set ( )
vmids : dict [ str , str ] = { }
for hote in payload . get ( " hotes " , [ ] ) :
nom = str ( hote . get ( " nom " , " " ) ) . strip ( )
if not nom :
raise ValueError ( " Un hote a un nom vide. " )
if nom in noms :
raise ValueError ( f " Hote en double: { nom } " )
noms . add ( nom )
if hote . get ( " etat " ) == " actif " and not str ( hote . get ( " adresse_ip " , " " ) ) . strip ( ) :
raise ValueError ( f " Hote actif sans adresse IP: { nom } " )
groupes = hote . get ( " groupes " , [ ] )
if not groupes :
raise ValueError ( f " Aucun groupe operationnel pour: { nom } " )
inconnus = sorted ( set ( groupes ) - set ( groupes_connus ) )
if inconnus :
raise ValueError ( f " Groupes inconnus pour { nom } : { ' , ' . join ( inconnus ) } " )
vlan = str ( hote . get ( " vlan " , " " ) ) . strip ( )
if vlan and ( not vlan . isdigit ( ) or not 1 < = int ( vlan ) < = 4094 ) :
raise ValueError ( f " VLAN invalide pour { nom } : { vlan } (attendu 1-4094) " )
cidr = str ( hote . get ( " cidr " , " " ) ) . strip ( )
if cidr and ( not cidr . isdigit ( ) or not 0 < = int ( cidr ) < = 32 ) :
raise ValueError ( f " CIDR invalide pour { nom } : { cidr } (attendu 0-32) " )
for cle , libelle in ( ( " memoire " , " Memoire " ) , ( " coeurs " , " Coeurs " ) ) :
valeur = str ( hote . get ( cle , " " ) ) . strip ( )
if valeur and ( not valeur . isdigit ( ) or int ( valeur ) < = 0 ) :
raise ValueError ( f " { libelle } invalide pour { nom } : { valeur } (entier positif attendu) " )
vmid = str ( hote . get ( " vmid " , " " ) ) . strip ( )
if not vmid :
continue
if vmid in vmids :
raise ValueError ( f " VMID en double: { vmid } pour { vmids [ vmid ] } et { nom } " )
vmids [ vmid ] = nom
valider_dependances_payload ( payload , dependencies )
HTML = r """ <!doctype html>
< html lang = " fr " >
< head >
< meta charset = " utf-8 " >
< meta name = " viewport " content = " width=device-width, initial-scale=1 " >
< title > Set - OPS · Votre artisan numérique < / title >
< style >
: root {
color - scheme : dark ;
- - fond : #0c1119;
- - fond - degrade : radial - gradient ( 1100 px 540 px at 100 % - 8 % , #142033 0, transparent 55%), var(--fond);
- - surface : #151d2b;
- - surface - 2 : #1a2333;
- - surface - 3 : #212d41;
- - texte : #e8edf6;
- - muted : #93a1bb;
- - faint : #67738c;
- - ligne : #27324a;
- - ligne - forte : #36445f;
- - primaire : #3b82f6;
- - primaire - fort : #2f6fe0;
- - teal : #2dd4bf;
- - vert : #34d399; --vert-fond:rgba(52,211,153,.13);
- - bleu : #60a5fa; --bleu-fond:rgba(96,165,250,.13);
- - rouge : #f87171; --rouge-fond:rgba(248,113,113,.13);
- - ambre : #fbbf24; --ambre-fond:rgba(251,191,36,.13);
- - ombre : 0 1 px 2 px rgba ( 0 , 0 , 0 , .35 ) , 0 10 px 30 px - 14 px rgba ( 0 , 0 , 0 , .7 ) ;
- - ombre - forte : 0 18 px 50 px - 18 px rgba ( 0 , 0 , 0 , .8 ) ;
- - rayon : 11 px ; - - rayon - sm : 8 px ;
}
* { box - sizing : border - box ; }
body {
margin : 0 ; background : var ( - - fond - degrade ) ; color : var ( - - texte ) ; min - height : 100 vh ;
font - family : ui - sans - serif , system - ui , - apple - system , " Segoe UI " , Roboto , sans - serif ;
font - size : 14 px ; line - height : 1.5 ; - webkit - font - smoothing : antialiased ;
}
: : selection { background : color - mix ( in srgb , var ( - - teal ) 35 % , transparent ) ; }
* { scrollbar - width : thin ; scrollbar - color : var ( - - ligne - forte ) transparent ; }
header {
position : sticky ; top : 0 ; z - index : 20 ;
display : flex ; align - items : center ; gap : 16 px ; flex - wrap : wrap ;
padding : 12 px 22 px ; border - bottom : 1 px solid var ( - - ligne ) ;
background : color - mix ( in srgb , var ( - - surface ) 82 % , transparent ) ;
backdrop - filter : saturate ( 150 % ) blur ( 12 px ) ;
}
. marque { display : flex ; align - items : center ; gap : 9 px ; min - width : 0 ; }
. marque . pastille { width : 9 px ; height : 9 px ; border - radius : 50 % ; background : var ( - - teal ) ; box - shadow : 0 0 0 4 px color - mix ( in srgb , var ( - - teal ) 18 % , transparent ) ; flex : none ; }
. marque . titre { display : flex ; flex - direction : column ; line - height : 1.15 ; min - width : 0 ; }
h1 { margin : 0 ; font - size : 15 px ; font - weight : 800 ; letter - spacing : - .01 em ; white - space : nowrap ; }
. slogan { font - size : 10.5 px ; color : var ( - - muted ) ; font - weight : 600 ; letter - spacing : .02 em ; white - space : nowrap ; }
. chemin { color : var ( - - muted ) ; font - size : 12 px ; font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; white - space : nowrap ; overflow : hidden ; text - overflow : ellipsis ; }
. grandit { flex : 1 1 auto ; min - width : 0 ; }
. resume { display : flex ; gap : 6 px ; flex - wrap : wrap ; }
. chip { display : inline - flex ; align - items : center ; gap : 6 px ; border : 1 px solid var ( - - ligne ) ; border - radius : 999 px ; padding : 3 px 10 px ; background : var ( - - surface - 2 ) ; color : var ( - - muted ) ; font - size : 11.5 px ; font - weight : 650 ; }
. chip b { color : var ( - - texte ) ; font - weight : 800 ; }
. pt { width : 7 px ; height : 7 px ; border - radius : 50 % ; flex : none ; }
. pt . actif { background : var ( - - vert ) ; } . pt . plan { background : var ( - - bleu ) ; } . pt . bloq { background : var ( - - rouge ) ; }
. barre { display : flex ; gap : 8 px ; align - items : center ; flex - wrap : wrap ; }
. recherche { position : relative ; }
. recherche input { padding - left : 30 px ; min - width : 180 px ; }
. recherche : : before { content : " ⌕ " ; position : absolute ; left : 10 px ; top : 50 % ; transform : translateY ( - 50 % ) ; color : var ( - - faint ) ; font - size : 16 px ; }
button {
border : 1 px solid var ( - - ligne - forte ) ; background : var ( - - surface - 2 ) ; color : var ( - - texte ) ;
padding : 8 px 13 px ; border - radius : var ( - - rayon - sm ) ; cursor : pointer ; font : inherit ; font - weight : 700 ;
transition : background .15 s , border - color .15 s , transform .05 s , box - shadow .15 s , opacity .15 s , color .15 s ;
}
button : hover : not ( : disabled ) { border - color : var ( - - teal ) ; }
button : active : not ( : disabled ) { transform : translateY ( 1 px ) ; }
button : disabled { opacity : .4 ; cursor : not - allowed ; }
button . primaire { background : var ( - - primaire ) ; color : #fff; border-color: var(--primaire); box-shadow: var(--ombre); }
button . primaire : hover : not ( : disabled ) { background : var ( - - primaire - fort ) ; border - color : var ( - - primaire - fort ) ; }
button . fantome { background : transparent ; }
button . attention { border - color : var ( - - ambre ) ; color : var ( - - ambre ) ; }
button . danger { color : var ( - - rouge ) ; border - color : transparent ; background : transparent ; }
button . danger : hover : not ( : disabled ) { background : var ( - - rouge - fond ) ; }
input , select {
width : 100 % ; border : 1 px solid var ( - - ligne - forte ) ; border - radius : var ( - - rayon - sm ) ;
padding : 8 px 10 px ; background : var ( - - surface ) ; color : var ( - - texte ) ; font : inherit ;
transition : border - color .15 s , box - shadow .15 s ;
}
input : : placeholder { color : var ( - - faint ) ; }
input : focus , select : focus { outline : none ; border - color : var ( - - teal ) ; box - shadow : 0 0 0 3 px color - mix ( in srgb , var ( - - teal ) 22 % , transparent ) ; }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
main { padding : 16 px 22 px 44 px ; display : grid ; grid - template - columns : minmax ( 0 , 1.6 fr ) minmax ( 420 px , 1 fr ) ; gap : 20 px ; align - items : start ; max - width : 1760 px ; margin : 0 auto ; }
2026-06-30 12:19:26 -04:00
. toasts { position : fixed ; top : 14 px ; right : 14 px ; z - index : 60 ; display : flex ; flex - direction : column ; gap : 8 px ; max - width : min ( 380 px , 92 vw ) ; pointer - events : none ; }
. toast { pointer - events : auto ; background : var ( - - surface - 2 ) ; border : 1 px solid var ( - - ligne - forte ) ; border - left : 3 px solid var ( - - bleu ) ; border - radius : var ( - - rayon - sm ) ; box - shadow : var ( - - ombre - forte ) ; padding : 9 px 12 px ; font - size : 13 px ; font - weight : 600 ; color : var ( - - texte ) ; display : flex ; gap : 9 px ; align - items : flex - start ; animation : toast - in .18 s ease - out ; }
. toast . ok { border - left - color : var ( - - vert ) ; } . toast . erreur { border - left - color : var ( - - rouge ) ; }
. toast . sortant { opacity : 0 ; transform : translateX ( 12 px ) ; transition : opacity .25 s , transform .25 s ; }
. toast . x { margin - left : auto ; cursor : pointer ; opacity : .5 ; font - weight : 800 ; background : none ; border : 0 ; color : inherit ; padding : 0 2 px ; font - size : 15 px ; line - height : 1 ; }
. toast . x : hover { opacity : 1 ; }
@keyframes toast - in { from { opacity : 0 ; transform : translateX ( 12 px ) ; } }
input . invalide , select . invalide { border - color : var ( - - rouge ) ! important ; box - shadow : 0 0 0 1 px var ( - - rouge ) ; }
2026-06-24 20:17:46 -04:00
. section - grille { margin - bottom : 22 px ; }
. section - tete { display : flex ; align - items : center ; gap : 10 px ; margin : 0 0 11 px ; color : var ( - - faint ) ; font - size : 11 px ; font - weight : 800 ; letter - spacing : .09 em ; text - transform : uppercase ; }
. section - tete . compte { color : var ( - - muted ) ; }
. section - tete : : after { content : " " ; height : 1 px ; background : var ( - - ligne ) ; flex : 1 ; }
. grille - cartes { display : grid ; grid - template - columns : repeat ( auto - fill , minmax ( 190 px , 1 fr ) ) ; gap : 12 px ; }
. carte {
position : relative ; text - align : left ; display : flex ; flex - direction : column ; gap : 8 px ;
background : var ( - - surface ) ; border : 1 px solid var ( - - ligne ) ; border - radius : var ( - - rayon ) ;
padding : 12 px 13 px ; cursor : pointer ; font : inherit ; color : var ( - - texte ) ;
transition : border - color .14 s , box - shadow .14 s , transform .05 s , background .14 s ;
}
. carte : : before { content : " " ; position : absolute ; left : 0 ; top : 11 px ; bottom : 11 px ; width : 3 px ; border - radius : 0 3 px 3 px 0 ; background : var ( - - bleu ) ; }
. carte . actif : : before { background : var ( - - vert ) ; }
. carte . bloque : : before { background : var ( - - rouge ) ; }
. carte : hover { border - color : var ( - - ligne - forte ) ; box - shadow : var ( - - ombre ) ; }
. carte . selectionnee { border - color : var ( - - teal ) ; box - shadow : 0 0 0 1 px var ( - - teal ) , var ( - - ombre ) ; background : var ( - - surface - 2 ) ; }
. carte - haut { display : flex ; align - items : center ; justify - content : space - between ; gap : 8 px ; }
. carte - vmid { color : var ( - - faint ) ; font - size : 11 px ; font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; }
. carte - nom { font - weight : 800 ; font - size : 14 px ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
. carte - nom . sans { color : var ( - - faint ) ; font - style : italic ; font - weight : 600 ; }
. carte - ip { color : var ( - - muted ) ; font - size : 12 px ; font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; }
. carte - grps { display : flex ; gap : 5 px ; flex - wrap : wrap ; margin - top : 1 px ; }
. tag { font - size : 10.5 px ; font - weight : 700 ; padding : 2 px 7 px ; border - radius : 999 px ; background : var ( - - surface - 3 ) ; color : var ( - - muted ) ; white - space : nowrap ; }
. tag . plus { background : transparent ; color : var ( - - faint ) ; }
. badge - statut { display : inline - flex ; align - items : center ; gap : 5 px ; border - radius : 999 px ; padding : 3 px 9 px ; font - size : 10.5 px ; font - weight : 800 ; }
. badge - statut . ok { color : var ( - - vert ) ; background : var ( - - vert - fond ) ; }
. badge - statut . attente { color : var ( - - bleu ) ; background : var ( - - bleu - fond ) ; }
. badge - statut . bloque { color : var ( - - rouge ) ; background : var ( - - rouge - fond ) ; }
aside { position : sticky ; top : 70 px ; max - height : calc ( 100 vh - 90 px ) ; overflow : auto ; }
. detail { background : var ( - - surface ) ; border : 1 px solid var ( - - ligne ) ; border - radius : var ( - - rayon ) ; box - shadow : var ( - - ombre ) ; overflow : hidden ; }
. detail . bloque { border - color : color - mix ( in srgb , var ( - - rouge ) 50 % , var ( - - ligne ) ) ; }
. detail - tete { display : flex ; align - items : center ; gap : 10 px ; padding : 14 px 16 px 12 px ; border - bottom : 1 px solid var ( - - ligne ) ; flex - wrap : wrap ; }
. detail - nom { flex : 1 1 150 px ; min - width : 0 ; }
. zone - hint { display : inline - block ; margin : 4 px 0 0 8 px ; font - size : 11 px ; font - weight : 700 ; color : var ( - - teal ) ; }
. stats { display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 64 px , 1 fr ) ) ; gap : 8 px ; padding : 14 px 16 px ; }
. stat { background : var ( - - surface - 2 ) ; border : 1 px solid var ( - - ligne ) ; border - radius : var ( - - rayon - sm ) ; padding : 9 px 10 px ; min - width : 0 ; }
. stat . v { font - size : 18 px ; font - weight : 800 ; line - height : 1.1 ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
. stat . v . vide { color : var ( - - faint ) ; }
. stat . l { font - size : 9.5 px ; text - transform : uppercase ; letter - spacing : .06 em ; color : var ( - - faint ) ; font - weight : 800 ; margin - top : 3 px ; }
. grille { display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 135 px , 1 fr ) ) ; gap : 11 px 12 px ; }
. champ { display : flex ; flex - direction : column ; gap : 4 px ; min - width : 0 ; }
. champ > span { color : var ( - - muted ) ; font - size : 11 px ; font - weight : 700 ; }
. champ . unite { color : var ( - - faint ) ; font - weight : 600 ; }
2026-06-30 10:07:17 -04:00
. champ > span . a - aide { cursor : help ; border - bottom : 1 px dotted var ( - - ligne - forte ) ; width : fit - content ; }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
. aide - modele { margin : 0 2 px 14 px ; background : var ( - - surface ) ; border : 1 px solid var ( - - ligne ) ; border - left : 3 px solid var ( - - bleu ) ; border - radius : var ( - - rayon - sm ) ; }
. aide - modele > summary { cursor : pointer ; padding : 10 px 13 px ; font - size : 12.5 px ; font - weight : 750 ; color : var ( - - bleu ) ; list - style : none ; }
. aide - modele > summary : : - webkit - details - marker { display : none ; }
. aide - modele > summary : : before { content : " ⓘ " ; }
. aide - corps { padding : 2 px 15 px 12 px ; color : var ( - - muted ) ; font - size : 12.5 px ; line - height : 1.55 ; }
. aide - corps p { margin : 6 px 0 ; } . aide - corps ul { margin : 6 px 0 ; padding - left : 18 px ; } . aide - corps li { margin : 3 px 0 ; }
. aide - corps b { color : var ( - - texte ) ; }
. aide - ro { border - top : 1 px solid var ( - - ligne ) ; padding - top : 8 px ; }
2026-06-30 12:19:26 -04:00
. detail - nom - txt { font - weight : 800 ; font - size : 17 px ; padding : 4 px 7 px ; display : inline - block ; overflow - wrap : anywhere ; }
2026-06-30 10:07:17 -04:00
#infobulle {
position : fixed ; z - index : 200 ; max - width : 260 px ; pointer - events : none ;
background : var ( - - surface - 3 ) ; color : var ( - - texte ) ;
border : 1 px solid var ( - - ligne - forte ) ; border - radius : var ( - - rayon - sm ) ;
padding : 7 px 10 px ; font - size : 12 px ; font - weight : 600 ; line - height : 1.4 ;
box - shadow : var ( - - ombre ) ; opacity : 0 ; transform : translateY ( 2 px ) ;
transition : opacity .12 s ease , transform .12 s ease ;
}
#infobulle.on { opacity: 1; transform: none; }
2026-06-24 20:17:46 -04:00
. pied { padding : 12 px 16 px ; border - top : 1 px solid var ( - - ligne ) ; display : flex ; gap : 8 px ; align - items : center ; }
. pied . grandit { flex : 1 ; }
. pied . hint - dep { color : var ( - - faint ) ; font - size : 12 px ; font - style : italic ; }
. dep - bloc { margin - top : 4 px ; }
. dep - bloc > summary { list - style : none ; cursor : pointer ; padding : 10 px 13 px ; display : flex ; align - items : center ; justify - content : space - between ; gap : 8 px ; font - weight : 800 ; font - size : 12.5 px ; background : var ( - - surface ) ; border : 1 px solid var ( - - ligne ) ; border - radius : var ( - - rayon - sm ) ; }
. dep - bloc > summary : : - webkit - details - marker { display : none ; }
. dep - bloc > summary : : after { content : " ▾ " ; color : var ( - - faint ) ; }
. dep - bloc [ open ] > summary { border - radius : var ( - - rayon - sm ) var ( - - rayon - sm ) 0 0 ; }
. dep - corps { border : 1 px solid var ( - - ligne ) ; border - top : none ; border - radius : 0 0 var ( - - rayon - sm ) var ( - - rayon - sm ) ; padding : 11 px ; display : grid ; grid - template - columns : repeat ( auto - fill , minmax ( 220 px , 1 fr ) ) ; gap : 8 px ; }
. carte - dep { border : 1 px solid var ( - - ligne ) ; border - radius : var ( - - rayon - sm ) ; padding : 9 px 10 px ; background : var ( - - surface - 2 ) ; }
. carte - dep . ok { border - color : color - mix ( in srgb , var ( - - vert ) 45 % , var ( - - ligne ) ) ; }
. carte - dep . attente { border - color : color - mix ( in srgb , var ( - - ambre ) 45 % , var ( - - ligne ) ) ; }
. carte - dep . bloque { border - color : color - mix ( in srgb , var ( - - rouge ) 50 % , var ( - - ligne ) ) ; }
. carte - dep . haut { display : flex ; align - items : center ; justify - content : space - between ; gap : 8 px ; margin - bottom : 4 px ; }
. carte - dep . nom { font - weight : 800 ; font - size : 12 px ; overflow - wrap : anywhere ; }
. carte - dep . txt { color : var ( - - muted ) ; font - size : 11 px ; line - height : 1.4 ; }
. vide { color : var ( - - muted ) ; padding : 40 px 24 px ; text - align : center ; }
. vide b { color : var ( - - texte ) ; display : block ; margin - bottom : 4 px ; font - size : 15 px ; }
. vue - bascule { display : inline - flex ; border : 1 px solid var ( - - ligne - forte ) ; border - radius : 999 px ; padding : 2 px ; background : var ( - - surface - 3 ) ; gap : 2 px ; }
. vue - bascule button { border : none ; background : transparent ; border - radius : 999 px ; padding : 6 px 13 px ; font - size : 12.5 px ; font - weight : 750 ; color : var ( - - muted ) ; box - shadow : none ; }
. vue - bascule button : hover { color : var ( - - texte ) ; background : transparent ; }
. vue - bascule button . on { background : var ( - - surface ) ; color : var ( - - teal ) ; box - shadow : var ( - - ombre ) ; }
. mono { font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; color : var ( - - teal ) ; font - size : 12 px ; }
. ch - stub { color : var ( - - ambre ) ; font - size : 11.5 px ; font - style : italic ; margin - top : 5 px ; }
. ch - bases { display : grid ; gap : 4 px ; margin : 6 px 0 0 23 px ; }
. ch - base { font - size : 11.5 px ; color : var ( - - muted ) ; overflow - wrap : anywhere ; }
. badge - bd { display : inline - block ; font - size : 10 px ; font - weight : 800 ; padding : 1 px 6 px ; border - radius : 999 px ; background : color - mix ( in srgb , var ( - - bleu ) 18 % , transparent ) ; color : var ( - - bleu ) ; }
. modale { position : fixed ; inset : 0 ; z - index : 50 ; display : flex ; align - items : center ; justify - content : center ; background : rgba ( 0 , 0 , 0 , .55 ) ; }
. modale [ hidden ] { display : none ; }
. modale - boite { background : var ( - - surface ) ; border : 1 px solid var ( - - ligne - forte ) ; border - radius : var ( - - rayon ) ; box - shadow : var ( - - ombre - forte ) ; padding : 18 px 20 px ; width : min ( 440 px , 92 vw ) ; display : grid ; gap : 12 px ; }
. modale - titre { font - weight : 800 ; font - size : 15 px ; }
. modale - texte { color : var ( - - muted ) ; font - size : 13 px ; line - height : 1.5 ; }
. modale - actions { display : flex ; justify - content : flex - end ; gap : 8 px ; margin - top : 4 px ; }
2026-06-30 10:07:17 -04:00
. modale - boite . large { width : min ( 760 px , 94 vw ) ; max - height : 88 vh ; overflow : auto ; }
. intrants - corps { display : grid ; gap : 16 px ; }
. intr - section { display : grid ; gap : 9 px ; }
. intr - tete { font - weight : 800 ; font - size : 13 px ; color : var ( - - bleu ) ; border - bottom : 1 px solid var ( - - ligne ) ; padding - bottom : 5 px ; }
. intr - grille { display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 220 px , 1 fr ) ) ; gap : 11 px ; }
. badge - classe { display : inline - block ; font - size : 9.5 px ; font - weight : 800 ; letter - spacing : .04 em ; text - transform : uppercase ; padding : 1 px 6 px ; border - radius : 999 px ; margin - left : 6 px ; vertical - align : middle ; }
. badge - classe . const { background : color - mix ( in srgb , var ( - - ambre ) 20 % , transparent ) ; color : var ( - - ambre ) ; }
. badge - classe . def { background : color - mix ( in srgb , var ( - - bleu ) 18 % , transparent ) ; color : var ( - - bleu ) ; }
2026-06-30 12:36:51 -04:00
. badge - classe . cat { background : color - mix ( in srgb , var ( - - teal ) 20 % , transparent ) ; color : var ( - - teal ) ; }
2026-06-30 16:20:18 -04:00
. badge - prod { display : inline - flex ; align - items : center ; font - size : 11 px ; font - weight : 800 ; letter - spacing : .06 em ; padding : 3 px 9 px ; border - radius : 999 px ; background : var ( - - rouge ) ; color : #fff; }
. badge - lab { display : inline - flex ; align - items : center ; font - size : 11 px ; font - weight : 800 ; letter - spacing : .04 em ; padding : 3 px 9 px ; border - radius : 999 px ; background : color - mix ( in srgb , var ( - - teal ) 22 % , transparent ) ; color : var ( - - teal ) ; }
2026-06-30 12:36:51 -04:00
. integ - cases { display : flex ; flex - wrap : wrap ; gap : 6 px 12 px ; padding : 4 px 2 px ; }
. integ - case { display : inline - flex ; align - items : center ; gap : 5 px ; font - size : 12 px ; font - weight : 600 ; color : var ( - - texte ) ; cursor : pointer ; }
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
. integ - univ { color : var ( - - vert ) ; font - weight : 700 ; cursor : help ; }
. integ - exempt { color : var ( - - faint ) ; text - decoration : line - through ; cursor : help ; }
2026-06-30 12:36:51 -04:00
. integ - case input { width : 14 px ; height : 14 px ; flex : none ; accent - color : var ( - - teal ) ; }
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
/ * Matrice serveurs x integrations * /
. mx - cadre { overflow : auto ; max - height : calc ( 100 vh - 210 px ) ; }
. mx { border - collapse : separate ; border - spacing : 0 ; font - size : 12 px ; width : max - content ; min - width : 100 % ; }
. mx th , . mx td { border - bottom : 1 px solid var ( - - ligne ) ; padding : 5 px 8 px ; }
. mx thead th , . mx tfoot td , . mx tfoot th { position : sticky ; background : var ( - - surface - 2 ) ; z - index : 2 ; }
. mx thead th { top : 0 ; font - size : 11 px ; font - weight : 700 ; letter - spacing : .04 em ; color : var ( - - muted ) ; text - align : center ; white - space : nowrap ; }
. mx thead th . mx - t - univ { color : var ( - - vert ) ; }
. mx tfoot td , . mx tfoot th { bottom : 0 ; font - variant - numeric : tabular - nums ; text - align : center ; color : var ( - - muted ) ; font - weight : 700 ; }
. mx . mx - plein { color : var ( - - vert ) ; }
. mx . mx - h { position : sticky ; left : 0 ; background : var ( - - surface ) ; text - align : left ; font - weight : 700 ; color : var ( - - texte ) ; white - space : nowrap ; cursor : pointer ; z - index : 1 ; }
. mx thead . mx - coin , . mx tfoot . mx - coin { left : 0 ; z - index : 3 ; }
. mx . mx - h : hover { color : var ( - - teal ) ; }
. mx . mx - fn { display : block ; font - size : 10 px ; font - weight : 500 ; color : var ( - - faint ) ; }
. mx tr . mx - sel . mx - h { box - shadow : inset 3 px 0 0 var ( - - teal ) ; }
. mx . mx - c { text - align : center ; }
. mx . mx - c input { width : 14 px ; height : 14 px ; accent - color : var ( - - teal ) ; cursor : pointer ; }
. mx . mx - univ { color : var ( - - vert ) ; font - weight : 800 ; cursor : help ; }
. mx . mx - exempt { color : var ( - - faint ) ; cursor : help ; }
. mx - leg { margin - top : 10 px ; }
. mx - leg code { font - size : 12 px ; }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
. lien - objet { background : none ; border : 0 ; padding : 0 ; color : var ( - - bleu ) ; font : inherit ; font - weight : 700 ; cursor : pointer ; text - decoration : underline dotted ; }
. lien - objet : hover { color : var ( - - teal ) ; }
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
. intr - proprio { margin - left : 8 px ; font - size : 10 px ; font - weight : 800 ; letter - spacing : .06 em ;
text - transform : uppercase ; padding : 2 px 8 px ; border - radius : 999 px ; cursor : help ; }
. intr - proprio . heb { background : color - mix ( in srgb , var ( - - ambre ) 20 % , transparent ) ; color : var ( - - ambre ) ; }
. intr - proprio . ten { background : color - mix ( in srgb , var ( - - bleu ) 18 % , transparent ) ; color : var ( - - bleu ) ; }
2026-06-30 10:07:17 -04:00
. intr - ro { color : var ( - - muted ) ; font - size : 12 px ; line - height : 1.55 ; }
. intr - ro b { color : var ( - - texte ) ; font - weight : 700 ; }
. intr - secrets { display : flex ; flex - wrap : wrap ; gap : 6 px ; margin - top : 6 px ; }
. intr - secrets code { font - size : 11 px ; background : var ( - - surface - 2 ) ; border : 1 px solid var ( - - ligne ) ; border - radius : var ( - - rayon - sm ) ; padding : 1 px 6 px ; color : var ( - - muted ) ; }
2026-06-24 20:17:46 -04:00
. console { position : fixed ; left : 0 ; right : 0 ; bottom : 0 ; height : 44 vh ; background : var ( - - surface ) ; border - top : 1 px solid var ( - - ligne - forte ) ; box - shadow : 0 - 16 px 50 px - 18 px rgba ( 0 , 0 , 0 , .8 ) ; z - index : 40 ; display : flex ; flex - direction : column ; }
. console [ hidden ] { display : none ; }
. console - tete { display : flex ; align - items : center ; gap : 12 px ; padding : 9 px 16 px ; border - bottom : 1 px solid var ( - - ligne ) ; background : var ( - - surface - 2 ) ; }
. console - titre { font - weight : 800 ; font - size : 13 px ; }
. console - etat { font - size : 12 px ; font - weight : 800 ; }
. console - etat . actif { color : var ( - - bleu ) ; animation : pulse 1.2 s ease - in - out infinite ; }
. console - etat . ok { color : var ( - - vert ) ; } . console - etat . echec { color : var ( - - rouge ) ; }
@keyframes pulse { 50 % { opacity : .4 ; } }
. console pre { margin : 0 ; padding : 12 px 16 px ; overflow : auto ; flex : 1 ; font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; font - size : 12 px ; line - height : 1.45 ; white - space : pre - wrap ; word - break : break - word ; color : #cdd6e6; }
@media ( max - width : 1000 px ) {
main { grid - template - columns : 1 fr ; }
aside { position : static ; max - height : none ; order : - 1 ; }
}
2026-07-07 11:34:55 -04:00
/ * Vues lecture seule : Flux + Couches * /
. tbl - lecture { width : 100 % ; border - collapse : collapse ; font - size : 12.5 px ; }
. tbl - lecture th { text - align : left ; padding : 7 px 10 px ; color : var ( - - muted ) ; font - weight : 750 ; border - bottom : 1 px solid var ( - - ligne - forte ) ; position : sticky ; top : 0 ; background : var ( - - surface ) ; }
. tbl - lecture td { padding : 6 px 10 px ; border - bottom : 1 px solid var ( - - ligne ) ; vertical - align : top ; }
. tbl - lecture tr : hover td { background : var ( - - surface - 2 ) ; }
. flux - raison { color : var ( - - muted ) ; max - width : 44 ch ; }
. sens { font - size : 11 px ; font - weight : 750 ; padding : 1 px 7 px ; border - radius : 999 px ; }
. sens - ingress { background : color - mix ( in srgb , var ( - - bleu ) 16 % , transparent ) ; color : var ( - - bleu ) ; }
. sens - egress { background : color - mix ( in srgb , var ( - - ambre ) 18 % , transparent ) ; color : var ( - - ambre ) ; }
. chip . chi - tls - requis { background : color - mix ( in srgb , var ( - - teal ) 22 % , transparent ) ; color : var ( - - teal ) ; }
. chip . chi - tls { background : color - mix ( in srgb , var ( - - teal ) 13 % , transparent ) ; color : var ( - - teal ) ; }
. chip . chi - starttls { background : color - mix ( in srgb , var ( - - ambre ) 18 % , transparent ) ; color : var ( - - ambre ) ; }
. chip . chi - ssh { background : color - mix ( in srgb , var ( - - bleu ) 18 % , transparent ) ; color : var ( - - bleu ) ; }
. chip . chi - tls - cible { background : color - mix ( in srgb , var ( - - ambre ) 12 % , transparent ) ; color : var ( - - muted ) ; }
. chip . chi - clair { background : var ( - - surface - 3 ) ; color : var ( - - muted ) ; }
. couches - pile { display : flex ; flex - direction : column ; gap : 0 ; padding : 6 px ; }
. couche { border : 1 px solid var ( - - ligne - forte ) ; border - radius : 10 px ; padding : 9 px 12 px ; background : var ( - - surface - 2 ) ; }
. couche - tete { display : flex ; align - items : center ; gap : 8 px ; margin - bottom : 6 px ; }
. couche - idx { display : inline - flex ; width : 20 px ; height : 20 px ; align - items : center ; justify - content : center ; border - radius : 999 px ; background : var ( - - teal ) ; color : var ( - - surface ) ; font - size : 11 px ; font - weight : 800 ; }
. couche - groupes { display : flex ; flex - wrap : wrap ; gap : 5 px ; }
. couche - fleche { text - align : center ; color : var ( - - muted ) ; font - size : 14 px ; line - height : 1.5 ; }
. legende { display : flex ; flex - direction : column ; gap : 6 px ; font - size : 12 px ; color : var ( - - muted ) ; }
. legende . chip { margin - right : 4 px ; }
2026-07-07 13:31:02 -04:00
/ * Résumé d ' erreur en langage clair (console) */
#console-resume { padding: 12px 16px; background: color-mix(in srgb, var(--rouge) 10%, var(--surface)); border-bottom: 1px solid var(--ligne); }
. re - tete { display : flex ; align - items : center ; gap : 10 px ; font - weight : 800 ; color : var ( - - rouge ) ; font - size : 13 px ; margin - bottom : 6 px ; }
. re - tete button { margin - left : auto ; }
. re - liste { margin : 0 ; padding - left : 18 px ; display : flex ; flex - direction : column ; gap : 7 px ; }
. re - liste li { font - size : 12.5 px ; }
. re - hote { color : var ( - - teal ) ; font - weight : 700 ; }
. re - type { color : var ( - - muted ) ; font - size : 11 px ; }
. re - msg { background : var ( - - surface - 3 ) ; border - radius : 6 px ; padding : 4 px 8 px ; margin - top : 3 px ; font - family : ui - monospace , monospace ; font - size : 11.5 px ; white - space : pre - wrap ; word - break : break - word ; }
2026-07-07 15:57:47 -04:00
/ * Devis réseau ( config switch ) * /
. devis - reseau { margin : 8 px 0 0 ; padding : 12 px 14 px ; background : var ( - - surface - 3 ) ; border - radius : 8 px ; font - family : ui - monospace , monospace ; font - size : 11.5 px ; line - height : 1.5 ; white - space : pre ; overflow : auto ; max - height : calc ( 100 vh - 210 px ) ; }
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
/ * - - - LES ASSISTANTS ( runbooks ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * /
. rb - liste { display : flex ; flex - wrap : wrap ; gap : 8 px ; margin : 2 px 0 14 px }
. rb - puce { border : 1 px solid var ( - - bord , #2a3340);background:#131a22;color:inherit;border-radius:999px;
padding : 6 px 13 px ; font - size : 12.5 px ; font - weight : 700 ; cursor : pointer ; display : flex ; align - items : center ; gap : 7 px }
. rb - puce . on { background : #1d2a38;border-color:#3d6ea8}
. rb - puce . hors { opacity : .55 }
. rb - etape { border : 1 px solid var ( - - bord , #2a3340);border-radius:10px;padding:11px 13px;margin-bottom:9px;
display : grid ; grid - template - columns : 30 px 1 fr auto ; gap : 11 px ; align - items : start ; background : #10161d}
. rb - etape . faite { border - color : #2f6b45}
. rb - etape . ratee { border - color : #8a3b3b}
. rb - etape . bloquee { opacity : .5 }
. rb - num { font - weight : 800 ; opacity : .55 ; text - align : right ; padding - top : 2 px }
. rb - cible { font - family : ui - monospace , Menlo , monospace ; font - size : 12.5 px ; font - weight : 700 }
. rb - libelle { font - size : 12.5 px ; opacity : .9 ; margin - top : 2 px }
. rb - pourquoi { font - size : 12 px ; opacity : .72 ; margin - top : 5 px ; line - height : 1.45 }
. rb - nature { font - size : 10.5 px ; font - weight : 800 ; letter - spacing : .03 em ; padding : 2 px 7 px ; border - radius : 999 px ; margin - left : 7 px }
. rb - nature . mesure { background : #1b2c3a;color:#8fc4f0}
. rb - nature . ecriture { background : #2e2716;color:#e6bf6a}
. rb - nature . destructif { background : #3a1c1c;color:#f08a8a}
. rb - meta { font - size : 11.5 px ; opacity : .6 ; margin - top : 4 px }
. rb - but { font - size : 12.5 px ; line - height : 1.5 ; opacity : .88 }
2026-10-11 14:43:01 -04:00
/ * - - - LOT 1 ( 2026 - 10 - 11 ) : UNE CONSOLE PAR PORTEE , UNE SEULE GRAMMAIRE - - - - - - - - - - - - -
La couleur d ' identite ne sert qu ' a dire QUI parle : bande , etiquette de portee , vue
active . Jamais un etat . Les etats gardent leurs couleurs , les memes partout : vert =
ca marche , ambre = ecart a regarder , rouge = ca empeche , bleu = pas encore . * /
: root { - - identite : var ( - - muted ) ; }
body [ data - portee = " tenant " ] { - - identite : var ( - - teal ) ; }
body [ data - portee = " site " ] { - - identite : #e0a83a; }
body [ data - portee = " poste " ] { - - identite : #a5b4cb; }
body [ data - portee = " orphelin " ] { - - identite : var ( - - rouge ) ; }
header { border - top : 3 px solid var ( - - identite ) ; gap : 14 px ; }
body [ data - portee = " site " ] header { background : color - mix ( in srgb , #1a170f 86%, transparent); }
. sep { width : 1 px ; height : 26 px ; background : var ( - - ligne - forte ) ; flex : none ; }
. identite { display : flex ; flex - direction : column ; gap : 1 px ; min - width : 0 ; }
. identite - haut { display : flex ; align - items : center ; gap : 8 px ; flex - wrap : wrap ; }
. portee - etiq { font - size : 11 px ; font - weight : 800 ; letter - spacing : .08 em ; text - transform : uppercase ; padding : 3 px 8 px ; border - radius : 5 px ; white - space : nowrap ;
color : var ( - - identite ) ; background : color - mix ( in srgb , var ( - - identite ) 16 % , transparent ) ; }
. ident - nom { font - weight : 800 ; font - size : 15 px ; white - space : nowrap ; }
. cadre { display : flex ; align - items : stretch ; min - height : calc ( 100 vh - 64 px ) ; }
. cadre > main { flex : 1 1 auto ; min - width : 0 ; max - width : none ; margin : 0 ; }
#navigation { flex: 0 0 224px; border-right: 1px solid var(--ligne); background: color-mix(in srgb, var(--fond) 70%, var(--surface)); }
. nav - interne { padding : 16 px 10 px 20 px ; display : flex ; flex - direction : column ; gap : 18 px ; position : sticky ; top : 64 px ; max - height : calc ( 100 vh - 64 px ) ; overflow : auto ; }
. nav - groupe { display : flex ; flex - direction : column ; gap : 2 px ; }
. nav - titre { font - size : 10.5 px ; font - weight : 800 ; letter - spacing : .1 em ; text - transform : uppercase ; color : var ( - - faint ) ; padding : 0 10 px ; margin : 0 0 3 px ; }
. nav - sous { font - size : 11 px ; color : var ( - - faint ) ; padding : 0 10 px ; margin : - 1 px 0 5 px ; line - height : 1.35 ; }
. nav - lien { display : flex ; align - items : center ; gap : 10 px ; width : 100 % ; min - height : 36 px ; padding : 0 10 px ; border - radius : 7 px ; border : 0 ; background : transparent ;
color : var ( - - muted ) ; font : inherit ; font - size : 13.5 px ; font - weight : 600 ; text - align : left ; cursor : pointer ; box - shadow : none ; }
. nav - lien : hover : not ( : disabled ) { background : var ( - - surface - 2 ) ; color : var ( - - texte ) ; border : 0 ; }
. nav - lien . on { background : color - mix ( in srgb , var ( - - identite ) 13 % , var ( - - surface ) ) ; color : var ( - - texte ) ; box - shadow : inset 2 px 0 0 var ( - - identite ) ; }
. nav - lien . n { margin - left : auto ; font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; font - size : 11.5 px ; color : var ( - - faint ) ; font - weight : 500 ; }
. pouvoirs { border : 1 px solid var ( - - ligne ) ; border - radius : var ( - - rayon - sm ) ; background : var ( - - surface ) ; padding : 11 px 12 px ; display : flex ; flex - direction : column ; gap : 7 px ; font - size : 12 px ; }
. pouvoir { display : flex ; gap : 7 px ; align - items : flex - start ; color : var ( - - texte ) ; line - height : 1.35 ; }
. pouvoir . non { color : var ( - - faint ) ; cursor : help ; }
. pouvoir . i { flex : none ; width : 14 px ; text - align : center ; font - weight : 800 ; }
. pouvoir . i . oui { color : var ( - - vert ) ; }
. raccourcis { font - size : 11 px ; color : var ( - - faint ) ; padding : 0 10 px ; line - height : 1.5 ; }
. bandeau - plan { display : flex ; align - items : center ; gap : 10 px ; flex - wrap : wrap ; margin : 0 0 14 px ; padding : 9 px 12 px ; border - radius : 9 px ;
background : color - mix ( in srgb , var ( - - ambre ) 9 % , var ( - - surface ) ) ; border : 1 px solid color - mix ( in srgb , var ( - - ambre ) 40 % , var ( - - ligne ) ) ; color : #f3dca0; font-size: 13px; }
. bandeau - plan [ hidden ] { display : none ; }
. vue - tete { display : flex ; align - items : flex - end ; gap : 12 px ; flex - wrap : wrap ; margin : 0 0 14 px ; }
. vue - titre { margin : 0 ; font - size : 21 px ; font - weight : 750 ; letter - spacing : - .01 em ; }
. vue - sous { font - size : 13 px ; color : var ( - - muted ) ; }
. etiq { display : inline - flex ; align - items : center ; gap : 6 px ; height : 22 px ; padding : 0 8 px ; border - radius : 5 px ; font - size : 11.5 px ; font - weight : 700 ; white - space : nowrap ; }
. e - actif { background : var ( - - vert - fond ) ; color : var ( - - vert ) ; }
. e - plan { color : var ( - - bleu ) ; box - shadow : inset 0 0 0 1 px color - mix ( in srgb , var ( - - bleu ) 45 % , transparent ) ; }
. e - recon { color : var ( - - muted ) ; background : var ( - - surface - 3 ) ; }
. e - div { background : var ( - - ambre - fond ) ; color : var ( - - ambre ) ; }
. e - bloq { background : var ( - - rouge - fond ) ; color : var ( - - rouge ) ; }
. reel { display : inline - flex ; align - items : center ; gap : 7 px ; font - size : 12.5 px ; color : var ( - - muted ) ; }
. pt . ns { background : var ( - - faint ) ; }
. tableau { background : var ( - - surface ) ; border : 1 px solid var ( - - ligne ) ; border - radius : var ( - - rayon ) ; overflow - x : auto ; margin - bottom : 18 px ; }
. tableau - corps { min - width : 720 px ; }
. rang { display : grid ; grid - template - columns : 1.35 fr 1 fr .75 fr .95 fr .9 fr 1.7 fr ; align - items : center ; gap : 10 px ; width : 100 % ; min - height : 42 px ; padding : 0 14 px ;
border : 0 ; border - bottom : 1 px solid var ( - - ligne ) ; border - radius : 0 ; background : transparent ; color : var ( - - texte ) ; font : inherit ; font - size : 13 px ; font - weight : 500 ; text - align : left ; cursor : pointer ; box - shadow : none ; }
. rang : hover : not ( : disabled ) { background : var ( - - surface - 2 ) ; border - color : var ( - - ligne ) ; }
. rang . tete { min - height : 34 px ; font - size : 11 px ; font - weight : 700 ; letter - spacing : .04 em ; color : var ( - - faint ) ; cursor : default ; }
. rang . tete : hover { background : transparent ; }
. rang . selectionnee { background : color - mix ( in srgb , var ( - - identite ) 10 % , var ( - - surface ) ) ; box - shadow : inset 3 px 0 0 var ( - - identite ) ; }
. rang . carte - nom { font - size : 13.5 px ; }
. rang . ip { font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; font - size : 12.5 px ; color : var ( - - muted ) ; }
. rang . rendus { font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; font - size : 12 px ; color : var ( - - muted ) ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
. rang . site { grid - template - columns : 1.3 fr 1 fr .6 fr .8 fr .9 fr 2 fr ; }
. groupe - tete { display : flex ; align - items : center ; gap : 10 px ; min - height : 32 px ; padding : 0 14 px ; background : var ( - - surface - 2 ) ; border - bottom : 1 px solid var ( - - ligne ) ;
color : var ( - - muted ) ; font - size : 11.5 px ; font - weight : 700 ; letter - spacing : .03 em ; }
. groupe - tete . mono { color : var ( - - faint ) ; font - weight : 500 ; }
. noeuds { display : grid ; grid - template - columns : repeat ( auto - fill , minmax ( 230 px , 1 fr ) ) ; gap : 10 px ; margin - bottom : 18 px ; }
. noeud { background : var ( - - surface ) ; border : 1 px solid var ( - - ligne ) ; border - radius : var ( - - rayon ) ; padding : 11 px 13 px ; display : flex ; flex - direction : column ; gap : 8 px ; }
. noeud - tete { display : flex ; align - items : center ; gap : 8 px ; }
. puces { display : flex ; flex - wrap : wrap ; gap : 5 px ; }
. puce { display : inline - flex ; align - items : center ; height : 24 px ; padding : 0 8 px ; border - radius : 5 px ; background : var ( - - surface - 3 ) ; color : var ( - - muted ) ; font - size : 12 px ; font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; }
. puce . socle { background : transparent ; box - shadow : inset 0 0 0 1 px var ( - - ligne - forte ) ; color : var ( - - faint ) ; }
. gestes { margin : 14 px 16 px 4 px ; border : 1 px solid var ( - - ligne - forte ) ; border - radius : var ( - - rayon - sm ) ; background : var ( - - surface - 2 ) ; padding : 12 px 13 px ; display : flex ; flex - direction : column ; gap : 11 px ; }
. gestes - tete { display : flex ; align - items : center ; gap : 8 px ; }
. geste { display : flex ; align - items : center ; gap : 12 px ; }
. geste . num { width : 24 px ; height : 24 px ; border - radius : 50 % ; flex : none ; display : flex ; align - items : center ; justify - content : center ; font - size : 12 px ; font - weight : 800 ; background : var ( - - surface - 3 ) ; color : var ( - - muted ) ; }
. geste . quoi { flex : 1 1 auto ; min - width : 0 ; display : flex ; flex - direction : column ; }
. geste . quoi b { font - size : 13.5 px ; }
. geste . quoi span { font - size : 12 px ; color : var ( - - faint ) ; }
. gestes - note { font - size : 12 px ; color : var ( - - faint ) ; border - top : 1 px solid var ( - - ligne ) ; padding - top : 9 px ; }
button . prod { background : #7f1d1d; border-color: #b23b3b; color: #fff; }
button . prod : hover : not ( : disabled ) { background : #8f2424; border-color: #c94a4a; }
. section - fiche { padding : 12 px 16 px 2 px ; }
. section - fiche > . section - tete { margin - bottom : 7 px ; }
. rb - autres { margin : - 6 px 0 14 px ; font - size : 12.5 px ; color : var ( - - muted ) ; }
. rb - autres > summary { cursor : pointer ; padding : 4 px 2 px ; }
. rb - autres . rb - liste { margin - top : 8 px ; }
. rb - entete { display : flex ; flex - direction : column ; gap : 6 px ; margin : 0 0 12 px ; }
. rb - progres { display : flex ; align - items : center ; gap : 12 px ; font - size : 12.5 px ; color : var ( - - muted ) ; }
. rb - barre { flex : 0 1 300 px ; height : 6 px ; border - radius : 3 px ; background : var ( - - surface - 3 ) ; display : flex ; overflow : hidden ; }
. rb - barre . ok { background : var ( - - vert ) ; } . rb - barre . ko { background : var ( - - rouge ) ; }
. rb - etape . rb - num { width : 26 px ; height : 26 px ; border - radius : 50 % ; display : flex ; align - items : center ; justify - content : center ; opacity : 1 ; padding : 0 ; font - size : 12 px ; background : var ( - - surface - 3 ) ; color : var ( - - muted ) ; }
. rb - etape . faite . rb - num { background : var ( - - vert - fond ) ; color : var ( - - vert ) ; }
. rb - etape . ratee . rb - num { background : var ( - - rouge - fond ) ; color : var ( - - rouge ) ; }
. rb - etape . ratee { background : color - mix ( in srgb , var ( - - rouge ) 6 % , #10161d); }
. rb - etape . bloquee { opacity : .55 ; }
. noeud . selectionnee { border - color : var ( - - identite ) ; box - shadow : 0 0 0 1 px var ( - - identite ) ; }
. rb - etape button . destructif { background : transparent ; border - color : #b23b3b; color: var(--rouge); }
2026-10-11 17:31:13 -04:00
/ * - - - LOT 2 ( 2026 - 10 - 11 ) : LES DEUX BOUTS DU CONTRAT SITE < - > LOCATAIRE - - - - - - - - - - - - - -
Deux fichiers passent entre eux , chacun a sa carte ; le commit dit ce que l ' autre lit. */
. contrat { display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 380 px , 1 fr ) ) ; gap : 14 px ; margin - bottom : 18 px ; }
. carte - contrat { background : var ( - - surface ) ; border : 1 px solid var ( - - ligne ) ; border - radius : var ( - - rayon ) ; display : flex ; flex - direction : column ; }
. carte - contrat > . tete { display : flex ; align - items : center ; gap : 8 px ; flex - wrap : wrap ; padding : 12 px 14 px ; border - bottom : 1 px solid var ( - - ligne ) ; }
. carte - contrat > . tete h3 { margin : 0 ; font - size : 14.5 px ; font - weight : 750 ; }
. carte - contrat > . tete . fichier { font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; font - size : 11.5 px ; color : var ( - - faint ) ; }
. carte - contrat > . corps { padding : 12 px 14 px 14 px ; display : flex ; flex - direction : column ; gap : 12 px ; }
. trace { font - size : 12.5 px ; color : var ( - - muted ) ; }
. trace . mono { color : var ( - - texte ) ; }
. chiffres { display : grid ; grid - template - columns : repeat ( auto - fill , minmax ( 130 px , 1 fr ) ) ; gap : 8 px ; }
. chiffre { display : flex ; flex - direction : column ; gap : 1 px ; padding : 8 px 10 px ; border - radius : 7 px ; background : var ( - - surface - 2 ) ; border : 1 px solid var ( - - ligne ) ; }
. chiffre . v { font - size : 16 px ; font - weight : 650 ; }
. chiffre . l { font - size : 11.5 px ; color : var ( - - faint ) ; }
. sous - tete { font - size : 11 px ; font - weight : 700 ; letter - spacing : .06 em ; text - transform : uppercase ; color : var ( - - faint ) ; margin : 4 px 0 6 px ; }
. carte - contrat . sous - tete { margin : 2 px 0 - 6 px ; }
. lignes { display : flex ; flex - direction : column ; border : 1 px solid var ( - - ligne ) ; border - radius : 7 px ; overflow : hidden ; }
. ligne { display : grid ; grid - template - columns : minmax ( 0 , 1.2 fr ) minmax ( 0 , 1 fr ) minmax ( 0 , 1 fr ) ; gap : 10 px ; align - items : center ; min - height : 32 px ; padding : 0 10 px ;
border - bottom : 1 px solid var ( - - ligne ) ; font - size : 12.5 px ; }
. ligne : last - child { border - bottom : 0 ; }
. ligne . mono { font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; font - size : 12 px ; color : var ( - - muted ) ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
. ligne . deux { grid - template - columns : minmax ( 0 , 1 fr ) minmax ( 0 , 1.3 fr ) ; }
. ecarts { margin : 0 ; padding : 8 px 10 px 8 px 26 px ; border - radius : 7 px ; background : var ( - - ambre - fond ) ; color : #f3dca0; font-size: 12.5px; }
. note - contrat { font - size : 12 px ; color : var ( - - faint ) ; line - height : 1.45 ; }
. rang . loc { grid - template - columns : 1.3 fr 1 fr .9 fr .45 fr .95 fr .85 fr ; min - height : 54 px ; }
. tableau - corps . loc { min - width : 640 px ; }
. rang . pile { display : flex ; flex - direction : column ; align - items : flex - start ; gap : 3 px ; min - width : 0 ; }
. rang . pile . bas { font - size : 11.5 px ; color : var ( - - faint ) ; white - space : nowrap ; }
2026-10-11 22:47:06 -04:00
. btn - lien { display : inline - flex ; align - items : center ; min - height : 32 px ; padding : 0 12 px ; border - radius : 7 px ; border : 1 px solid var ( - - ligne - forte ) ; color : var ( - - texte ) ; text - decoration : none ; font - size : 13 px ; font - weight : 600 ; white - space : nowrap ; }
. btn - lien : hover { background : var ( - - surface - 2 ) ; }
2026-10-11 17:31:13 -04:00
. qui - fait { display : flex ; flex - direction : column ; gap : 10 px ; }
. qui - fait . qui { display : grid ; grid - template - columns : 92 px minmax ( 0 , 1 fr ) ; gap : 10 px ; font - size : 12.5 px ; color : var ( - - muted ) ; line - height : 1.4 ; }
. qui - fait . qui b { color : var ( - - texte ) ; font - size : 11 px ; letter - spacing : .06 em ; text - transform : uppercase ; }
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
/ * - - - LOT 3 a ( 2026 - 10 - 11 ) : A TRAITER - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Une ligne par chose a regarder ; la barre de gauche dit la gravite ( ambre = agir ,
gris = a savoir , vert = verifie ) . Les verifications sans rien a faire restent visibles ,
repliees : un ecran vide ne dirait pas si l ' on a regarde. */
. veille - chiffres { display : flex ; gap : 8 px ; flex - wrap : wrap ; margin : 0 0 12 px ; }
. veille - chiffres . chiffre { min - width : 150 px ; }
. veille - filtres { display : flex ; gap : 6 px ; flex - wrap : wrap ; margin : 0 0 12 px ; }
. veille - filtres button { min - height : 30 px ; padding : 0 11 px ; font - size : 12.5 px ; border - radius : 15 px ; }
. veille - filtres button . on { background : color - mix ( in srgb , var ( - - identite ) 16 % , var ( - - surface ) ) ; border - color : var ( - - identite ) ; color : var ( - - texte ) ; }
. veille - liste { background : var ( - - surface ) ; border : 1 px solid var ( - - ligne ) ; border - radius : var ( - - rayon ) ; overflow : hidden ; margin - bottom : 14 px ; }
. veille - rang { display : grid ; grid - template - columns : 4 px minmax ( 0 , 1 fr ) auto ; gap : 12 px ; align - items : stretch ; width : 100 % ; padding : 0 14 px 0 0 ;
border : 0 ; border - bottom : 1 px solid var ( - - ligne ) ; border - radius : 0 ; background : transparent ; color : var ( - - texte ) ; font : inherit ; text - align : left ; cursor : pointer ; box - shadow : none ; }
. veille - rang : last - child { border - bottom : 0 ; }
. veille - rang : hover : not ( : disabled ) { background : var ( - - surface - 2 ) ; border - color : var ( - - ligne ) ; }
. veille - rang . selectionnee { background : color - mix ( in srgb , var ( - - identite ) 9 % , var ( - - surface ) ) ; }
. veille - rang . barre { background : var ( - - faint ) ; }
. veille - rang . g - agir . barre { background : var ( - - ambre ) ; }
. veille - rang . g - ok . barre { background : var ( - - vert ) ; }
. veille - rang . quoi { display : flex ; flex - direction : column ; gap : 3 px ; padding : 10 px 0 ; min - width : 0 ; }
. veille - rang . ligne1 { display : flex ; align - items : center ; gap : 8 px ; min - width : 0 ; }
. veille - rang . src { font - size : 10.5 px ; font - weight : 800 ; letter - spacing : .07 em ; color : var ( - - faint ) ; white - space : nowrap ; }
. veille - rang . titre { font - size : 13.5 px ; font - weight : 650 ; }
. veille - rang . impact { font - size : 12.5 px ; color : var ( - - muted ) ; }
. veille - rang . tech { font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; font - size : 11.5 px ; color : var ( - - faint ) ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
. veille - rang . etat { display : flex ; align - items : center ; }
. veille - ok > summary { cursor : pointer ; font - size : 12.5 px ; color : var ( - - muted ) ; padding : 4 px 2 px 8 px ; }
. commande { display : flex ; align - items : center ; gap : 8 px ; padding : 7 px 10 px ; border - radius : 7 px ; background : var ( - - surface - 2 ) ; border : 1 px solid var ( - - ligne ) ;
font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; font - size : 12 px ; }
. commande span { flex : 1 1 auto ; min - width : 0 ; overflow - wrap : anywhere ; }
2026-10-11 14:43:01 -04:00
@media ( max - width : 1000 px ) {
. cadre { flex - direction : column ; }
#navigation { flex: none; border-right: 0; border-bottom: 1px solid var(--ligne); }
. nav - interne { position : static ; max - height : none ; flex - direction : row ; flex - wrap : wrap ; }
. nav - groupe { flex : 1 1 200 px ; }
. pouvoirs { flex : 1 1 100 % ; }
}
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
< / style >
2026-06-24 20:17:46 -04:00
< / head >
< body >
2026-06-30 12:19:26 -04:00
< div class = " toasts " id = " toasts " aria - live = " polite " > < / div >
2026-06-24 20:17:46 -04:00
< header >
< div class = " marque " > < span class = " pastille " > < / span > < div class = " titre " > < h1 > Set - OPS < / h1 > < span class = " slogan " > Votre artisan numérique < / span > < / div > < / div >
2026-10-11 14:43:01 -04:00
< span class = " sep " > < / span >
< ! - - QUI PARLE . La portee se lit d ' abord, en couleur ; le titre complet reste en infobulle. -->
< div class = " identite " >
< div class = " identite-haut " >
< span id = " portee " class = " portee-etiq "
title = " Ce que cette console peut faire — dérivé de ce que sa machine porte, pas d ' un réglage " > < / span >
< span id = " ident-nom " class = " ident-nom " > < / span >
< span id = " ident-prod " > < / span >
2026-06-24 20:17:46 -04:00
< / div >
2026-10-11 14:43:01 -04:00
< span class = " chemin " id = " chemin " > < / span >
2026-06-24 20:17:46 -04:00
< / div >
2026-10-11 14:43:01 -04:00
< div class = " grandit " > < / div >
< div class = " resume " id = " resume " > < / div >
< span class = " recherche " > < input id = " filtre " type = " search " placeholder = " Filtrer ( / ) " aria - label = " Filtrer la vue " oninput = " filtrer(this.value) " > < / span >
< button type = " button " class = " fantome " onclick = " charger() " > Recharger < / button >
2026-06-24 20:17:46 -04:00
< / header >
2026-10-11 14:43:01 -04:00
< div class = " cadre " >
< ! - - LA NAVIGATION SE DESSINE SELON LA PORTEE ( dessinerNavigation ) . Un site et un
locataire n ' ont pas les memes vues : les leur montrer toutes, c ' etait proposer
des formulaires de plan a une console qui ne peut pas en ecrire un . - - >
< nav id = " navigation " aria - label = " Vues de la console " > < / nav >
< main >
< section >
< ! - - LE TRAVAIL NON SAUVEGARDE SE VOIT EN TETE DE LA VUE QUI L ' A PRODUIT, avec
l ' etape suivante. C ' etait un « • » sur un bouton parmi douze . - - >
< div class = " bandeau-plan " id = " bandeau-plan " hidden >
< span class = " pt " style = " background:var(--ambre) " > < / span >
< span id = " bandeau-texte " class = " grandit " > Plan modifié . < / span >
< button type = " button " class = " primaire " id = " btn-sauver " onclick = " sauvegarderVue() " > Sauvegarder < span style = " opacity:.7;font-weight:600 " > Ctrl + S < / span > < / button >
< button type = " button " onclick = " appliquerPlan() " title = " Régénère l ' inventaire depuis le plan sauvegardé " > Appliquer le plan < / button >
< / div >
< div id = " grilles " > < / div >
< details class = " dep-bloc " id = " panneau-dep " >
< summary > Prérequis entre groupes < span id = " dep-resume " class = " chip " > < / span > < / summary >
< div class = " dep-corps " id = " dependances " > < / div >
< / details >
< / section >
< aside > < div id = " detail " > < / div > < / aside >
< / main >
< / div >
2026-06-24 20:17:46 -04:00
< div class = " modale " id = " modale " hidden >
< div class = " modale-boite " >
< div class = " modale-titre " id = " modale-titre " > < / div >
< div class = " modale-texte " id = " modale-texte " > < / div >
< label class = " champ " > < span > Mot de passe du vault Ansible ( laisser vide si non requis ) < / span >
< input type = " password " id = " modale-vault " autocomplete = " off " placeholder = " •••••••• " onkeydown = " if(event.key=== ' Enter ' )confirmerModale() " > < / label >
< div class = " modale-actions " >
< button type = " button " onclick = " annulerModale() " > Annuler < / button >
< button type = " button " class = " primaire " onclick = " confirmerModale() " > Confirmer < / button >
< / div >
< / div >
< / div >
2026-06-30 10:07:17 -04:00
< div class = " modale " id = " intrants " hidden >
< div class = " modale-boite large " >
< div class = " modale-titre " > ⚙ Intrants de base de l ' écosystème</div>
< div class = " modale-texte " > Saisis ici les valeurs communes à tout l ' écosystème, depuis un endroit unique.
Les < b > constantes < / b > sont une valeur unique non surchargeable & nbsp ; ; les < b > défauts < / b > resurgissent
comme valeurs proposées là où ils réapparaissent dans les instances déclarées .
Les secrets ne sont jamais saisis ici & nbsp ; : ils restent dans le Vault Ansible ( CLI ) . < / div >
< div class = " intrants-corps " id = " intrants-corps " > < / div >
< div class = " modale-actions " >
< button type = " button " onclick = " fermerIntrants() " > Fermer < / button >
< button type = " button " class = " primaire " onclick = " enregistrerIntrants() " > Enregistrer < / button >
< / div >
< / div >
< / div >
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
< div class = " modale " id = " etape " hidden >
< div class = " modale-boite " >
< div class = " modale-titre " id = " etape-titre " > < / div >
< div class = " modale-texte " id = " etape-texte " > < / div >
< div id = " etape-champs " > < / div >
< label class = " champ " > < span > Mot de passe du vault Ansible ( laisser vide si non requis ) < / span >
< input type = " password " id = " etape-vault " autocomplete = " off " placeholder = " •••••••• " > < / label >
< div class = " modale-actions " >
< button type = " button " onclick = " annulerEtape() " > Annuler < / button >
< button type = " button " class = " primaire " id = " etape-ok " onclick = " confirmerEtape() " > Lancer < / button >
< / div >
< / div >
< / div >
2026-06-24 20:17:46 -04:00
< div class = " console " id = " console " hidden >
< div class = " console-tete " >
< span class = " console-titre " id = " console-titre " > < / span >
< span class = " console-etat " id = " console-etat " > < / span >
< span class = " grandit " > < / span >
< button type = " button " id = " btn-fermer-console " onclick = " fermerConsole() " > Fermer < / button >
< / div >
2026-07-07 13:31:02 -04:00
< div id = " console-resume " hidden > < / div >
2026-06-24 20:17:46 -04:00
< pre id = " console-sortie " > < / pre >
< / div >
< script >
const JETON = " __JETON__ " ;
let groupes = [ ] ;
let dependances = { } ;
let nomenclature = { } ;
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
/ / LE SCHEMA DES REGISTRES , a portee globale . Les formulaires generes le lisaient dans
/ / ` data . schema ` — or il n ' existe AUCUN `data` global ici : c ' est une const locale de
/ / ` charger ( ) ` . Le rendu levait donc ` ReferenceError ` dans un navigateur , et
/ / ` sauvegarderBases ` pire encore ( zone morte : ` const data ` y est declare plus bas ,
/ / dans la meme fonction ) . Eprouver le rendu sous node en LUI FOURNISSANT ` data ` ne
/ / pouvait pas le voir : le banc reproduisait la fonction , pas sa portee .
let schemaPlan = { } ;
2026-06-24 20:17:46 -04:00
let chaine = { } ;
let hotes = [ ] ;
let estProduction = false ;
let filtre = ' ' ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
let vuePrincipale = ' serveurs ' ;
2026-07-07 11:34:55 -04:00
let fluxRegistre = [ ] ; / / matrice des flux réseau ( lecture seule )
let couchesRegistre = [ ] ; / / ordre de déploiement en couches ( lecture seule )
2026-06-24 20:17:46 -04:00
let bdServeurs = [ ] ;
let bdApplis = [ ] ;
let basesModifie = false ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
let selApp = null ; / / index de l ' application sélectionnée (vue Applications)
let selServeur = null ; / / index du serveur sélectionné ( vue Serveurs , maître - détail fusionné )
let selBase = null ; / / clé de l ' item sélectionné (vue Bases) : " srv:<i> " ou " bd:<i> "
let hoteOpCourant = ' ' ; / / nom de l ' hôte dont les boutons Vérifier/Déployer sont affichés
2026-06-24 20:17:46 -04:00
let applications = [ ] ;
let applicationsModifie = false ;
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
let nomEdit = { } ; / / nomenclature en cours d ' édition (copie de travail)
let selNom = null ; / / ' cat:<clef> ' ou ' fon:<clef> ' — l ' entrée sélectionnée
let nomModifie = false ;
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
let domaines = [ ] ; / / registre des domaines ( éditable )
let selDomaine = null ; / / index du domaine sélectionné
let domainesModifie = false ;
const AUTORITES_DNS = [ ' primaire-cache ' , ' auto-heberge ' , ' delegue ' ] ;
2026-06-24 20:17:46 -04:00
let serveurs = [ ] ;
console de site : un vide doit avoir la forme de ce qu'il remplace
La console du runner de SITE-Chezlepro ne montrait rien. Elle a douze machines.
inventaire_api sert serveurs en LISTE, inventaire_api_du_site le servait en TABLE. Les deux
sont vides et la page ne les lit pas pareil : (data.serveurs || []).map trouve {} — truthy,
sans .map — et charger() leve. La console d'un site mourait avant sa premiere vue, et le
message accusait une methode manquante plutot qu'une forme qui ment.
Et la vue regardait au mauvais endroit : le registre vide est voulu, mais la page en tirait
le message d'un locataire sans serveurs et les douze machines servies dans hotes n'etaient
dessinees nulle part. Une console de site dessine desormais SES machines, en lecture seule,
avec leur adresse, leur VMID, leurs roles et leur point de vie ; le panneau droit dit ce
qu'elle peut et renvoie aux Assistants.
C'est le defaut du 16 septembre par une autre porte : la premiere fois l'inventaire etait
vide et la page dessinait ce vide comme un plan vide ; cette fois il est plein et c'est la
vue qui regarde ailleurs.
Ce qui a trouve la cause n'est pas une lecture mais le BANC : en donnant pour la premiere
fois a test_rendu_gui la charge d'une console de site, il a leve sur charger(). Sans lui
j'aurais livre une vue juste par dessus un charger() qui leve.
Valide : node --check sur le bloc script, 7 tests de rendu dont deux neufs, make test a 0
echec, runbooks.py verifier a 0 ecart, P81 et P83 vertes. P02 reste en echec pour la raison
anterieure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:36:00 -04:00
/ / La selection de la vue « machines du site » — distincte de ` selServeur ` , qui indexe un
/ / registre de plan que cette console n ' a pas.
let selMachine = null ;
2026-06-24 20:17:46 -04:00
let serveursModifie = false ;
let ongletActif = ' reseau ' ;
let occupe = false ;
let verifie = { } ;
let motVault = ' ' ;
let modaleResolveur = null ;
2026-06-30 10:07:17 -04:00
let intrants = null ;
2026-06-30 12:36:51 -04:00
let integrationsDispo = [ ] ;
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
let integrationsUniv = { } ;
2026-07-22 21:32:42 -04:00
let liensAcceptes = { } ;
2026-07-01 17:39:44 -04:00
let sondes = { } ; / / état RÉEL par hôte : joignable ( SSH ) ou non — distinct du plan
2026-06-30 12:19:26 -04:00
let chronoConsole = null ;
let consoleDebut = 0 ;
2026-06-24 20:17:46 -04:00
2026-06-30 12:19:26 -04:00
let toastSeq = 0 ;
function message ( texte , type = ' ' , opts = { } ) {
const conteneur = document . getElementById ( ' toasts ' ) ;
if ( ! conteneur ) return null ;
const t = document . createElement ( ' div ' ) ;
t . className = ' toast ' + ( type ? ' ' + type : ' ' ) ;
t . id = ' toast- ' + ( + + toastSeq ) ;
const corps = document . createElement ( ' span ' ) ;
corps . textContent = texte ;
t . appendChild ( corps ) ;
const x = document . createElement ( ' button ' ) ;
x . type = ' button ' ; x . className = ' x ' ; x . textContent = ' × ' ;
x . onclick = ( ) = > retirerToast ( t ) ;
t . appendChild ( x ) ;
conteneur . appendChild ( t ) ;
while ( conteneur . children . length > 6 ) conteneur . removeChild ( conteneur . firstChild ) ;
const duree = opts . persistant ? 0 : ( opts . duree != null ? opts . duree : ( type == = ' erreur ' ? 7000 : 3500 ) ) ;
if ( duree > 0 ) setTimeout ( ( ) = > retirerToast ( t ) , duree ) ;
return t ; / / renvoie le nœud pour permettre de le mettre à jour ( opérations longues )
}
function majToast ( t , texte , type , dureeFin ) {
if ( ! t ) return ;
if ( type != = undefined ) t . className = ' toast ' + ( type ? ' ' + type : ' ' ) ;
const corps = t . querySelector ( ' span ' ) ; if ( corps ) corps . textContent = texte ;
if ( dureeFin ) setTimeout ( ( ) = > retirerToast ( t ) , dureeFin ) ;
}
function retirerToast ( t ) {
if ( ! t | | ! t . parentNode ) return ;
t . classList . add ( ' sortant ' ) ;
setTimeout ( ( ) = > { if ( t . parentNode ) t . parentNode . removeChild ( t ) ; } , 260 ) ;
2026-06-24 20:17:46 -04:00
}
2026-09-16 14:36:59 -04:00
let contexte = null ;
2026-09-16 14:57:39 -04:00
/ / LE MEME PARTAGE QUE ` POUVOIR_REQUIS ` COTE SERVEUR , ecrit une fois ici pour la page .
/ / Ce n ' est pas la garde — la garde refuse au serveur — c ' est ce qui evite d ' aller au
/ / refus pour l ' apprendre. P81 verifie que les quatre modes y figurent encore.
const POUVOIR_PAR_MODE = { creer : ' materialiser ' , verifier : ' configurer ' ,
deployer : ' configurer ' , pousser : ' configurer ' } ;
/ / SANS CONTEXTE CONNU , ON NE RETIRE RIEN . Une console qui griserait ses boutons parce
/ / qu ' elle n ' a pas su lire sa portée serait pire que le défaut qu ' on corrige : elle
/ / empêcherait un geste légitime sans rien expliquer . Le serveur , lui , refuse quand
/ / même — c ' est lui la garde.
function peut ( pouvoir ) { return ! contexte | | ! contexte . peut | | ! ! contexte . peut [ pouvoir ] ; }
function pourquoiPas ( pouvoir ) {
return ( contexte & & contexte . raison & & contexte . raison [ pouvoir ] ) | | ' ' ;
}
2026-09-16 14:36:59 -04:00
/ / LA CONSOLE DIT CE QU ' ELLE EST. Trois consoles identiques à trois URL différentes
/ / sont un piège pour qui en ouvre deux : celle d ' un site et celle d ' un locataire ne
/ / peuvent pas les mêmes gestes , et rien ne le disait .
function appliquerContexte ( ctx ) {
2026-09-16 14:57:39 -04:00
/ / ON VERIFIE LA FORME AVANT DE L ' ADOPTER (mesuré le 2026-09-16, par le banc de
/ / rendu ) . Ma première version prenait tout ce qu ' on lui donnait : une réponse qui
/ / n ' était pas un contexte — celle d ' une autre route — passait , puis ` peut ( ) ` lisait
/ / ` contexte . peut [ . . . ] ` sur ` undefined ` et la page mourait d ' un TypeError à
/ / l ' ouverture. `node --check` n ' y voyait rien : la syntaxe était juste .
if ( ! ctx | | typeof ctx . peut != = ' object ' | | typeof ctx . portee != = ' string ' ) return ;
contexte = ctx ;
2026-09-16 14:36:59 -04:00
const badge = document . getElementById ( ' portee ' ) ;
2026-09-16 14:57:39 -04:00
if ( ! badge ) return ;
2026-10-11 14:43:01 -04:00
/ / LA COULEUR VIENT DU THEME ( ` body [ data - portee ] ` → ` - - identite ` ) , pas d ' ici. Le
/ / 2026 - 09 - 16 , une couleur posee en dur donnait un texte sombre sur fond clair dans
/ / une console sombre : une couleur ne se relit pas , elle se regarde .
const etiquettes = { site : ' Site · hébergeur ' , tenant : ' Locataire ' ,
poste : ' Poste du mainteneur ' , orphelin : ' Sans pouvoir ' } ;
badge . textContent = etiquettes [ ctx . portee ] | | ctx . portee ;
badge . title = ctx . titre ;
const nom = document . getElementById ( ' ident-nom ' ) ;
if ( nom ) nom . textContent = ctx . portee == = ' site ' ? ( ctx . fabric | | ' ' )
: ( ctx . ecosysteme | | ctx . fabric | | ' ' ) ;
2026-09-16 14:36:59 -04:00
document . body . dataset . portee = ctx . portee ;
/ / Ce que la portée interdit ne se propose pas . Le serveur refuse de toute façon
/ / ( POUVOIR_REQUIS ) ; ceci évite d ' aller au refus pour l ' apprendre .
const verrou = ( el , permis , pourquoi ) = > {
if ( ! el ) return ;
el . disabled = ! permis ;
el . title = permis ? ( el . title | | ' ' ) : pourquoi ;
el . style . opacity = permis ? ' ' : ' .45 ' ;
} ;
verrou ( document . getElementById ( ' btn-sauver ' ) , ctx . peut . editer_le_plan ,
ctx . raison . editer_le_plan ) ;
2026-10-11 14:43:01 -04:00
dessinerNavigation ( ) ;
2026-09-16 14:36:59 -04:00
document . querySelectorAll ( ' button[onclick^= " ouvrirIntrants " ] ' ) . forEach (
b = > verrou ( b , ctx . peut . editer_le_plan , ctx . raison . editer_le_plan ) ) ;
}
2026-10-11 14:43:01 -04:00
/ / LES VUES DE CETTE CONSOLE , SELON CE QU ' ELLE EST (2026-10-11).
/ /
/ / Un locataire EXPLOITE son parc , ECRIT son plan et LIT ce qui en derive . Un site
/ / tient son TERRAIN , conduit ses GESTES et LIT son plan , dont les adresses sont
/ / ecrites . Les dix boutons d ' avant etaient les memes pour les deux : le site se voyait
/ / offrir la nomenclature et les intrants d ' un plan qu ' il ne peut pas ecrire .
/ / Rien n ' est retire : une vue sans objet ici (nomenclature vide d ' un site ) n ' est pas
/ / proposee , et les gestes hors de portee restent dits dans « Cette console peut » .
function groupesDeNavigation ( ) {
const n = ( x ) = > ( x == null ? ' ' : String ( x ) ) ;
const nbRb = ( typeof runbooks != = ' undefined ' & & runbooks . length )
? runbooks . filter ( rb = > rb . conduisible ) . length : ' ' ;
const aNomenclature = Object . keys ( ( nomenclature | | { } ) . fonctions | | { } ) . length > 0 ;
2026-10-11 17:31:13 -04:00
/ / LE CONTRAT AVEC L ' AUTRE BOUT (lot 2) : en tete, parce que c ' est ce qu ' on regarde
/ / avant d ' agir. Le poste du mainteneur monte les deux bouts, il voit les deux.
const veille = [ ] ;
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
if ( aCote ( ' a_traiter ' ) ) veille . push ( [ ' a-traiter ' , ' À traiter ' , n ( compteAgir ( ) ) ] ) ;
2026-10-11 17:31:13 -04:00
if ( aCote ( ' lien_avec_le_site ' ) ) veille . push ( [ ' lien ' , ' Lien avec le site ' , ' ' ] ) ;
if ( aCote ( ' locataires_heberges ' ) ) veille . push ( [ ' locataires ' , ' Locataires hébergés ' ,
locatairesCharge ? n ( ( locatairesCharge . locataires | | [ ] ) . length ) : ' ' ] ) ;
const enTete = veille . length ? [ { titre : ' Veille ' , vues : veille } ] : [ ] ;
2026-10-11 14:43:01 -04:00
if ( peut ( ' editer_le_plan ' ) ) {
2026-10-11 17:31:13 -04:00
return [ . . . enTete ,
2026-10-11 14:43:01 -04:00
{ titre : ' Exploiter ' , vues : [
[ ' serveurs ' , ' Parc ' , n ( serveurs . length ) ] ,
[ ' assistants ' , ' Assistants ' , n ( nbRb ) ] ] } ,
{ titre : ' Le plan ' , sous : ' Ce que tu écris ; « Appliquer le plan » en dérive l \' inventaire. ' , vues : [
[ ' applications ' , ' Applications ' , n ( applications . length ) ] ,
[ ' bases ' , ' Bases ' , n ( bdServeurs . length + bdApplis . length ) ] ,
[ ' domaines ' , ' Domaines ' , n ( domaines . length ) ] ,
[ ' nomenclature ' , ' Nomenclature ' , ' ' ] ,
[ ' integrations ' , ' Intégrations ' , ' ' ] ,
[ ' @intrants ' , ' Intrants de base ' , ' ' ] ] } ,
{ titre : ' Lire — dérivé ' , vues : [
[ ' flux ' , ' Flux ' , n ( fluxRegistre . length ) ] ,
[ ' couches ' , ' Couches de déploiement ' , n ( couchesRegistre . length ) ] ,
[ ' reseau ' , ' Réseau & fédération ' , ' ' ] ] } ,
] ;
}
const lecture = [
[ ' applications ' , ' Applications ' , n ( applications . length ) ] ,
[ ' bases ' , ' Bases ' , n ( bdServeurs . length + bdApplis . length ) ] ,
[ ' domaines ' , ' Domaines ' , n ( domaines . length ) ] ] ;
if ( aNomenclature ) lecture . push ( [ ' nomenclature ' , ' Nomenclature ' , ' ' ] ) ;
lecture . push ( [ ' integrations ' , ' Intégrations ' , ' ' ] ,
[ ' flux ' , ' Flux ' , n ( fluxRegistre . length ) ] ,
[ ' couches ' , ' Couches de déploiement ' , n ( couchesRegistre . length ) ] ) ;
2026-10-11 17:31:13 -04:00
return [ . . . enTete ,
2026-10-11 14:43:01 -04:00
{ titre : ' Le terrain ' , vues : [
[ ' serveurs ' , ' Machines & fabric ' , n ( hotes . length ) ] ,
[ ' reseau ' , ' Réseau & frontière ' , ' ' ] ] } ,
{ titre : ' Gestes ' , vues : [ [ ' assistants ' , ' Assistants ' , n ( nbRb ) ] ] } ,
{ titre : ' Plan — lecture ' , sous : ' Ses adresses y sont écrites ; il se lit ici et s \' édite dans son dépôt. ' , vues : lecture } ,
] ;
}
const LIBELLES_POUVOIRS = [
[ ' editer_le_plan ' , ' Éditer le plan ' ] ,
[ ' configurer ' , ' Configurer : vérifier, déployer ' ] ,
[ ' materialiser ' , ' Matérialiser : créer, détruire des VM ' ] ,
[ ' fabric ' , ' Lire la fabric ' ] ,
] ;
function dessinerNavigation ( ) {
const nav = document . getElementById ( ' navigation ' ) ;
if ( ! nav ) return ;
const groupes = groupesDeNavigation ( ) . map ( g = > {
const liens = g . vues . map ( ( [ vue , libelle , compte ] ) = > {
const action = vue == = ' @intrants ' ? ' ouvrirIntrants() ' : ` setVue ( ' $ {vue} ' ) ` ;
const on = vue == = vuePrincipale ? ' on ' : ' ' ;
const courant = on ? ' aria-current= " page " ' : ' ' ;
return ` < button type = " button " class = " nav-lien$ {on} " $ { courant } onclick = " $ {action} " > $ { echapper ( libelle ) } `
+ ( compte != = ' ' ? ` < span class = " n " > $ { echapper ( compte ) } < / span > ` : ' ' ) + ' </button> ' ;
} ) . join ( ' ' ) ;
return ` < div class = " nav-groupe " > < p class = " nav-titre " > $ { echapper ( g . titre ) } < / p > `
+ ( g . sous ? ` < p class = " nav-sous " > $ { echapper ( g . sous ) } < / p > ` : ' ' ) + liens + ' </div> ' ;
} ) . join ( ' ' ) ;
/ / CE QUE CETTE CONSOLE PEUT , EN CLAIR . Les raisons ne vivaient qu ' en infobulle sur
/ / des boutons grises : on decouvrait l ' interdit en survolant, au hasard.
const pouvoirs = contexte & & contexte . peut ? LIBELLES_POUVOIRS . map ( ( [ p , libelle ] ) = > {
const oui = ! ! contexte . peut [ p ] ;
return ` < div class = " pouvoir$ { oui ? ' ' : ' non ' } " $ { oui ? ' ' : ` title = " $ { echapper(pourquoiPas(p))} " ` } > `
+ ` < span class = " i$ { oui ? ' oui ' : ' ' } " > $ { oui ? ' ✓ ' : ' — ' } < / span > < span > $ { echapper ( libelle ) } < / span > < / div > ` ;
} ) . join ( ' ' ) : ' ' ;
nav . innerHTML = ' <div class= " nav-interne " > ' + groupes
+ ( pouvoirs ? ` < div class = " pouvoirs " > < div class = " section-tete " style = " margin:0 0 2px " > Cette console peut < / div > $ { pouvoirs } `
+ ' <div class= " aide-ro " style= " font-size:11px;color:var(--faint) " >Survoler une ligne grisée pour savoir pourquoi.</div></div> ' : ' ' )
+ ' <div class= " raccourcis " >Raccourcis : 1-3 vues · j/k serveur suivant · v vérifier · d déployer · / filtrer · Ctrl+S sauvegarder</div></div> ' ;
}
2026-06-24 20:17:46 -04:00
async function charger ( ) {
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
lienCharge = null ; locatairesCharge = null ; aTraiterCharge = null ; / / « Recharger » relit aussi le contrat et la veille
2026-09-16 14:36:59 -04:00
try {
const ctxRep = await fetch ( ' /api/contexte ' ) ;
if ( ctxRep . ok ) appliquerContexte ( await ctxRep . json ( ) ) ;
} catch ( e ) { / * une console sans contexte reste utilisable , en aveugle * / }
2026-06-24 20:17:46 -04:00
const reponse = await fetch ( ' /api/inventaire ' ) ;
const data = await reponse . json ( ) ;
2026-09-16 14:36:59 -04:00
if ( data . contexte ) appliquerContexte ( data . contexte ) ;
2026-06-24 20:17:46 -04:00
groupes = data . groupes ;
dependances = data . dependances | | { } ;
nomenclature = data . nomenclature | | { } ;
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
schemaPlan = data . schema | | { } ;
2026-06-24 20:17:46 -04:00
chaine = data . chaine | | { } ;
2026-07-07 11:34:55 -04:00
fluxRegistre = data . flux | | [ ] ;
couchesRegistre = data . couches | | [ ] ;
2026-06-30 10:07:17 -04:00
intrants = data . intrants | | null ;
2026-06-30 12:36:51 -04:00
integrationsDispo = data . integrations_disponibles | | [ ] ;
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
integrationsUniv = data . integrations_universelles | | { } ;
2026-07-22 21:32:42 -04:00
liensAcceptes = data . liens_acceptes | | { } ;
2026-06-24 20:17:46 -04:00
chargerBases ( data ) ;
chargerApplications ( data ) ;
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
chargerDomaines ( data ) ;
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
chargerNomEdit ( data ) ;
2026-06-24 20:17:46 -04:00
chargerServeurs ( data ) ;
hotes = data . hotes ;
estProduction = ! ! data . production ;
2026-06-30 12:19:26 -04:00
verifie = { } ;
2026-06-24 20:17:46 -04:00
document . getElementById ( ' chemin ' ) . textContent = data . inventaire ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
if ( selServeur == null | | selServeur > = serveurs . length ) selServeur = serveurs . length ? 0 : null ;
2026-06-24 20:17:46 -04:00
dessiner ( ) ;
message ( ' Inventaire chargé. ' , ' ok ' ) ;
2026-07-01 17:39:44 -04:00
sonder ( ) ; / / état réel ( joignabilité ) en arrière - plan , non bloquant
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
prechargerATraiter ( ) ; / / le compte « À traiter » de la navigation , en arrière - plan
2026-07-01 17:39:44 -04:00
}
async function sonder ( ) {
try {
const r = await fetch ( ' /api/sondes ' ) ;
if ( r . ok ) { sondes = await r . json ( ) ; dessiner ( ) ; }
} catch ( e ) { / * silencieux * / }
2026-06-24 20:17:46 -04:00
}
function setOnglet ( o ) { ongletActif = o ; dessinerDetail ( ) ; }
function filtrer ( valeur ) { filtre = ( valeur | | ' ' ) . trim ( ) . toLowerCase ( ) ; dessinerGrilles ( ) ; }
function groupesActifs ( ) { const a = new Set ( ) ; hotes . forEach ( h = > { if ( h . etat == = ' actif ' ) ( h . groupes | | [ ] ) . forEach ( g = > a . add ( g ) ) ; } ) ; return a ; }
function groupesPlanifies ( ) { const p = new Set ( ) ; hotes . forEach ( h = > ( h . groupes | | [ ] ) . forEach ( g = > p . add ( g ) ) ) ; return p ; }
function statutDependance ( groupe ) {
const requis = ( ( dependances [ groupe ] | | { } ) . requiert_groupes_actifs | | [ ] ) ;
if ( ! requis . length ) return ' neutre ' ;
const actifs = groupesActifs ( ) , planifies = groupesPlanifies ( ) ;
const manquants = requis . filter ( i = > ! actifs . has ( i ) ) ;
if ( ! manquants . length ) return ' ok ' ;
if ( manquants . every ( i = > planifies . has ( i ) ) ) return ' attente ' ;
return ' bloque ' ;
}
function manquantsPourGroupe ( groupe ) { const a = groupesActifs ( ) ; return ( ( dependances [ groupe ] | | { } ) . requiert_groupes_actifs | | [ ] ) . filter ( i = > ! a . has ( i ) ) ; }
function hoteBloque ( h ) { if ( ! h | | h . etat != = ' actif ' ) return false ; return ( h . groupes | | [ ] ) . some ( g = > manquantsPourGroupe ( g ) . length ) ; }
function libelleStatut ( s ) { return { ok : ' actif ' , attente : ' planifié ' , bloque : ' bloqué ' , neutre : ' libre ' } [ s ] | | s ; }
2026-10-11 14:43:01 -04:00
function dessiner ( ) { dessinerResume ( ) ; dessinerNavigation ( ) ; dessinerGrilles ( ) ; dessinerDependances ( ) ; dessinerDetail ( ) ; majIndicateurSauvegarde ( ) ; }
2026-06-24 20:17:46 -04:00
2026-10-11 14:43:01 -04:00
/ / LE BANDEAU « PLAN MODIFIE » remplace le « • » du bouton global : il dit QUELLE vue
/ / porte du travail non sauvegarde , et l ' etape qui suit. Il ne parait que la ou l ' on
/ / peut ecrire — une console qui lit son plan n ' a rien a sauvegarder.
const NOMS_VUES_EDITABLES = { serveurs : ' serveurs ' , integrations : ' intégrations ' , applications : ' applications ' ,
bases : ' bases ' , domaines : ' domaines ' , nomenclature : ' nomenclature ' } ;
2026-06-24 20:17:46 -04:00
function majIndicateurSauvegarde ( ) {
const b = document . getElementById ( ' btn-sauver ' ) ;
2026-06-30 12:19:26 -04:00
if ( ! b ) return ;
2026-10-11 14:43:01 -04:00
const editable = vuePrincipale in NOMS_VUES_EDITABLES ;
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
const sale = ( ( vuePrincipale == = ' serveurs ' | | vuePrincipale == = ' integrations ' ) & & serveursModifie )
2026-06-30 12:19:26 -04:00
| | ( vuePrincipale == = ' applications ' & & applicationsModifie )
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
| | ( vuePrincipale == = ' bases ' & & basesModifie )
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
| | ( vuePrincipale == = ' domaines ' & & domainesModifie )
| | ( vuePrincipale == = ' nomenclature ' & & nomModifie ) ;
2026-10-11 14:43:01 -04:00
b . disabled = ! editable | | ! peut ( ' editer_le_plan ' ) ;
2026-06-30 12:19:26 -04:00
b . title = editable ? ' Sauvegarder cette vue (Ctrl+S) ' : ' Lecture seule — édite dans Serveurs / Applications / Bases ' ;
2026-10-11 14:43:01 -04:00
const bandeau = document . getElementById ( ' bandeau-plan ' ) ;
if ( bandeau ) bandeau . hidden = ! ( sale & & editable & & peut ( ' editer_le_plan ' ) ) ;
const texte = document . getElementById ( ' bandeau-texte ' ) ;
if ( texte & & sale ) texte . innerHTML = ' <b>Plan modifié</b> — ' + echapper ( NOMS_VUES_EDITABLES [ vuePrincipale ] | | ' ' )
+ ' pas encore sauvegardé. Ensuite : appliquer le plan pour régénérer l \' inventaire. ' ;
2026-06-24 20:17:46 -04:00
}
function dessinerResume ( ) {
const actifs = hotes . filter ( h = > h . etat == = ' actif ' ) . length ;
const bloques = hotes . filter ( h = > hoteBloque ( h ) ) . length ;
2026-07-01 17:52:39 -04:00
const vivantes = hotes . filter ( h = > sondes [ h . nom ] == = true ) . length ;
const sonde = Object . keys ( sondes ) . length > 0 ;
2026-10-11 14:43:01 -04:00
/ / PROD OU BAC A SABLE fait partie de l ' IDENTITE : on le lit avec le nom, pas parmi
/ / les compteurs .
const prod = document . getElementById ( ' ident-prod ' ) ;
if ( prod ) prod . innerHTML = estProduction
? ' <span class= " badge-prod " title= " Instance de PRODUCTION : déploiement sous confirmation renforcée " >PROD</span> '
: ' <span class= " badge-lab " title= " Instance bac à sable (setops_production: false) " >bac à sable</span> ' ;
2026-06-24 20:17:46 -04:00
document . getElementById ( ' resume ' ) . innerHTML = `
< span class = " chip " > < b > $ { hotes . length } < / b > hôtes < / span >
< span class = " chip " > < span class = " pt actif " > < / span > < b > $ { actifs } < / b > actifs < / span >
< span class = " chip " > < span class = " pt plan " > < / span > < b > $ { hotes . length - actifs } < / b > planifiés < / span >
2026-07-01 17:52:39 -04:00
$ { bloques ? ` < span class = " chip " > < span class = " pt bloq " > < / span > < b > $ { bloques } < / b > bloqués < / span > ` : ' ' }
$ { sonde ? ` < span class = " chip " title = " État RÉEL : VM joignables (sonde SSH), indépendamment du plan " > < span class = " pt actif " > < / span > < b > $ { vivantes } < / b > vivantes < / span > ` : ' ' } ` ;
2026-06-24 20:17:46 -04:00
}
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
function correspondServeur ( s ) {
if ( ! filtre ) return true ;
return [ s . nom , s . fonction , s . adresse_ip , s . vmid , s . etat ] . join ( ' ' ) . toLowerCase ( ) . includes ( filtre ) ;
2026-06-24 20:17:46 -04:00
}
function setVue ( v ) {
vuePrincipale = v ;
2026-10-11 17:31:13 -04:00
if ( v == = ' lien ' ) lienCharge = null ;
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
if ( v == = ' a-traiter ' ) aTraiterCharge = null ;
2026-10-11 17:31:13 -04:00
if ( v == = ' locataires ' ) locatairesCharge = null ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
if ( v == = ' serveurs ' & & selServeur == null & & serveurs . length ) selServeur = 0 ;
if ( v == = ' applications ' & & selApp == null & & applications . length ) selApp = 0 ;
if ( v == = ' bases ' & & selBase == null ) selBase = bdServeurs . length ? ' srv:0 ' : ( bdApplis . length ? ' bd:0 ' : null ) ;
2026-10-11 14:43:01 -04:00
dessinerNavigation ( ) ;
2026-06-24 20:17:46 -04:00
dessinerGrilles ( ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
dessinerDetail ( ) ; / / le panneau droit reflète la sélection de la vue courante
2026-06-30 12:19:26 -04:00
majIndicateurSauvegarde ( ) ;
2026-06-24 20:17:46 -04:00
}
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
const BANNIERE_MODELE = ` < details class = " aide-modele " >
< summary > Comment lire ce parc — serveur · hôte · groupe · application · base < / summary >
< div class = " aide-corps " >
< p > < b > Serveur < / b > ( ce que tu écris ici ) → < b > « Appliquer le plan » < / b > → < b > Hôte < / b > ( la VM générée ) . C ' est le même objet, à deux étapes.</p>
< ul >
< li > < b > Serveur / Hôte < / b > — une VM ; elle < b > appartient à des groupes < / b > . < / li >
< li > < b > Groupe < / b > — un rôle appliqué à l ' hôte (ex. <span class= " mono " >serveur_nginx</span>, <span class= " mono " >client_dns</span>).</li>
< li > < b > Application < / b > — un service qui < b > tourne sur < / b > un hôte , < b > via < / b > un groupe , et < b > expose < / b > un FQDN . < / li >
< li > < b > Base < / b > — une donnée < b > hébergée sur < / b > un hôte et < b > consommée par < / b > une application . < / li >
< / ul >
2026-10-11 14:43:01 -04:00
< p class = " aide-ro " > Choisis à < b > gauche < / b > ( une ligne par serveur ) , édite à < b > droite < / b > ; les dérivés ( VMID / IP / VLAN / groupes ) se calculent à « Appliquer le plan » . < / p >
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
< / div > < / details > ` ;
2026-06-24 20:17:46 -04:00
function dessinerGrilles ( ) {
if ( vuePrincipale == = ' applications ' ) { dessinerApplications ( ) ; return ; }
if ( vuePrincipale == = ' bases ' ) { dessinerBases ( ) ; return ; }
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
if ( vuePrincipale == = ' domaines ' ) { dessinerDomaines ( ) ; return ; }
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
if ( vuePrincipale == = ' nomenclature ' ) { dessinerNomenclature ( ) ; return ; }
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
if ( vuePrincipale == = ' integrations ' ) { dessinerIntegrations ( ) ; return ; }
2026-07-07 11:34:55 -04:00
if ( vuePrincipale == = ' flux ' ) { dessinerFlux ( ) ; return ; }
if ( vuePrincipale == = ' couches ' ) { dessinerCouches ( ) ; return ; }
2026-07-07 15:57:47 -04:00
if ( vuePrincipale == = ' reseau ' ) { dessinerReseau ( ) ; return ; }
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
if ( vuePrincipale == = ' assistants ' ) { dessinerAssistants ( ) ; return ; }
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
if ( vuePrincipale == = ' a-traiter ' ) { dessinerATraiter ( ) ; return ; }
2026-10-11 17:31:13 -04:00
if ( vuePrincipale == = ' lien ' ) { dessinerLienSite ( ) ; return ; }
if ( vuePrincipale == = ' locataires ' ) { dessinerLocataires ( ) ; return ; }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
dessinerServeurs ( ) ;
2026-06-24 20:17:46 -04:00
}
2026-07-07 11:34:55 -04:00
function legendeLecture ( ) {
const d = document . getElementById ( ' detail ' ) ;
if ( vuePrincipale == = ' flux ' ) {
d . innerHTML = ` < div class = " detail " > < div class = " section-tete " > Chiffrement < / div >
< div class = " legende " >
< div > < span class = " chip chi-tls-requis " > tls - requis < / span > chiffré + pair vérifié < / div >
< div > < span class = " chip chi-tls " > tls < / span > chiffré < / div >
< div > < span class = " chip chi-starttls " > starttls < / span > mise à niveau opportuniste < / div >
< div > < span class = " chip chi-ssh " > ssh < / span > transport SSH < / div >
< div > < span class = " chip chi-tls-cible " > tls - cible < / span > TLS visé ( roadmap ) < / div >
< div > < span class = " chip chi-clair " > clair < / span > non chiffré ( local / edge ) < / div > < / div >
< div class = " hint-dep " style = " margin-top:10px " > Lecture seule . Éditer les < code > meta / flux . yml < / code > des rôles , puis < code > make flux < / code > . < / div > < / div > ` ;
2026-07-07 15:57:47 -04:00
} else if ( vuePrincipale == = ' reseau ' ) {
Aligne le GUI et la doc sur le multi-instances
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>
2026-07-23 11:57:59 -04:00
d . innerHTML = ` < div class = " detail " > < div class = " section-tete " > Réseau & amp ; fédération < / div >
docs : tisser les devis dans les points d'entree, et corriger trois faits perimes
Pas de refonte : 54 roles / 54 README, 34 documents, une carte, un registre de
decisions. Le retard etait ailleurs — le travail du jour vivait dans son coin,
les cinq devis n'existant que dans deux fichiers. Donc decouvrables seulement
par qui connait le Makefile, ce qui contredit « exploitable sans IA ».
Tisses dans les quatre points d'entree : ligne « Conformite du deploye » dans
la carte, section « Ecrire, puis relire (D-68) » dans AGENTS.md, §6.0 du
runbook (le premier reflexe), vue Reconstruction de la GUI.
Le tissage a fait tomber trois affirmations perimees :
- la carte annoncait 28 decisions, il y en a 66 en vigueur (D-01 -> D-69) ;
- elle disait les acces « decides, non construits, ou=people et ou=groups
restent vides » — mesure : un compte, un groupe, chaine exercee de bout en
bout sur Icinga Web 2 le jour meme ;
- la GUI parlait des « deux » devis d'infrastructure ; il y en a quatre.
Formation et wiki differes : la reconstruction from-zero est le test de cette
documentation, et enseigner une procedure que personne n'a executee serait
enseigner une hypothese.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 12:33:35 -04:00
< div class = " hint-dep " > Chaque instance fédérée dérive son adressage de son seed < code > index < / code > ( VLAN / VNI = 1000 + index × 10 + zone , unique dans la fédération ) . La vue liste la flotte , signale toute collision d ' index, et génère les devis d ' infrastructure : commutateurs ( < code > make devis - reseau < / code > ) , frontière nord / sud ( < code > make devis - opnsense < / code > ) , SDN EVPN ( < code > make devis - sdn < / code > ) et pare - feu est - ouest Proxmox ( < code > make devis - proxmox - fw < / code > ) . < / div >
2026-08-03 08:52:22 -04:00
< div class = " hint-dep " style = " margin-top:10px " > Lecture seule , régénérés du plan — < code > make devis - reseau < / code > / < code > make devis - opnsense < / code > . Le < strong > dialecte de CLI < / strong > et le < strong > mode de routage < / strong > ( commutateur ou SDN EVPN ) se règlent dans la section < em > Fabric < / em > du panneau « Intrants de base » . En mode SDN , les commutateurs ne portent ni VLAN tenant , ni SVI , ni ACL : le routage et le filtrage inter - zone vivent sur les hyperviseurs . < / div >
< div class = " hint-dep " style = " margin-top:10px " > Ce qui reste à nommer à la main : les < strong > ports physiques < / strong > ( < code > & lt ; PORT - VERS - … & gt ; < / code > ) et , en SDN , le < strong > nœud de sortie EVPN < / strong > . Rien d ' autre — adresses, VLAN et routes se dérivent.</div></div>`;
2026-07-07 11:34:55 -04:00
} else {
d . innerHTML = ` < div class = " detail " > < div class = " section-tete " > Reconstruction < / div >
< div class = " hint-dep " > L ' ordre part du socle et remonte : chaque couche suppose la précédente debout. Le tri intra-couche vient du graphe des dépendances.</div>
docs : tisser les devis dans les points d'entree, et corriger trois faits perimes
Pas de refonte : 54 roles / 54 README, 34 documents, une carte, un registre de
decisions. Le retard etait ailleurs — le travail du jour vivait dans son coin,
les cinq devis n'existant que dans deux fichiers. Donc decouvrables seulement
par qui connait le Makefile, ce qui contredit « exploitable sans IA ».
Tisses dans les quatre points d'entree : ligne « Conformite du deploye » dans
la carte, section « Ecrire, puis relire (D-68) » dans AGENTS.md, §6.0 du
runbook (le premier reflexe), vue Reconstruction de la GUI.
Le tissage a fait tomber trois affirmations perimees :
- la carte annoncait 28 decisions, il y en a 66 en vigueur (D-01 -> D-69) ;
- elle disait les acces « decides, non construits, ou=people et ou=groups
restent vides » — mesure : un compte, un groupe, chaine exercee de bout en
bout sur Icinga Web 2 le jour meme ;
- la GUI parlait des « deux » devis d'infrastructure ; il y en a quatre.
Formation et wiki differes : la reconstruction from-zero est le test de cette
documentation, et enseigner une procedure que personne n'a executee serait
enseigner une hypothese.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 12:33:35 -04:00
< div class = " hint-dep " style = " margin-top:10px " > Lecture seule . Éditer < code > docs / couches - deploiement . yml < / code > ; généré dans < code > playbooks / site . yml < / code > par < code > make site < / code > . < / div >
< div class = " hint-dep " style = " margin-top:10px " > < strong > Vérifier ce qui a été remonté . < / strong > Un déploiement vert ne dit pas que le service rend son service . Les < strong > devis de service < / strong > lisent le système en marche et le comparent à ce que le plan dérive — ils n ' écrivent rien et sortent en erreur s ' il y a un écart :
< code > make identite - plan < / code > , < code > make certificats - plan < / code > , < code > make expositions - plan < / code > , < code > make postgresql - plan < / code > , < code > make courriel - plan < / code > .
Détail dans < code > docs / devis - services . md < / code > . < / div > < / div > ` ;
2026-07-07 11:34:55 -04:00
}
}
function dessinerFlux ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
if ( ! fluxRegistre . length ) { cible . innerHTML = ' <div class= " vide " style= " padding:14px " >Aucun flux (meta/flux.yml).</div> ' ; return ; }
const lignes = fluxRegistre . map ( f = > ` < tr >
< td class = " mono " > $ { echapper ( f . role ) } < / td >
< td > < span class = " sens sens-$ { echapper(f.sens)} " > $ { echapper ( f . sens ) } < / span > < / td >
< td class = " mono " > $ { echapper ( f . port ) } / $ { echapper ( f . protocole ) } < / td >
< td class = " mono " > $ { echapper ( f . pair ) } < / td >
< td > < span class = " chip chi-$ { echapper(f.chiffrement)} " > $ { echapper ( f . chiffrement ) } < / span > < / td >
< td class = " flux-raison " > $ { echapper ( f . raison ) } < / td > < / tr > ` ) . join ( ' ' ) ;
cible . innerHTML = ` < div class = " section-grille " >
< div class = " section-tete " > Flux réseau — matrice d ' audit <span class= " compte " >$ {fluxRegistre.length} </span>
< span class = " hint-dep " style = " margin-left:auto " > ingress = le rôle écoute · egress = il se connecte · source de nftables + audit < / span > < / div >
< table class = " tbl-lecture " > < thead > < tr > < th > Rôle < / th > < th > Sens < / th > < th > Port < / th > < th > Pair < / th > < th > Chiffrement < / th > < th > Raison < / th > < / tr > < / thead >
< tbody > $ { lignes } < / tbody > < / table > < / div > ` ;
}
2026-07-07 15:57:47 -04:00
let devisReseauTexte = ' ' ;
2026-08-03 08:52:22 -04:00
let devisFrontiereTexte = ' ' ;
2026-07-23 14:13:36 -04:00
function tableauFlotte ( inst , coll , fmodeles , findexPris ) {
Aligne le GUI et la doc sur le multi-instances
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>
2026-07-23 11:57:59 -04:00
if ( ! inst . length ) return ' <div class= " vide " style= " padding:14px " >Aucune instance découverte (dépôts frères avec plan/nomenclature.yml).</div> ' ;
const banniere = Object . keys ( coll | | { } ) . length
? ` < div class = " hint-dep " style = " color:var(--danger);border:1px solid var(--danger);border-radius:8px;padding:8px 10px;margin-bottom:8px " > ⚠ COLLISION d ' index entre instances fédérées : $ { Object.entries(coll).map(([i, n]) => ' index ' + i + ' → ' + n.map(echapper).join( ' , ' )).join( ' ; ' )} — mêmes VLAN/VMID sur le trunk.</div>`
: ' ' ;
const lignes = inst . map ( i = > {
const vlan = i . vlans ? ( i . vlans [ 0 ] + ' - ' + i . vlans [ 1 ] ) : ' — ' ;
const fed = i . federe ? ' <span class= " chip " >fédérée</span> ' : ' <span class= " chip " style= " opacity:.6 " >local</span> ' ;
const prod = i . production ? ' <span class= " chip chi-tls-requis " >prod</span> ' : ' ' ;
2026-07-23 12:12:29 -04:00
const action = i . actif
? ' <span class= " chip chi-tls-requis " >active</span> '
2026-09-16 14:57:39 -04:00
: ` < button type = " button " class = " fantome " $ { peut ( ' editer_le_plan ' ) ? ' '
: ` disabled title = " $ { echapper(pourquoiPas( ' editer_le_plan ' ))} " style = " opacity:.45 " ` } onclick = " basculerInstance( ' $ { echapper(i.nom)} ' , $ { i.production ? ' true ' : ' false ' }) " > Activer < / button > ` ;
Aligne le GUI et la doc sur le multi-instances
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>
2026-07-23 11:57:59 -04:00
return ` < tr >
< td class = " mono " > $ { i . actif ? ' ★ ' : ' ' } $ { echapper ( i . nom ) } < / td >
< td class = " mono " > $ { i . index } < / td >
< td class = " mono " > $ { vlan } < / td >
2026-07-23 12:12:29 -04:00
< td > $ { fed } $ { prod } < / td >
< td > $ { action } < / td > < / tr > ` ;
Aligne le GUI et la doc sur le multi-instances
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>
2026-07-23 11:57:59 -04:00
} ) . join ( ' ' ) ;
2026-07-23 14:13:36 -04:00
const optModeles = ( fmodeles | | [ ] ) . map ( m = > ` < option value = " $ { echapper(m)} " > $ { echapper ( m ) } < / option > ` ) . join ( ' ' ) ;
const creer = ( fmodeles | | [ ] ) . length ? `
< div class = " section-tete " style = " margin-top:14px " > Créer une instance depuis un modèle < / div >
< div style = " display:flex;flex-wrap:wrap;gap:8px;align-items:flex-end " >
< label class = " champ " style = " max-width:14rem " > < span > Modèle < / span > < select id = " ci-modele " > $ { optModeles } < / select > < / label >
< label class = " champ " style = " max-width:12rem " > < span > Nom ( dépôt frère ) < / span > < input id = " ci-nom " placeholder = " OPS-ClientX " > < / label >
< label class = " champ " style = " max-width:9rem " > < span > Index < / span > < input id = " ci-index " type = " number " placeholder = " libre " title = " Index unique ; $ { (findexPris||[]).length ? ' déjà pris : ' + (findexPris||[]).join( ' , ' ) : ' aucun pris ' } " > < / label >
2026-09-16 14:57:39 -04:00
< button type = " button " class = " primaire " $ { peut ( ' editer_le_plan ' ) ? ' '
: ` disabled title = " $ { echapper(pourquoiPas( ' editer_le_plan ' ))} " style = " opacity:.45 " ` } onclick = " creerInstance() " > Créer < / button >
2026-07-23 14:13:36 -04:00
< / div >
< div class = " hint-dep " style = " margin-top:4px " > Copie le modèle vers < code > . . / & lt ; nom & gt ; < / code > et fixe l ' index. Ne bascule pas : reste à renseigner intrants + voûte, puis « Activer ».$ { (findexPris||[]).length ? ' Index déjà pris : ' + (findexPris||[]).join( ' , ' ) + ' . ' : ' ' }</div>` : ' ' ;
2026-07-23 12:12:29 -04:00
return ` $ { banniere } < table class = " tbl-lecture " > < thead > < tr > < th > Instance < / th > < th > Index < / th > < th > VLAN < / th > < th > Statut < / th > < th > Active < / th > < / tr > < / thead > < tbody > $ { lignes } < / tbody > < / table >
2026-07-23 14:13:36 -04:00
< div class = " hint-dep " style = " margin-top:6px " > ★ = active ( symlink < code > instance < / code > ) . « Activer » bascule toutes les vues et les déploiements vers cette instance . < / div >
$ { creer } ` ;
}
async function creerInstance ( ) {
const modele = ( document . getElementById ( ' ci-modele ' ) | | { } ) . value | | ' ' ;
const nom = ( ( document . getElementById ( ' ci-nom ' ) | | { } ) . value | | ' ' ) . trim ( ) ;
const index = ( ( document . getElementById ( ' ci-index ' ) | | { } ) . value | | ' ' ) . trim ( ) ;
if ( ! nom ) { message ( ' Donne un nom de dépôt (ex. OPS-ClientX). ' , ' erreur ' ) ; return ; }
if ( ! confirm ( ` Créer l ' instance « $ {nom} » depuis le modèle « $ {modele} »$ { index ? ' ( index ' + index + ' ) ' : ' ' } ? \\ n \\ nCopie le modèle vers ../$ {nom} . L ' instance active n ' est PAS changée.`)) return;
try {
const rep = await fetch ( ' /api/instance-creer ' , {
method : ' POST ' , headers : { ' Content-Type ' : ' application/json ' , ' X-Jeton ' : JETON } ,
body : JSON . stringify ( { nom , modele , index } ) } ) ;
const data = await rep . json ( ) ;
if ( ! rep . ok ) { message ( data . erreur | | ' Création refusée. ' , ' erreur ' ) ; return ; }
message ( ' Instance « ' + nom + ' » créée. Renseigne intrants + voûte, puis « Activer ». ' , ' ok ' ) ;
dessinerReseau ( ) ;
} catch ( e ) { message ( ' Erreur : ' + e . message , ' erreur ' ) ; }
2026-07-23 12:12:29 -04:00
}
async function basculerInstance ( nom , estProd ) {
const avert = estProd
? ` \\n \\n ⚠ « $ { nom } » est une instance de PRODUCTION . Déployer y touchera l ' infra réelle.`
: ' ' ;
if ( ! confirm ( ` Basculer l ' instance active vers « $ {nom} » ? \\ n \\ nToutes les vues et les déploiements viseront alors « $ {nom} ».$ {avert} `)) return;
try {
const rep = await fetch ( ' /api/instance-utiliser ' , {
method : ' POST ' , headers : { ' Content-Type ' : ' application/json ' , ' X-Jeton ' : JETON } ,
body : JSON . stringify ( { nom } ) } ) ;
const data = await rep . json ( ) ;
if ( ! rep . ok ) { message ( data . erreur | | ' Bascule refusée. ' , ' erreur ' ) ; return ; }
message ( ' Instance active : ' + nom + ' — rechargement… ' , ' ok ' ) ;
setTimeout ( ( ) = > location . reload ( ) , 500 ) ;
} catch ( e ) { message ( ' Erreur : ' + e . message , ' erreur ' ) ; }
Aligne le GUI et la doc sur le multi-instances
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>
2026-07-23 11:57:59 -04:00
}
2026-07-07 15:57:47 -04:00
async function dessinerReseau ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
Aligne le GUI et la doc sur le multi-instances
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>
2026-07-23 11:57:59 -04:00
cible . innerHTML = ' <div class= " vide " style= " padding:14px " >Lecture de la fédération…</div> ' ;
2026-07-07 15:57:47 -04:00
try {
2026-08-03 08:52:22 -04:00
const [ ri , rd , rf ] = await Promise . all ( [
Aligne le GUI et la doc sur le multi-instances
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>
2026-07-23 11:57:59 -04:00
fetch ( ' /api/instances ' , { headers : { ' X-Jeton ' : JETON } } ) ,
fetch ( ' /api/devis-reseau ' , { headers : { ' X-Jeton ' : JETON } } ) ,
2026-08-03 08:52:22 -04:00
fetch ( ' /api/devis-opnsense ' , { headers : { ' X-Jeton ' : JETON } } ) ,
Aligne le GUI et la doc sur le multi-instances
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>
2026-07-23 11:57:59 -04:00
] ) ;
2026-08-03 08:52:22 -04:00
const di = await ri . json ( ) , data = await rd . json ( ) , dfr = await rf . json ( ) ;
Aligne le GUI et la doc sur le multi-instances
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>
2026-07-23 11:57:59 -04:00
if ( ! rd . ok ) { cible . innerHTML = ' <div class= " vide " style= " padding:14px " > ' + echapper ( data . erreur | | ' Indisponible. ' ) + ' </div> ' ; return ; }
2026-07-07 15:57:47 -04:00
devisReseauTexte = data . config | | ' ' ;
2026-07-23 14:13:36 -04:00
const flotte = ` < div class = " section-grille " > < div class = " section-tete " > Flotte d ' instances <span class= " compte " >$ { (di.instances || []).length}</span></div>$ { tableauFlotte(di.instances || [], di.collisions || {} , di.modeles || [], di.index_pris || [])}</div>`;
Aligne le GUI et la doc sur le multi-instances
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>
2026-07-23 11:57:59 -04:00
const devis = devisReseauTexte
? ` < div class = " section-grille " style = " margin-top:12px " > < div class = " section-tete " > Devis config réseau — switches ( instances fédérées )
< button type = " button " class = " fantome " style = " margin-left:auto " onclick = " copierDevis() " > Copier < / button > < / div >
< pre class = " devis-reseau " > $ { echapper ( devisReseauTexte ) } < / pre > < / div > `
: ' <div class= " hint-dep " style= " margin-top:12px " >Aucune instance fédérée : pas de devis convergé à générer.</div> ' ;
2026-08-03 08:52:22 -04:00
devisFrontiereTexte = dfr . config | | ' ' ;
const frontiere = devisFrontiereTexte
? ` < div class = " section-grille " style = " margin-top:12px " > < div class = " section-tete " > Devis config réseau — frontière nord / sud ( OPNsense )
< button type = " button " class = " fantome " style = " margin-left:auto " onclick = " copierDevisFrontiere() " > Copier < / button > < / div >
< pre class = " devis-reseau " > $ { echapper ( devisFrontiereTexte ) } < / pre > < / div > `
: ` < div class = " hint-dep " style = " margin-top:12px " > Devis frontière indisponible $ { dfr . erreur ? ' : ' + echapper ( dfr . erreur ) : ' ' } . < / div > ` ;
2026-09-16 14:57:39 -04:00
/ / LA FABRIC APPARTIENT A L ' HEBERGEUR, ET LE DIRE CHANGE CE QU ' ON LIT . Chez un
/ / locataire , ces devis sont calcules depuis une COPIE locale de la carte du
/ / cluster — et une copie avait deja diverge : deux listes de stockages
/ / contradictoires pour le meme materiel . Le panneau reste visible ( l ' omettre
/ / ferait croire que la fabric n ' existe pas) ; il cesse d ' etre lu comme une source .
const avisFabric = peut ( ' fabric ' ) ? ' ' :
` < div class = " note-fabric " style = " margin-bottom:12px;padding:10px 12px;border-left:3px solid #b9860b;background:color-mix(in srgb,#b9860b 12 % ,var(--surface));font-size:12.5px " >
< b > Lecture seule — la fabric appartient à l ' hébergeur.</b> $ { echapper(pourquoiPas( ' fabric ' ))}
Cette console configure un écosystème ; elle ne pose ni VLAN , ni route , ni règle de frontière . < / div > ` ;
cible . innerHTML = avisFabric + flotte + devis + frontiere ;
2026-07-07 15:57:47 -04:00
} catch ( e ) {
cible . innerHTML = ' <div class= " vide " style= " padding:14px " >Erreur : ' + echapper ( e . message ) + ' </div> ' ;
}
}
function copierDevis ( ) {
if ( navigator . clipboard ) navigator . clipboard . writeText ( devisReseauTexte )
. then ( ( ) = > message ( ' Devis réseau copié — prêt à coller sur le switch. ' , ' ok ' ) , ( ) = > { } ) ;
}
2026-08-03 08:52:22 -04:00
function copierDevisFrontiere ( ) {
if ( navigator . clipboard ) navigator . clipboard . writeText ( devisFrontiereTexte )
. then ( ( ) = > message ( ' Devis frontière copié. ' , ' ok ' ) , ( ) = > { } ) ;
}
2026-07-07 15:57:47 -04:00
2026-07-07 11:34:55 -04:00
function dessinerCouches ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
if ( ! couchesRegistre . length ) { cible . innerHTML = ' <div class= " vide " style= " padding:14px " >Ordre de déploiement indisponible.</div> ' ; return ; }
const blocs = couchesRegistre . map ( ( c , i ) = > ` < div class = " couche " >
< div class = " couche-tete " > < span class = " couche-idx " > $ { i } < / span > < b > $ { echapper ( c . nom ) } < / b > < span class = " compte " > $ { c . groupes . length } < / span > < / div >
< div class = " couche-groupes " > $ { c . groupes . map ( g = > ` < span class = " chip mono " > $ { echapper ( g ) } < / span > ` ) . join ( ' ' ) } < / div > < / div > `
) . join ( ' <div class= " couche-fleche " >↓</div> ' ) ;
cible . innerHTML = ` < div class = " section-grille " >
< div class = " section-tete " > Ordre de déploiement — couches < span class = " compte " > $ { couchesRegistre . length } < / span >
< span class = " hint-dep " style = " margin-left:auto " > make deployer - tout / myDay déroule dans cet ordre · tri topo intra - couche < / span > < / div >
< div class = " couches-pile " > $ { blocs } < / div > < / div > ` ;
}
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
/ / - - - Matrice serveurs x integrations - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/ / Pourquoi cette vue : la fiche de detail montre les integrations d ' UN serveur.
/ / Savoir qui n ' a pas de sauvegarde demandait donc d ' ouvrir chaque fiche — et un
/ / trou ( deux serveurs sans supervision ) est reste invisible jusqu ' a ce qu ' un
/ / devis de pare - feu l ' enumere. Ici il se lit dans une colonne.
function servicesRendus ( nom ) {
return new Set ( applications . filter ( a = > a . hote == = nom ) . map ( a = > a . groupe ) ) ;
}
function estExempt ( role , rendus ) {
const p = integrationsUniv [ role ] ;
return ! ! ( p & & p . sauf_role & & rendus . has ( p . sauf_role ) ) ;
}
function basculerIntegrationDe ( i , role , actif ) {
const s = serveurs [ i ] ; if ( ! s ) return ;
const set = new Set ( listeDepuisTexte ( s . integrations ) ) ;
if ( actif ) set . add ( role ) ; else set . delete ( role ) ;
s . integrations = Array . from ( set ) . join ( ' , ' ) ;
marquerServeursModifie ( ) ; dessinerIntegrations ( ) ;
}
function dessinerIntegrations ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
const univ = Object . keys ( integrationsUniv ) . sort ( ) ;
/ / Colonnes facultatives : celles proposees + toute valeur deja presente dans le
/ / plan ( un role retire de roles / ne doit pas faire disparaitre sa colonne ) .
const dansPlan = new Set ( ) ;
serveurs . forEach ( s = > listeDepuisTexte ( s . integrations ) . forEach ( r = > dansPlan . add ( r ) ) ) ;
const opt = [ . . . new Set ( [ . . . integrationsDispo , . . . dansPlan ] ) ] . filter ( r = > ! integrationsUniv [ r ] ) . sort ( ) ;
const colonnes = [ . . . univ , . . . opt ] ;
const lignes = serveurs . map ( ( s , i ) = > [ s , i ] ) . filter ( ( [ s ] ) = > correspondServeur ( s ) ) ;
if ( ! colonnes . length ) { cible . innerHTML = ' <div class= " vide " style= " padding:14px " >Aucun rôle client_* dans roles/.</div> ' ; return ; }
if ( ! lignes . length ) { cible . innerHTML = ' <div class= " vide " style= " padding:14px " >Aucun serveur ne correspond au filtre.</div> ' ; return ; }
const couvert = { } ; colonnes . forEach ( r = > couvert [ r ] = 0 ) ;
const corps = lignes . map ( ( [ s , i ] ) = > {
const rendus = servicesRendus ( s . nom ) ;
const set = new Set ( listeDepuisTexte ( s . integrations ) ) ;
const cellules = colonnes . map ( role = > {
if ( integrationsUniv [ role ] ) {
if ( estExempt ( role , rendus ) ) {
return ` < td class = " mx-c mx-exempt " title = " Exempté : cet hôte rend $ { echapper(integrationsUniv[role].sauf_role)} — il ne s ' intègre pas à lui-même " > — < / td > ` ;
}
couvert [ role ] + + ;
return ` < td class = " mx-c mx-univ " title = " $ { echapper(integrationsUniv[role].raison || ' ' )} " > ✓ < / td > ` ;
}
if ( set . has ( role ) ) couvert [ role ] + + ;
return ` < td class = " mx-c " > < input type = " checkbox " $ { set . has ( role ) ? ' checked ' : ' ' } `
+ ` onchange = " basculerIntegrationDe($ {i} , ' $ { echapper(role)} ' , this.checked) " `
+ ` title = " $ { echapper(role)} sur $ { echapper(s.nom)} " > < / td > ` ;
} ) . join ( ' ' ) ;
const sel = i == = selServeur ? ' class= " mx-sel " ' : ' ' ;
return ` < tr $ { sel } > < th class = " mx-h " onclick = " setVue( ' serveurs ' ); choisirServeur($ {i} ) " title = " Ouvrir la fiche de $ { echapper(s.nom)} " > `
+ ` $ { echapper ( s . nom | | ' (sans nom) ' ) } < span class = " mx-fn " > $ { echapper ( s . fonction | | ' ' ) } < / span > < / th > $ { cellules } < / tr > ` ;
} ) . join ( ' ' ) ;
const n = lignes . length ;
const tetes = colonnes . map ( role = > {
const u = ! ! integrationsUniv [ role ] ;
return ` < th class = " mx-t$ { u ? ' mx-t-univ ' : ' ' } " title = " $ { u ? ' Politique du rôle — s \' applique à tout hôte ' : ' Facultative — un choix, serveur par serveur ' } " > `
+ ` < span > $ { echapper ( role . replace ( / ^ client_ / , ' ' ) ) } < / span > < / th > ` ;
} ) . join ( ' ' ) ;
const pieds = colonnes . map ( role = > {
const c = couvert [ role ] ;
const cls = integrationsUniv [ role ] ? ' mx-p mx-univ ' : ( c == = n ? ' mx-p mx-plein ' : ' mx-p ' ) ;
return ` < td class = " $ {cls} " > $ { c } / $ { n } < / td > ` ;
} ) . join ( ' ' ) ;
cible . innerHTML = ` < div class = " section-grille " >
< div class = " section-tete " > Intégrations — matrice < span class = " compte " > $ { n } × $ { colonnes . length } < / span >
< span class = " hint-dep " style = " margin-left:auto " > Les colonnes ✓ sont la politique des rôles ; les cases sont les choix , éditables ici < / span > < / div >
< div class = " mx-cadre " > < table class = " mx " >
< thead > < tr > < th class = " mx-t mx-coin " > serveur < / th > $ { tetes } < / tr > < / thead >
< tbody > $ { corps } < / tbody >
< tfoot > < tr > < th class = " mx-h mx-coin " > couverture < / th > $ { pieds } < / tr > < / tfoot >
< / table > < / div > < / div > ` ;
}
function legendeIntegrations ( ) {
const univ = Object . entries ( integrationsUniv ) . sort ( ) ;
const lignes = univ . map ( ( [ role , p ] ) = > ` < div class = " mx-leg " > < span class = " mx-univ " > ✓ < / span > < code > $ { echapper ( role ) } < / code >
< div class = " hint-dep " > $ { echapper ( p . raison | | ' ' ) } $ { p . sauf_role ? ` < b > Exemption < / b > : l ' hôte qui rend <code>$ { echapper(p.sauf_role)}</code>.` : ' ' }</div></div>`).join( ' ' );
document . getElementById ( ' detail ' ) . innerHTML = ` < div class = " detail " >
< div class = " section-tete " > Deux genres d ' intégration</div>
< div class = " hint-dep " > < b > Universelle < / b > — aucun choix de cible : un seul Prometheus , un seul Loki , une seule AC .
Déclarée < b > une fois par le rôle < / b > ( < code > roles / & lt ; rôle & gt ; / meta / integration . yml < / code > ) , elle s ' applique à tout hôte
et ne se décoche pas . Le plan < b > refuse < / b > de la redire : deux sources finiraient par diverger . < / div >
$ { lignes }
< div class = " hint-dep " style = " margin-top:12px " > < b > Facultative < / b > — un vrai choix , serveur par serveur , écrit dans
< code > plan / serveurs . yml < / code > . Coche dans la matrice , puis < b > Sauvegarder < / b > . < / div >
< div class = " hint-dep " style = " margin-top:12px " > La ligne < b > couverture < / b > ne juge pas : une colonne à < code > 7 / 14 < / code >
peut être exactement juste . Elle rend le motif < em > visible < / em > — c ' est au lecteur de savoir si les sept manquants
sont des décisions ou des oublis . < / div > < / div > ` ;
}
2026-06-24 20:17:46 -04:00
function basesDeApp ( app , hoteNom ) {
return bdApplis . filter ( a = > {
const p = a . portee | | ' groupe ' ;
return ( p == = ' application ' & & a . consommateur == = app . id )
| | ( p == = ' groupe ' & & a . consommateur == = app . groupe )
| | ( p == = ' hote ' & & a . consommateur == = hoteNom ) ;
} ) ;
}
function chargerBases ( data ) {
const b = data . bases | | { serveurs_bd : { } , bases_donnees : { } } ;
bdServeurs = Object . entries ( b . serveurs_bd | | { } ) . map ( ( [ nom , s ] ) = > ( {
nom , type : s . type | | ' postgres ' , hote : s . hote | | ' ' , port : s . port | | 5432 , groupe : s . groupe | | ' ' } ) ) ;
bdApplis = Object . entries ( b . bases_donnees | | { } ) . map ( ( [ cle , x ] ) = > ( {
cle , serveur : x . serveur | | ' ' , base : x . base | | ' ' , proprietaire : x . proprietaire | | ' ' , secret : x . secret | | ' ' ,
consommateur : x . consommateur | | ' ' , portee : x . portee | | ' groupe ' , usage : x . usage | | ' principale ' } ) ) ;
basesModifie = false ;
}
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
function chargerDomaines ( data ) {
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
/ / Les valeurs restent DE LEUR TYPE ( les listes restent des listes ) : le formulaire
/ / est genere depuis le schema , qui sait deja qu ' une liste se saisit par virgules.
/ / L ' ancien chargement les aplatissait en texte, et la sauvegarde devait deviner
/ / comment les reconstruire .
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
const dp = ( ( data . domaines | | { } ) . domaines_publics ) | | { } ;
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
domaines = Object . entries ( dp ) . map ( ( [ nom , x ] ) = > Object . assign ( { nom } , x ) ) ;
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
domainesModifie = false ;
}
function marquerDomainesModifie ( ) { domainesModifie = true ; majIndicateurSauvegarde ( ) ; }
function choisirDomaine ( i ) { selDomaine = i ; dessiner ( ) ; }
function definirDomaine ( champ , v ) {
const d = domaines [ selDomaine ] ; if ( ! d ) return ;
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
poserChemin ( d , champ , v ) ; marquerDomainesModifie ( ) ;
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
if ( champ == = ' nom ' ) { const t = document . querySelector ( ' #tuile-dom- ' + selDomaine + ' .carte-nom ' ) ; if ( t ) { t . textContent = v | | ' (sans nom) ' ; t . className = ' carte-nom ' + ( v ? ' ' : ' sans ' ) ; } }
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
else dessinerDetail ( ) ;
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
}
function ajouterDomaine ( ) {
const edgeDefaut = groupes . includes ( ' serveur_nginx ' ) ? ' serveur_nginx ' : ( groupes [ 0 ] | | ' ' ) ;
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
domaines . unshift ( { nom : ' ' , autorite : ' auto-heberge ' , edge : edgeDefaut , dnssec : false , secondaires : [ ] } ) ;
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
selDomaine = 0 ; marquerDomainesModifie ( ) ; dessiner ( ) ;
const c = document . querySelector ( ' #detail .grille input ' ) ; if ( c ) c . focus ( ) ;
}
function retirerDomaine ( ) {
const d = domaines [ selDomaine ] ; if ( ! d ) return ;
if ( d . nom & & ! confirm ( ' Retirer le domaine « ' + d . nom + ' » ? ' ) ) return ;
domaines . splice ( selDomaine , 1 ) ;
selDomaine = domaines . length ? Math . min ( selDomaine , domaines . length - 1 ) : null ;
marquerDomainesModifie ( ) ; dessiner ( ) ;
}
function carteDomaine ( d , i ) {
return ` < button type = " button " class = " carte $ { i === selDomaine ? ' selectionnee ' : ' ' } " id = " tuile-dom-$ {i} " onclick = " choisirDomaine($ {i} ) " >
< div class = " carte-haut " > < span class = " badge-bd " > zone < / span > $ { d . dnssec ? ' <span class= " carte-vmid " >dnssec</span> ' : ' ' } < / div >
< div class = " carte-nom $ { d.nom ? ' ' : ' sans ' } " > $ { echapper ( d . nom | | ' (sans nom) ' ) } < / div >
< div class = " carte-ip " > $ { echapper ( d . autorite | | ' — ' ) } < / div >
< / button > ` ;
}
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
/ / - - - - VUE NOMENCLATURE - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/ /
/ / Le dernier registre que le GUI ne savait pas ecrire du tout ( 2026 - 09 - 08 ) . Il n ' est
/ / pas un registre comme les autres : il ne decrit pas des objets , il decrit la REGLE
/ / dont VMID , VLAN , adresse et passerelle se derivent . D ' ou deux partis pris :
/ /
/ / - ` index ` est montre mais PAS editable ici . Le fichier le dit lui - meme : il est
/ / RECU du site , pas decide par le tenant , et ` make underlay ` refuse tout ecart .
/ / Il s ' ecrit par le panneau Intrants, qui est le chemin de l ' intrant recu .
/ / - chaque fonction affiche CE QU ' ELLE DERIVE (zone, VLAN, 3e octet) et combien de
/ / VM la portent : sans ca , changer un chiffre ici est un geste a l ' aveugle.
function chargerNomEdit ( data ) {
/ / Copie de travail : editer ` nomenclature ` en place ferait suivre les indices
/ / derives de la vue Serveurs a des valeurs non encore enregistrees .
nomEdit = JSON . parse ( JSON . stringify ( data . nomenclature | | { } ) ) ;
nomModifie = false ;
}
function marquerNomModifie ( ) { nomModifie = true ; majIndicateurSauvegarde ( ) ; }
function selNomInfo ( ) { if ( ! selNom ) return null ; const i = selNom . indexOf ( ' : ' ) ; return { kind : selNom . slice ( 0 , i ) , cle : selNom . slice ( i + 1 ) } ; }
function choisirNom ( k ) { selNom = k ; dessiner ( ) ; }
function definirNom ( chemin , v ) {
/ / UN SEUL setter pour toute la profondeur : le generateur transmet une clef
/ / POINTEE ( ` reservations . passerelle ` , ` fonctions . idm . service ` ) .
const parts = String ( chemin ) . split ( ' . ' ) ;
let o = nomEdit ;
for ( let i = 0 ; i < parts . length - 1 ; i + + ) {
if ( o [ parts [ i ] ] == null | | typeof o [ parts [ i ] ] != = ' object ' ) o [ parts [ i ] ] = { } ;
o = o [ parts [ i ] ] ;
}
const feuille = parts [ parts . length - 1 ] ;
/ / Les entiers restent des ENTIERS : le plan est relu par le moteur , pas par un
/ / navigateur , et ` categorie : " 6 " ` ne derive pas la meme chose que ` categorie : 6 ` .
o [ feuille ] = ( v != = ' ' & & v != null & & String ( v ) . match ( / ^ - ? \d + $ / ) ) ? parseInt ( v , 10 ) : v ;
marquerNomModifie ( ) ;
if ( chemin . startsWith ( ' fonctions. ' ) | | chemin . startsWith ( ' categories. ' ) ) dessiner ( ) ;
}
function renommerNom ( table , ancienne , nouvelle ) {
nouvelle = String ( nouvelle | | ' ' ) . trim ( ) ;
if ( ! nouvelle | | nouvelle == = ancienne ) return ;
const t = nomEdit [ table ] | | { } ;
if ( Object . prototype . hasOwnProperty . call ( t , nouvelle ) ) { message ( ' « ' + nouvelle + ' » existe déjà. ' , ' erreur ' ) ; return ; }
/ / On preserve l ' ORDRE : le fichier est lu par des humains, et un renommage ne doit
/ / pas remonter la clef en tete .
const refait = { } ;
Object . keys ( t ) . forEach ( k = > { refait [ k == = ancienne ? nouvelle : k ] = t [ k ] ; } ) ;
nomEdit [ table ] = refait ;
selNom = ( table == = ' categories ' ? ' cat: ' : ' fon: ' ) + nouvelle ;
marquerNomModifie ( ) ; dessiner ( ) ;
}
function ajouterZone ( ) {
const cats = nomEdit . categories | | ( nomEdit . categories = { } ) ;
/ / La prochaine zone LIBRE , pas « la derniere + 1 » : un trou se rebouche .
let n = 1 ; while ( cats [ n ] | | cats [ String ( n ) ] ) n + + ;
cats [ n ] = { libelle : ' ' } ;
selNom = ' cat: ' + n ; marquerNomModifie ( ) ; dessiner ( ) ;
}
function ajouterFonction ( ) {
const f = nomEdit . fonctions | | ( nomEdit . fonctions = { } ) ;
let nom = ' nouvelle ' , i = 2 ; while ( f [ nom ] ) nom = ' nouvelle- ' + ( i + + ) ;
const premiereZone = Object . keys ( nomEdit . categories | | { } ) [ 0 ] ;
f [ nom ] = { categorie : premiereZone != null ? parseInt ( premiereZone , 10 ) : 1 , service : 1 } ;
selNom = ' fon: ' + nom ; marquerNomModifie ( ) ; dessiner ( ) ;
}
function retirerNom ( ) {
const info = selNomInfo ( ) ; if ( ! info ) return ;
const table = info . kind == = ' cat ' ? ' categories ' : ' fonctions ' ;
if ( info . kind == = ' fon ' & & vmsPortant ( info . cle ) . length ) {
message ( ' « ' + info . cle + ' » est encore portée par ' + vmsPortant ( info . cle ) . length + ' VM. ' , ' erreur ' ) ;
return ;
}
if ( info . kind == = ' cat ' & & fonctionsDansZone ( info . cle ) . length ) {
message ( ' Zone ' + info . cle + ' encore utilisée par ' + fonctionsDansZone ( info . cle ) . length + ' fonction(s). ' , ' erreur ' ) ;
return ;
}
delete ( nomEdit [ table ] | | { } ) [ info . cle ] ;
selNom = null ; marquerNomModifie ( ) ; dessiner ( ) ;
}
function vmsPortant ( fonction ) { return hotes . filter ( h = > fonctionDe ( h . nom ) . fonction == = fonction ) ; }
function fonctionsDansZone ( zone ) {
return Object . entries ( nomEdit . fonctions | | { } ) . filter ( ( [ , f ] ) = > String ( f . categorie ) == = String ( zone ) ) . map ( ( [ n ] ) = > n ) ;
}
function deriveDe ( fonction ) {
/ / Miroir de ` inventory_rules . deriver_nomenclature ` , cote ecran . Il ne DECIDE rien :
/ / il montre ce que le moteur derivera , pour que le chiffre saisi ait un sens visible .
const f = ( nomEdit . fonctions | | { } ) [ fonction ] ;
if ( ! f ) return null ;
const c = ( nomEdit . categories | | { } ) [ f . categorie ] | | ( nomEdit . categories | | { } ) [ String ( f . categorie ) ] ;
const idx = ( nomEdit . index != null ) ? nomEdit . index : 9 ;
const cat = Number ( f . categorie ) ;
if ( ! c | | isNaN ( cat ) ) return null ;
return { libelle : c . libelle | | ' ? ' , vlan : 1000 + idx * 10 + cat ,
base3 : ' 10. ' + idx + ' . ' + ( 15 + cat ) , bloc : ( Number ( f . service ) * 10 ) } ;
}
function carteZone ( cle ) {
const c = ( nomEdit . categories | | { } ) [ cle ] | | { } ;
const sel = selNom == = ' cat: ' + cle ? ' on ' : ' ' ;
const n = fonctionsDansZone ( cle ) . length ;
return ` < button type = " button " class = " carte$ {sel} " onclick = " choisirNom( ' cat:$ { echapper(cle)} ' ) " >
< div class = " carte-nom " > $ { echapper ( c . libelle | | ' (sans libellé) ' ) } < / div >
< div class = " carte-ip " > zone $ { echapper ( cle ) } · $ { n } fonction ( s ) < / div > < / button > ` ;
}
function carteFonction ( nom ) {
const sel = selNom == = ' fon: ' + nom ? ' on ' : ' ' ;
const d = deriveDe ( nom ) ;
const nb = vmsPortant ( nom ) . length ;
return ` < button type = " button " class = " carte$ {sel} " onclick = " choisirNom( ' fon:$ { echapper(nom)} ' ) " >
< div class = " carte-nom " > $ { echapper ( nom ) } < / div >
< div class = " carte-ip " > $ { d ? echapper ( d . libelle ) + ' · VLAN ' + d . vlan : ' <zone inconnue> ' } · $ { nb } VM < / div > < / button > ` ;
}
function dessinerNomenclature ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
const reg = schemaPlan . nomenclature ;
if ( ! reg ) { cible . innerHTML = ' <div class= " vide " style= " padding:14px " >Aucun schéma — lancer <code>make schema</code>.</div> ' ; return ; }
const props = ( reg . entite | | { } ) . properties | | { } ;
/ / Le seed , montre et NON editable ici ( il vient du site ) .
const idx = nomEdit . index == null ? ' — ' : nomEdit . index ;
const seed = ` < label class = " champ " title = " $ { echapper((props.index || {} ).description || ' ' )} " >
< span > $ { echapper ( ( props . index | | { } ) . title | | ' Index ' ) } < / span >
< input value = " $ { echapper(idx)} " disabled > < / label >
< div class = " ch-stub " style = " grid-column:1/-1 " > L ' index est <b>alloué par le site</b>, pas décidé ici
( < code > make underlay < / code > refuse tout écart ) . Il s ' édite dans le panneau <b>Intrants</b>.</div>`;
const cidr = props . cidr_hote ? champDepuisSchema ( ' cidr_hote ' , props . cidr_hote , nomEdit . cidr_hote , nomEdit , ' definirNom ' ) : ' ' ;
const resv = props . reservations ? champDepuisSchema ( ' reservations ' , props . reservations , nomEdit . reservations , nomEdit , ' definirNom ' ) : ' ' ;
const zones = Object . keys ( nomEdit . categories | | { } ) ;
const fons = Object . keys ( nomEdit . fonctions | | { } ) ;
cible . innerHTML = `
< div class = " section-grille " >
< div class = " section-tete " > Modèle d ' adressage</div>
< div class = " grille " > $ { seed } $ { cidr } $ { resv } < / div >
< / div >
< div class = " section-grille " >
< div class = " section-tete " > Zones de sécurité < span class = " compte " > $ { zones . length } < / span >
< button type = " button " class = " fantome " style = " margin-left:auto " onclick = " ajouterZone() " > + Zone < / button > < / div >
$ { zones . length ? ` < div class = " grille-cartes " > $ { zones . map ( carteZone ) . join ( ' ' ) } < / div > `
: ' <div class= " vide " style= " padding:14px " >Aucune zone. Sans zone déclarée, aucune fonction ne dérive d \' adresse.</div> ' }
< / div >
< div class = " section-grille " >
< div class = " section-tete " > Fonctions < span class = " compte " > $ { fons . length } < / span >
< button type = " button " class = " fantome " style = " margin-left:auto " onclick = " ajouterFonction() " > + Fonction < / button > < / div >
$ { fons . length ? ` < div class = " grille-cartes " > $ { fons . map ( carteFonction ) . join ( ' ' ) } < / div > `
: ' <div class= " vide " style= " padding:14px " >Aucune fonction déclarée.</div> ' }
< / div > ` ;
}
function inspecteurNomenclature ( ) {
const cible = document . getElementById ( ' detail ' ) ;
const info = selNomInfo ( ) ;
const reg = schemaPlan . nomenclature | | { } ;
const props = ( reg . entite | | { } ) . properties | | { } ;
if ( ! info ) {
cible . innerHTML = ' <div class= " detail " ><div class= " vide " ><b>Nomenclature</b> '
+ ' Le modèle dont tout l \' adressage dérive. Choisis une zone ou une fonction.</div></div> ' ;
return ;
}
const table = info . kind == = ' cat ' ? ' categories ' : ' fonctions ' ;
const entree = ( nomEdit [ table ] | | { } ) [ info . cle ] ;
if ( ! entree ) { selNom = null ; cible . innerHTML = ' ' ; return ; }
const specEntree = ( props [ table ] | | { } ) . additionalProperties | | { } ;
const corps = sousFormulaireDepuisSchema ( specEntree , entree , table + ' . ' + info . cle , ' definirNom ' ) ;
let consequence = ' ' ;
if ( info . kind == = ' fon ' ) {
const d = deriveDe ( info . cle ) ;
const vms = vmsPortant ( info . cle ) ;
consequence = ` < div class = " section-tete " style = " margin:12px 2px 4px " > Ce que cette fonction dérive < / div > `
+ ( d ? ` < div class = " intr-ro " > Zone < b > $ { echapper ( d . libelle ) } < / b > · VLAN < b > $ { d . vlan } < / b >
· sous - réseau < b > $ { echapper ( d . base3 ) } .0 / $ { echapper ( nomEdit . cidr_hote | | 24 ) } < / b >
· bloc d ' hôtes <b>.$ {d.bloc} x</b></div>`
: ' <div class= " intr-ro " >Zone inconnue — aucune adresse ne se dérive.</div> ' )
+ ` < div class = " section-tete " style = " margin:12px 2px 4px " > VM portant cette fonction < span class = " compte " > $ { vms . length } < / span > < / div > `
+ ( vms . length ? ` < div class = " ch-bases " > $ { vms . map ( h = > ` < div class = " ch-base " > < span class = " mono " > $ { echapper ( h . nom ) } < / span > < / div > ` ) . join ( ' ' ) } < / div > `
: ' <div class= " ch-stub " >aucune — cette fonction peut être retirée</div> ' ) ;
} else {
const fs = fonctionsDansZone ( info . cle ) ;
consequence = ` < div class = " section-tete " style = " margin:12px 2px 4px " > Fonctions dans cette zone < span class = " compte " > $ { fs . length } < / span > < / div > `
+ ( fs . length ? ` < div class = " ch-bases " > $ { fs . map ( f = > ` < div class = " ch-base " > < span class = " mono " > $ { echapper ( f ) } < / span > < / div > ` ) . join ( ' ' ) } < / div > `
: ' <div class= " ch-stub " >aucune — cette zone peut être retirée</div> ' ) ;
}
const libelleCle = info . kind == = ' cat ' ? ' Numéro de zone ' : ' Nom de la fonction ' ;
cible . innerHTML = ` < article class = " detail " >
< div class = " detail-tete " > < div class = " detail-nom " > < span class = " detail-nom-txt " > $ { echapper ( info . cle ) } < / span > < / div >
< span class = " grandit " > < / span >
< button type = " button " class = " danger " title = " Retirer " onclick = " retirerNom() " > ✕ < / button > < / div >
< div class = " grille " >
< label class = " champ " style = " grid-column:1/-1 " > < span > $ { libelleCle } < / span >
< input value = " $ { echapper(info.cle)} " onchange = " renommerNom( ' $ {table} ' , ' $ { echapper(info.cle)} ' , this.value) " > < / label >
< / div >
$ { corps }
$ { consequence }
< / article > ` ;
}
async function sauvegarderNomenclature ( ) {
const rep = await fetch ( ' /api/nomenclature ' , { method : ' POST ' ,
headers : { ' Content-Type ' : ' application/json ' , ' X-Jeton ' : JETON } ,
body : JSON . stringify ( nomEdit ) } ) ;
const data2 = await rep . json ( ) ;
if ( ! rep . ok ) { message ( data2 . erreur | | ' Sauvegarde refusée. ' , ' erreur ' ) ; return ; }
schemaPlan = data2 . schema | | schemaPlan ;
nomenclature = data2 . nomenclature | | nomenclature ;
chargerNomEdit ( data2 ) ; dessiner ( ) ;
message ( ' Nomenclature sauvegardée. ' , ' ok ' ) ;
}
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
function dessinerDomaines ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
const tuiles = domaines . length
? ` < div class = " grille-cartes " > $ { domaines . map ( ( d , i ) = > carteDomaine ( d , i ) ) . join ( ' ' ) } < / div > `
: ' <div class= " vide " style= " padding:14px " >Aucun domaine. Une zone DNS = parent des expositions (FQDN) derrière l \' edge.</div> ' ;
cible . innerHTML = ` < div class = " section-grille " >
< div class = " section-tete " > Domaines < span class = " compte " > $ { domaines . length } < / span > < button type = " button " class = " fantome " style = " margin-left:auto " onclick = " ajouterDomaine() " > + Domaine < / button > < / div >
$ { tuiles } < / div > ` ;
}
function expositionsSous ( zone ) {
if ( ! zone ) return [ ] ;
return applications . flatMap ( a = > listeDepuisTexte ( a . expose ) ) . filter ( f = > f == = zone | | f . endsWith ( ' . ' + zone ) ) ;
}
function inspecteurDomaine ( ) {
const cible = document . getElementById ( ' detail ' ) ;
if ( selDomaine == null | | ! domaines [ selDomaine ] ) {
cible . innerHTML = ' <div class= " detail " ><div class= " vide " ><b>Aucun domaine sélectionné</b>Choisis une tuile, ou ajoute un domaine.</div></div> ' ;
return ;
}
const d = domaines [ selDomaine ] ;
const fqdns = expositionsSous ( d . nom ) ;
const liens = fqdns . length
? ` < div class = " ch-bases " > $ { fqdns . map ( f = > ` < div class = " ch-base " > < span class = " mono " > $ { echapper ( f ) } < / span > < / div > ` ) . join ( ' ' ) } < / div > `
: ' <div class= " ch-stub " >aucune exposition sous cette zone</div> ' ;
cible . innerHTML = ` < article class = " detail " >
< div class = " detail-tete " > < div class = " detail-nom " > < span class = " detail-nom-txt " > $ { echapper ( d . nom | | ' (sans nom) ' ) } < / span > < / div >
< span class = " grandit " > < / span >
< button type = " button " class = " danger " title = " Retirer ce domaine " onclick = " retirerDomaine() " > ✕ < / button > < / div >
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
$ { formulaireDepuisSchema ( ' domaines_publics ' , d , ' definirDomaine ' , d . nom , " definirDomaine.bind(null, ' nom ' ) " ) }
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
< div class = " section-tete " style = " margin:12px 2px 4px " > Expositions sous cette zone < / div >
$ { liens }
< / article > ` ;
}
async function sauvegarderDomaines ( ) {
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
/ / SAUVEGARDE DERIVEE DU SCHEMA : les champs ecrits sont ceux qu ' il declare. Un
/ / champ ajoute au registre apparait au formulaire ET arrive au fichier — sans quoi
/ / il serait saisissable et perdu en silence , le pire des deux mondes .
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
const dp = { } ;
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
const propsD = ( ( schemaPlan . domaines_publics | | { } ) . entite | | { } ) . properties | | { } ;
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
domaines . forEach ( d = > {
if ( ! String ( d . nom ) . trim ( ) ) return ;
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
const o = { } ;
Object . keys ( propsD ) . forEach ( cle = > {
const v = d [ cle ] ;
if ( v == = undefined | | v == = null | | v == = ' ' ) {
if ( propsD [ cle ] . default != = undefined ) o [ cle ] = propsD [ cle ] . default ;
return ;
}
o [ cle ] = ( propsD [ cle ] . type == = ' boolean ' ) ? ! ! v : v ;
} ) ;
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
dp [ d . nom ] = o ;
} ) ;
const rep = await fetch ( ' /api/domaines ' , {
method : ' POST ' , headers : { ' Content-Type ' : ' application/json ' , ' X-Jeton ' : JETON } , body : JSON . stringify ( { domaines_publics : dp } )
} ) ;
const data = await rep . json ( ) ;
if ( ! rep . ok ) { message ( data . erreur | | ' Sauvegarde refusée. ' , ' erreur ' ) ; return ; }
chargerDomaines ( data ) ; dessinerDomaines ( ) ;
message ( ' Domaines sauvegardés. ' , ' ok ' ) ;
}
2026-06-24 20:17:46 -04:00
function chargerApplications ( data ) {
const a = ( data . applications | | { } ) . applications | | { } ;
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
/ / Les valeurs gardent LEUR TYPE : le formulaire est genere depuis le schema , qui
/ / sait qu ' une liste se saisit par virgules. Les aplatir ici obligeait la sauvegarde
/ / a deviner comment les reconstruire .
applications = Object . entries ( a ) . map ( ( [ id , x ] ) = > Object . assign ( { id } , x , {
2026-07-22 21:32:42 -04:00
liens : ( x . liens | | [ ] ) . map ( l = > ( { vers : l . vers | | ' ' , role : l . role | | ' ' } ) ) } ) ) ;
2026-06-24 20:17:46 -04:00
applicationsModifie = false ;
}
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
/ / - - - - FORMULAIRES GENERES DEPUIS LE SCHEMA - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/ /
/ / Le schema ( docs / audit / schema - plan . json , ` make schema ` ) decrit la FORME des
/ / registres ; ces trois fonctions la rendent . Un champ ajoute au plan apparait donc
/ / a l ' ecran sans qu ' on touche a ce fichier — c ' est tout l ' objet .
/ /
/ / Ce qu ' elles ne font PAS : valider. La coherence reste aux `valider_*` du serveur,
/ / qui refusent la sauvegarde . Un formulaire genere ne saurait pas dire qu ' un
/ / consommateur designe une application inexistante .
function sourceDeValeurs ( nom ) {
/ / Les listes fermees , resolues a l ' execution. C ' est ce qui rend l ' hote fantome
/ / IMPOSSIBLE A SAISIR , au lieu d ' etre refuse apres coup.
if ( nom == = ' serveurs ' ) return hotes . map ( h = > h . nom ) . filter ( Boolean ) ;
if ( nom == = ' applications ' ) return applications . map ( x = > x . id ) . filter ( Boolean ) ;
if ( nom == = ' groupes_operationnels ' ) return groupes ;
if ( nom == = ' serveurs_bd ' ) return bdServeurs . map ( x = > x . nom ) . filter ( Boolean ) ;
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
/ / ` nomenclature ` , PAS ` data . nomenclature ` : il n ' existe aucun `data` global. Cette
/ / ligne dormait — la vue Serveurs , seule a employer cette source , avait un
/ / formulaire ecrit a la main . Elle a leve a la seconde ou le generateur l ' a prise.
if ( nom == = ' nomenclature.fonctions ' ) return Object . keys ( ( nomEdit . fonctions | | nomenclature . fonctions ) | | { } ) ;
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
/ / La zone se choisit dans les zones DECLAREES — et on la lit dans la copie en
/ / cours d ' edition, pour qu ' une zone ajoutee a l ' instant soit offerte tout de suite.
/ / ` { v , t } ` : la valeur ecrite au plan reste le numero , l ' ecran montre le libelle.
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
/ / ` edge ` designe un GROUPE Ansible : ` instancier ` compare cette valeur aux
/ / groupes d ' un hote pour lui deriver ses SAN. Offrir des HOTES ici produirait un
/ / certificat correct sur un nom que personne ne peut appeler .
if ( nom == = ' groupes_edge ' ) return groupes . filter ( g = > g . startsWith ( ' serveur_nginx ' ) ) ;
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
/ / Les listes que le SITE a declarees dans les intrants ( noeuds , stockages ) .
if ( nom . startsWith ( ' intrants. ' ) ) {
const v = ( ( intrants | | { } ) . valeurs | | { } ) [ nom . slice ( 9 ) ] ;
return Array . isArray ( v ) ? v : [ ] ;
}
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
if ( nom == = ' nomenclature.categories ' ) {
const cats = ( nomEdit . categories | | { } ) ;
return Object . keys ( cats ) . map ( k = > ( { v : k , t : k + ' — ' + ( ( cats [ k ] | | { } ) . libelle | | ' ? ' ) } ) ) ;
}
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
return [ ] ;
}
function champDepuisSchema ( cle , spec , valeur , entite , surChangement ) {
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
/ / UN CHAMP PEUT AVOIR SON PROPRE EDITEUR ( ` x - editeur ` ) , plus riche que ce que le
/ / schema sait dire : la matrice des integrations montre aussi les universelles et
/ / leurs exemptions ; l ' editeur de liens contraint le role a ce que le groupe
/ / porteur accepte . Le generateur s ' efface — la vue les rend elle-meme.
if ( spec [ ' x-editeur ' ] ) return ' ' ;
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
const titre = echapper ( spec . title | | cle ) ;
const aide = spec . description ? ` title = " $ { echapper(spec.description)} " ` : ' ' ;
2026-09-08 16:34:25 -04:00
/ / Marqueur discret : le schema SAIT ce qui est requis , le formulaire ecrit a la
/ / main ne le montrait pas . Une asterisque plutot que le mot « requis » , qui se
/ / collait au libelle et se lisait « Base requis » .
const req = ( spec [ ' x-requis ' ] ? ' <abbr class= " dsn " title= " Champ requis " style= " text-decoration:none " > *</abbr> ' : ' ' ) ;
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
const appel = ( expr ) = > ` $ { surChangement } ( ' $ {cle} ' , $ { expr } ) ` ;
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
/ / ` data - champ ` remplace l ' ancien `data-v`, pose a la main sur trois champs de la
/ / vue Serveurs . La validation client ( ` serveurValeurValide ` ) le lit ; sans lui ,
/ / elle aurait cherche des elements qui n ' existent plus et serait passee au vert
/ / sur ZERO champ . Un controle qui ne trouve rien ne dit pas « tout va bien » .
const marque = ` data - champ = " $ { echapper(cle)} " ` ;
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
/ / Liste fermee : soit fixe ( enum ) , soit alimentee a l ' execution, soit dependante
/ / d ' un autre champ de la meme entite.
let options = null ;
if ( spec . enum ) options = spec . enum ;
else if ( spec [ ' x-source-valeurs ' ] ) options = sourceDeValeurs ( spec [ ' x-source-valeurs ' ] ) ;
else if ( spec [ ' x-source-selon ' ] ) {
const s = spec [ ' x-source-selon ' ] ;
const pivot = ( entite | | { } ) [ s . champ ] ;
options = sourceDeValeurs ( ( s . cas | | { } ) [ pivot ] | | s . defaut ) ;
}
if ( options ) {
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
/ / L ' option VIDE n ' est pas « rien » : c ' est « le defaut ». Quand le schema nomme
/ / l ' intrant qui le porte (`x-defaut-intrant`), on l ' affiche , plutot que de
/ / laisser l ' operateur deviner ce qu ' un champ vide produira .
const nomDef = spec [ ' x-defaut-intrant ' ] ;
const valDef = nomDef ? ( ( intrants | | { } ) . valeurs | | { } ) [ nomDef ] : null ;
const libVide = valDef ? ` ( défaut : $ { valDef } ) ` : ' — ' ;
const opts = [ ` < option value = " " > $ { echapper ( libVide ) } < / option > ` ] . concat ( options . map ( o = > {
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
const v = ( o & & typeof o == = ' object ' ) ? o . v : o ;
const txt = ( o & & typeof o == = ' object ' ) ? o . t : o ;
return ` < option value = " $ { echapper(v)} " $ { String ( v ) == = String ( valeur == null ? ' ' : valeur ) ? ' selected ' : ' ' } > $ { echapper ( txt ) } < / option > ` ;
} ) ) . join ( ' ' ) ;
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
return ` < label class = " champ " $ { aide } > < span > $ { titre } $ { req } < / span > < select $ { marque } onchange = " $ { appel( ' this.value ' )} " > $ { opts } < / select > < / label > ` ;
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
}
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
if ( spec . type == = ' object ' & & spec . properties ) {
/ / BLOC FIXE ( ` sous_champs ` au schema ) : ses cases une fois , sans « ajouter » .
/ / La clef transmise au setter est POINTEE ( ` reservations . passerelle ` ) — c ' est ce
/ / qui permet a un seul setter de servir toute la profondeur .
const sousReq = new Set ( spec . required | | [ ] ) ;
const cases = Object . keys ( spec . properties ) . map ( sc = > champDepuisSchema (
cle + ' . ' + sc ,
Object . assign ( { } , spec . properties [ sc ] , { ' x-requis ' : sousReq . has ( sc ) } ) ,
( valeur | | { } ) [ sc ] , valeur , surChangement ) ) . join ( ' ' ) ;
return ` < div style = " grid-column:1/-1 " $ { aide } > < div class = " section-tete " > $ { titre } < / div > `
+ ` < div class = " grille " > $ { cases } < / div > < / div > ` ;
}
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
if ( spec . type == = ' boolean ' ) {
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
return ` < label class = " champ " $ { aide } > < span > $ { titre } $ { req } < / span > < input type = " checkbox " $ { marque } $ { valeur ? ' checked ' : ' ' } onchange = " $ { appel( ' this.checked ' )} " > < / label > ` ;
}
/ / Repere de saisie : la valeur REELLEMENT derivee pour cette entite , lue dans
/ / l ' inventaire genere. Le formulaire ecrit a la main annoncait « 2048 » et « 2 » —
/ / des constantes inventees , alors que ` deriver_ressources ` calcule depuis les roles
/ / portes . Un repere faux fait croire qu ' on connait la valeur.
const derive = spec [ ' x-defaut-derive ' ] ;
let repere = ' ' ;
if ( derive & & entite & & entite . nom ) {
const h = hotes . find ( x = > x . nom == = entite . nom ) ;
if ( h & & h [ derive ] != null & & h [ derive ] != = ' ' ) repere = ` placeholder = " dérivé : $ { echapper(h[derive])} " ` ;
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
}
if ( spec . type == = ' integer ' ) {
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
return ` < label class = " champ " $ { aide } > < span > $ { titre } $ { req } < / span > < input type = " number " $ { marque } $ { repere } value = " $ { echapper(valeur == null ? ' ' : valeur)} " oninput = " $ { appel( ' this.value ' )} " > < / label > ` ;
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
}
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
if ( spec . type == = ' array ' & & ( spec . items | | { } ) . properties ) {
return listeDepuisSchema ( cle , spec , valeur , surChangement ) ;
}
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
if ( spec . type == = ' array ' ) {
/ / Une liste de textes se saisit separee par des virgules : le YAML reste une
/ / liste , la saisie reste lisible .
const v = Array . isArray ( valeur ) ? valeur . join ( ' , ' ) : ' ' ;
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
return ` < label class = " champ " $ { aide } > < span > $ { titre } $ { req } < / span > < input $ { marque } value = " $ { echapper(v)} " placeholder = " a, b " oninput = " $ { appel( ' this.value.split( \' , \' ).map(x=>x.trim()).filter(Boolean) ' )} " > < / label > ` ;
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
}
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
return ` < label class = " champ " $ { aide } > < span > $ { titre } $ { req } < / span > < input $ { marque } $ { repere } value = " $ { echapper(valeur == null ? ' ' : valeur)} " oninput = " $ { appel( ' this.value ' )} " > < / label > ` ;
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
}
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
function formulaireDepuisSchema ( nomRegistre , entite , surChangement , cleCourante , surChangementCle , nomClef ) {
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
const reg = schemaPlan [ nomRegistre ] | | null ;
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
if ( ! reg ) return ' <div class= " vide " >Aucun schéma pour ce registre — lancer <code>make schema</code>.</div> ' ;
const props = ( reg . entite | | { } ) . properties | | { } ;
const requis = new Set ( ( reg . entite | | { } ) . required | | [ ] ) ;
const morceaux = [ ] ;
if ( reg [ ' x-clef ' ] & & surChangementCle ) {
const k = reg [ ' x-clef ' ] ;
morceaux . push ( ` < label class = " champ " title = " $ { echapper(k.description || ' ' )} " > < span > $ { echapper ( k . title ) } < / span > ` +
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
` < input data - champ = " $ { echapper(nomClef || ' cle ' )} " value = " $ { echapper(cleCourante == null ? ' ' : cleCourante)} " oninput = " ($ {surChangementCle} )(this.value) " > < / label > ` ) ;
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
}
Object . keys ( props ) . forEach ( cle = > {
const spec = Object . assign ( { } , props [ cle ] , { ' x-requis ' : requis . has ( cle ) } ) ;
morceaux . push ( champDepuisSchema ( cle , spec , ( entite | | { } ) [ cle ] , entite , surChangement ) ) ;
} ) ;
return ` < div class = " grille " > $ { morceaux . join ( ' ' ) } < / div > ` ;
}
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
function poserChemin ( objet , chemin , valeur ) {
/ / ` reservations . passerelle ` , ` exposition .0 . cible ` : un seul mecanisme pour toute
/ / la profondeur , tableaux compris . Les setters de chaque vue s ' appuient dessus.
const parts = String ( chemin ) . split ( ' . ' ) ;
let o = objet ;
for ( let i = 0 ; i < parts . length - 1 ; i + + ) {
const suivant = parts [ i + 1 ] ;
if ( o [ parts [ i ] ] == null | | typeof o [ parts [ i ] ] != = ' object ' ) {
o [ parts [ i ] ] = String ( suivant ) . match ( / ^ \d + $ / ) ? [ ] : { } ;
}
o = o [ parts [ i ] ] ;
}
o [ parts [ parts . length - 1 ] ] = valeur ;
}
function listeDepuisSchema ( cle , spec , valeur , surChangement ) {
/ / UNE LISTE D ' OBJETS de forme connue (`entrees` au schema) : une sous-fiche par
/ / element , plus un bouton d ' ajout. L ' ajout et le retrait passent par le MEME
/ / setter que les champs — on lui remet le tableau entier , serialise dans
/ / l ' attribut. Pas de fonction globale a resoudre au clic : ce qui marche sous le
/ / banc marche dans le navigateur .
const items = spec . items | | { } ;
const arr = Array . isArray ( valeur ) ? valeur : [ ] ;
const defauts = { } ;
Object . keys ( items . properties | | { } ) . forEach ( c = > {
if ( items . properties [ c ] . default != = undefined ) defauts [ c ] = items . properties [ c ] . default ;
} ) ;
const ajout = echapper ( JSON . stringify ( arr . concat ( [ defauts ] ) ) ) ;
const lignes = arr . map ( ( e , i ) = > {
const retrait = echapper ( JSON . stringify ( arr . filter ( ( _ , j ) = > j != = i ) ) ) ;
return ` < div class = " ch-base " style = " display:block " >
< div style = " display:flex;align-items:center;gap:6px " >
< span class = " dsn " > #${i + 1}</span><span class="grandit"></span>
< button type = " button " class = " danger " title = " Retirer "
onclick = " $ {surChangement} ( ' $ {cle} ' , $ {retrait} ) " > ✕ < / button > < / div >
$ { sousFormulaireDepuisSchema ( items , e , cle + ' . ' + i , surChangement ) } < / div > ` ;
} ) . join ( ' ' ) ;
return ` < div style = " grid-column:1/-1 " >
< div class = " section-tete " > $ { echapper ( spec . title | | cle ) } < span class = " compte " > $ { arr . length } < / span >
< button type = " button " class = " fantome " style = " margin-left:auto "
onclick = " $ {surChangement} ( ' $ {cle} ' , $ {ajout} ) " > + Ajouter < / button > < / div >
$ { arr . length ? lignes : ' <div class= " ch-stub " >aucune entrée</div> ' } < / div > ` ;
}
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
function sousFormulaireDepuisSchema ( specEntree , entree , prefixe , surChangement ) {
/ / Une ligne de table ( ` entree ` au schema ) : les memes cases qu ' une entite, mais
/ / sous une clef pointee , pour que le setter sache ou poser la valeur .
const props = ( specEntree | | { } ) . properties | | { } ;
const requis = new Set ( ( specEntree | | { } ) . required | | [ ] ) ;
const cases = Object . keys ( props ) . map ( c = > champDepuisSchema (
prefixe + ' . ' + c , Object . assign ( { } , props [ c ] , { ' x-requis ' : requis . has ( c ) } ) ,
( entree | | { } ) [ c ] , entree , surChangement ) ) . join ( ' ' ) ;
return ` < div class = " grille " > $ { cases } < / div > ` ;
}
2026-06-24 20:17:46 -04:00
function dsnBase ( a ) {
const s = bdServeurs . find ( x = > x . nom == = a . serveur ) ;
if ( ! s ) return ' (serveur inconnu : ' + echapper ( a . serveur | | ' — ' ) + ' ) ' ;
return ` $ { s . type } : / / $ { a . proprietaire | | ' ? ' } : * * * * @ $ { s . hote | | ' ? ' } : $ { s . port | | ' ? ' } / $ { a . base | | ' ? ' } ` ;
}
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
function marquerBasesModifie ( ) { basesModifie = true ; majIndicateurSauvegarde ( ) ; }
function selBaseInfo ( ) { if ( ! selBase ) return null ; const p = selBase . split ( ' : ' ) ; return { kind : p [ 0 ] , i : + p [ 1 ] } ; }
function choisirBase ( key ) { selBase = key ; dessiner ( ) ; }
function definirServeurBd ( champ , v ) {
const info = selBaseInfo ( ) ; if ( ! info | | info . kind != = ' srv ' ) return ;
const s = bdServeurs [ info . i ] ; if ( ! s ) return ;
s [ champ ] = v ; marquerBasesModifie ( ) ;
if ( champ == = ' nom ' ) { const t = document . querySelector ( ' #tuile-base-srv- ' + info . i + ' .carte-nom ' ) ; if ( t ) t . textContent = v | | ' (sans nom) ' ; }
}
function definirBaseBd ( champ , v ) {
const info = selBaseInfo ( ) ; if ( ! info | | info . kind != = ' bd ' ) return ;
const a = bdApplis [ info . i ] ; if ( ! a ) return ;
a [ champ ] = v ; marquerBasesModifie ( ) ;
if ( champ == = ' cle ' ) { const t = document . querySelector ( ' #tuile-base-bd- ' + info . i + ' .carte-nom ' ) ; if ( t ) t . textContent = v | | ' (sans id) ' ; }
else if ( champ == = ' serveur ' | | champ == = ' portee ' ) dessinerDetail ( ) ;
}
function ajouterServeurBd ( ) { bdServeurs . unshift ( { nom : ' ' , type : ' postgres ' , hote : ' ' , port : 5432 , groupe : ' ' } ) ; selBase = ' srv:0 ' ; marquerBasesModifie ( ) ; dessiner ( ) ; }
function retirerServeurBd ( ) { const info = selBaseInfo ( ) ; if ( ! info | | info . kind != = ' srv ' ) return ; bdServeurs . splice ( info . i , 1 ) ; selBase = null ; marquerBasesModifie ( ) ; dessiner ( ) ; }
function ajouterBaseBd ( ) { bdApplis . unshift ( { cle : ' ' , serveur : ( bdServeurs [ 0 ] | | { } ) . nom | | ' ' , base : ' ' , proprietaire : ' ' , secret : ' ' , consommateur : ' ' , portee : ' groupe ' , usage : ' principale ' } ) ; selBase = ' bd:0 ' ; marquerBasesModifie ( ) ; dessiner ( ) ; }
function retirerBaseBd ( ) { const info = selBaseInfo ( ) ; if ( ! info | | info . kind != = ' bd ' ) return ; bdApplis . splice ( info . i , 1 ) ; selBase = null ; marquerBasesModifie ( ) ; dessiner ( ) ; }
function carteBaseServeur ( s , i ) {
const key = ' srv: ' + i ;
return ` < button type = " button " class = " carte $ { selBase === key ? ' selectionnee ' : ' ' } " id = " tuile-base-srv-$ {i} " onclick = " choisirBase( ' $ {key} ' ) " >
< div class = " carte-haut " > < span class = " badge-bd " > $ { echapper ( s . type | | ' bd ' ) } < / span > $ { s . port ? ` < span class = " carte-vmid " > : $ { echapper ( s . port ) } < / span > ` : ' ' } < / div >
< div class = " carte-nom $ { s.nom ? ' ' : ' sans ' } " > $ { echapper ( s . nom | | ' (sans nom) ' ) } < / div >
< div class = " carte-ip " > $ { echapper ( s . hote | | ' — hôte ? ' ) } < / div >
< / button > ` ;
}
function carteBaseDonnee ( a , i ) {
const key = ' bd: ' + i ;
return ` < button type = " button " class = " carte $ { selBase === key ? ' selectionnee ' : ' ' } " id = " tuile-base-bd-$ {i} " onclick = " choisirBase( ' $ {key} ' ) " >
< div class = " carte-haut " > < span class = " badge-bd " > $ { echapper ( a . portee | | ' groupe ' ) } < / span > < / div >
< div class = " carte-nom $ { a.cle ? ' ' : ' sans ' } " > $ { echapper ( a . cle | | ' (sans id) ' ) } < / div >
< div class = " carte-ip " > $ { echapper ( a . base | | ' — ' ) } @ $ { echapper ( a . serveur | | ' ? ' ) } < / div >
< / button > ` ;
2026-06-24 20:17:46 -04:00
}
function dessinerBases ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
const srv = bdServeurs . length ? ` < div class = " grille-cartes " > $ { bdServeurs . map ( ( s , i ) = > carteBaseServeur ( s , i ) ) . join ( ' ' ) } < / div > ` : ' <div class= " vide " style= " padding:12px " >Aucun serveur de BD.</div> ' ;
const bd = bdApplis . length ? ` < div class = " grille-cartes " > $ { bdApplis . map ( ( a , i ) = > carteBaseDonnee ( a , i ) ) . join ( ' ' ) } < / div > ` : ' <div class= " vide " style= " padding:12px " >Aucune base.</div> ' ;
cible . innerHTML = ` < div class = " section-grille " > < div class = " section-tete " > Serveurs de bases < span class = " compte " > $ { bdServeurs . length } < / span > < button type = " button " class = " fantome " style = " margin-left:auto " onclick = " ajouterServeurBd() " > + Serveur BD < / button > < / div > $ { srv } < / div >
< div class = " section-grille " > < div class = " section-tete " > Bases applicatives < span class = " compte " > $ { bdApplis . length } < / span > < button type = " button " class = " fantome " style = " margin-left:auto " onclick = " ajouterBaseBd() " > + Base < / button > < / div > $ { bd } < / div > ` ;
}
function inspecteurBase ( ) {
const cible = document . getElementById ( ' detail ' ) ;
const info = selBaseInfo ( ) ;
if ( ! info ) { cible . innerHTML = ' <div class= " detail " ><div class= " vide " ><b>Aucun élément sélectionné</b>Choisis un serveur de base ou une base à gauche.</div></div> ' ; return ; }
if ( info . kind == = ' srv ' ) {
const s = bdServeurs [ info . i ] ;
if ( ! s ) { cible . innerHTML = ' <div class= " detail " ><div class= " vide " >Introuvable.</div></div> ' ; return ; }
const ici = bdApplis . filter ( a = > a . serveur == = s . nom ) ;
const iciHtml = ici . length ? ` < div class = " ch-bases " > $ { ici . map ( a = > ` < div class = " ch-base " > < span class = " badge-bd " > $ { echapper ( a . portee | | ' groupe ' ) } < / span > < span class = " mono " > $ { echapper ( a . base | | ' ? ' ) } < / span > ( $ { echapper ( a . cle ) } ) < / div > ` ) . join ( ' ' ) } < / div > ` : ' <div class= " ch-stub " >aucune base sur ce serveur</div> ' ;
cible . innerHTML = ` < article class = " detail " >
< div class = " detail-tete " > < div class = " detail-nom " > < span class = " detail-nom-txt " > $ { echapper ( s . nom | | ' (sans nom) ' ) } < / span > < / div > < span class = " grandit " > < / span > < span class = " badge-bd " > $ { echapper ( s . type | | ' bd ' ) } < / span > < button type = " button " class = " danger " title = " Retirer " onclick = " retirerServeurBd() " > ✕ < / button > < / div >
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
$ { formulaireDepuisSchema ( ' serveurs_bd ' , s , ' definirServeurBd ' , s . nom , " definirServeurBd.bind(null, ' nom ' ) " ) }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
< div class = " section-tete " style = " margin:12px 2px 2px " > Bases hébergées ici < / div > $ { iciHtml }
2026-07-01 15:58:32 -04:00
$ { ( ( ) = > {
const hS = hotes . find ( h = > h . nom == = s . hote ) ;
if ( hS & & hS . etat == = ' actif ' ) { const hiS = hotes . indexOf ( hS ) ;
2026-07-04 14:40:02 -04:00
return ` < div class = " pied " > < span class = " hint-dep " > Déployer applique les rôles sur l ' hôte « $ { echapper(s.hote)} ».</span><span class= " grandit " ></span>$ { estProduction ? ' < span class = " badge-prod " > PROD < / span > ' : ' ' }<button type= " button " class= " $ { estProduction ? ' danger ' : ' primaire ' } " onclick= " lancer( ' pousser ' , $ {hiS} ) " title= " Déployer l ' hôte du serveur de BD " >Déployer</button></div>`; }
2026-07-01 15:58:32 -04:00
return ` < div class = " pied " > < span class = " hint-dep " > $ { s . hote ? ` Active + pousse d ' abord l ' hôte « $ { echapper ( s . hote ) } » . ` : ' Aucun hôte assigné. ' } < / span > < / div > ` ;
} ) ( ) }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
< / article > ` ;
} else {
const a = bdApplis [ info . i ] ;
if ( ! a ) { cible . innerHTML = ' <div class= " detail " ><div class= " vide " >Introuvable.</div></div> ' ; return ; }
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
/ / FORMULAIRE GENERE DEPUIS LE SCHEMA ( 2026 - 09 - 08 ) — plus aucun champ en dur .
/ / Les huit ` < label > ` qui vivaient ici sont derives de docs / audit / schema - plan . json :
/ / libelles , types , listes fermees , et la source CONDITIONNELLE du consommateur
/ / ( elle depend de la portee ) que le schema declare desormais au lieu de la laisser
/ / vivre dans ce fichier . Un champ ajoute au registre apparait sans toucher au JS ,
/ / et P61 refuse qu ' un champ du plan manque au schema.
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
cible . innerHTML = ` < article class = " detail " >
< div class = " detail-tete " > < div class = " detail-nom " > < span class = " detail-nom-txt " > $ { echapper ( a . cle | | ' (sans id) ' ) } < / span > < / div > < span class = " grandit " > < / span > < button type = " button " class = " danger " title = " Retirer " onclick = " retirerBaseBd() " > ✕ < / button > < / div >
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
$ { formulaireDepuisSchema ( ' bases_donnees ' , a , ' definirBaseBd ' , a . cle , " definirBaseBd.bind(null, ' cle ' ) " ) }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
< div class = " dsn mono " style = " margin-top:8px " > $ { echapper ( dsnBase ( a ) ) } < / div >
2026-07-01 15:58:32 -04:00
$ { ( ( ) = > {
const srv = bdServeurs . find ( x = > x . nom == = a . serveur ) ;
const hB = srv ? hotes . find ( h = > h . nom == = srv . hote ) : null ;
if ( hB & & hB . etat == = ' actif ' ) { const hiB = hotes . indexOf ( hB ) ;
2026-07-04 14:40:02 -04:00
return ` < div class = " pied " > < span class = " hint-dep " > Déployer applique les rôles sur « $ { echapper ( srv . hote ) } » ( le serveur de BD qui héberge cette base ) . < / span > < span class = " grandit " > < / span > $ { estProduction ? ' <span class= " badge-prod " >PROD</span> ' : ' ' } < button type = " button " class = " $ { estProduction ? ' danger ' : ' primaire ' } " onclick = " lancer( ' pousser ' , $ {hiB} ) " title = " Déployer le serveur de BD (crée cette base) " > Déployer < / button > < / div > ` ; }
2026-07-01 15:58:32 -04:00
return ` < div class = " pied " > < span class = " hint-dep " > $ { ( srv & & srv . hote ) ? ` Active + pousse d ' abord l ' hôte « $ { echapper ( srv . hote ) } » . ` : ' Serveur de BD sans hôte assigné. ' } < / span > < / div > ` ;
} ) ( ) }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
< / article > ` ;
}
2026-06-24 20:17:46 -04:00
}
async function sauvegarderBases ( ) {
const sd = { } ;
plan : sauvegarder n emportait plus quarante lignes de commentaire
En voulant generer deux formulaires de plus, j ai trouve pire que ce que
je cherchais.
CE QUI ETAIT DEJA LA. Les quatre ecrivains de registre ecrasaient le
fichier au safe_dump. Mesure sur les fichiers reels : domaines.yml 6->3,
applications.yml 27->5, serveurs.yml 18->3. Quarante lignes, detruites
par n importe quel clic sur Sauvegarder dans les vues Serveurs,
Applications ou Domaines. Parmi elles, celle qui explique pourquoi
backup-01 a ete retire, et celle qui dit dans quel ordre les deux roles
du runner s appliquent. C etait l incident du 2026-08-18, jamais corrige
pour les registres du plan. Les quatre passent par _ecrire_registre :
aller-retour a vide identique a l octet, sur les quatre fichiers.
TROIS ECARTS DE SCHEMA, trouves en confrontant le schema aux VALIDATEURS
et non aux seuls plans :
- edge designe un GROUPE, pas un hote. Le schema disait serveurs : un
formulaire genere aurait offert une valeur qu aucun hote ne reconnait,
donc aucun SAN, donc la panne du 2026-08-25 reintroduite ;
- exposition, entierement valide par le moteur, manquait au schema ;
- liens etait items: {type: object} — une liste d objets sans forme.
Et mail, offert par la vue Domaines depuis sa creation, decrit ici comme
un booleen, saisi la-bas comme du texte, lu par rien : retire.
P62 garde tout ca. Elle separe l entite du reste mecaniquement : un
validateur lit son entite par des variables LOCALES, les autres registres
par ses PARAMETRES. Controle negatif rejoue.
LES FORMULAIRES. Serveurs de BD et Domaines sont generes, chargement et
sauvegarde compris. Quatre registres sur six. Le generateur a appris la
liste d objets.
LIMITE : restent serveurs et applications, les deux plus gros ; et je n ai
toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute (62 preuves).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:54:49 -04:00
const propsSrv = ( ( schemaPlan . serveurs_bd | | { } ) . entite | | { } ) . properties | | { } ;
bdServeurs . forEach ( s = > {
if ( ! String ( s . nom ) . trim ( ) ) return ;
const o = { } ;
Object . keys ( propsSrv ) . forEach ( cle = > {
const v = s [ cle ] ;
if ( v == = undefined | | v == = null | | v == = ' ' ) return ;
o [ cle ] = ( propsSrv [ cle ] . type == = ' integer ' ) ? ( parseInt ( v , 10 ) | | v ) : v ;
} ) ;
sd [ s . nom ] = o ;
} ) ;
2026-06-24 20:17:46 -04:00
const bd = { } ;
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
/ / SAUVEGARDE DERIVEE DU SCHEMA ( 2026 - 09 - 08 ) . Les sept champs etaient enumeres ici
/ / en dur : un champ ajoute au registre serait apparu au formulaire genere et
/ / aurait disparu SILENCIEUSEMENT a l ' enregistrement — le pire des deux mondes.
/ / Les valeurs par defaut viennent du schema ( ` default ` ) , plus du JS .
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
const propsBd = ( ( schemaPlan . bases_donnees | | { } ) . entite | | { } ) . properties | | { } ;
GUI : le formulaire des bases est GENERE depuis le schema
Etape 3, sur un seul registre — `bases_donnees`, le plus simple et le seul ou
observe et editable coincidaient deja. Les cinq autres gardent leurs formulaires
ecrits a la main : on ne bascule pas six vues d un coup.
CE QUI DISPARAIT DU JAVASCRIPT
Huit `<label>` en dur, trois constructions de `<option>`, et la regle qui
choisissait la source du consommateur selon la portee. Cette derniere ne vivait
que dans le JS ; elle est desormais DECLAREE au schema (`x-source-selon`), donc
lisible et gardee.
Le formulaire rend exactement les memes huit champs qu avant — verifie en
EXECUTANT le moteur sous node avec le schema et des donnees reelles, pas
seulement en passant `node --check`.
LA BOUCLE EST FERMEE DES DEUX COTES
Le chemin de SAUVEGARDE enumerait lui aussi les sept champs en dur. Un champ
ajoute au registre serait apparu au formulaire genere et aurait disparu
SILENCIEUSEMENT a l enregistrement — le pire des deux mondes. Il derive
maintenant du schema, valeurs par defaut comprises (`default`).
LA SEPARATION FORME / COHERENCE, MONTREE
portee=groupe + consommateur APPLICATION -> REFUSE par valider_bases
portee=application + consommateur app -> ACCEPTE
secret absent -> REFUSE
Le schema a rempli la FORME (les defauts `groupe` et `principale` se sont
poses), le validateur a attrape l INCOHERENCE. Aucune de ces trois regles ne
s exprime en JSON Schema, et vouloir l y mettre creerait la seconde source de
verite que ce depot refuse.
CHAMPS_ECRITS_PAR_GUI COMMENCE A DISPARAITRE
Renomme CHAMPS_ECRITS_A_LA_MAIN, et `bases_donnees` en est SORTIE : sa
couverture se derive du schema. P19 lit desormais `champs_ecrits_par_gui()`, qui
reunit les deux. Le jour ou la table sera vide, elle gardera un mecanisme au
lieu d une liste.
UNE GARDE A CORRIGER AU PASSAGE
`declaration_derive()` verifiait que chaque champ declare apparait dans le
SOURCE du GUI. Pour un registre genere il n y apparait plus — c est le but.
Elle aurait crie sur precisement le progres qu elle devait constater. Les
registres generes en sont exemptes : c est P61 qui tient la promesse pour eux.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
P07 (node --check), P19 (couverture), P61 (schema) : verts.
Reste : les cinq autres vues, et le trou de la nomenclature — que le passage au
generateur fermera par construction, puisque le schema decrit deja `categorie`
et `service`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 15:30:08 -04:00
bdApplis . forEach ( a = > {
if ( ! String ( a . cle ) . trim ( ) ) return ;
const e = { } ;
Object . keys ( propsBd ) . forEach ( cle = > {
const v = a [ cle ] ;
e [ cle ] = ( v == = undefined | | v == = ' ' | | v == = null ) ? ( propsBd [ cle ] . default ? ? v ) : v ;
} ) ;
bd [ a . cle ] = e ;
} ) ;
2026-06-24 20:17:46 -04:00
const rep = await fetch ( ' /api/bases ' , {
method : ' POST ' , headers : { ' Content-Type ' : ' application/json ' , ' X-Jeton ' : JETON } , body : JSON . stringify ( { serveurs_bd : sd , bases_donnees : bd } )
} ) ;
const data = await rep . json ( ) ;
if ( ! rep . ok ) { message ( data . erreur | | ' Sauvegarde refusée. ' , ' erreur ' ) ; return ; }
chargerBases ( data ) ;
dessinerBases ( ) ;
message ( ' Bases de données sauvegardées. ' , ' ok ' ) ;
}
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
function marquerApplicationsModifie ( ) { applicationsModifie = true ; majIndicateurSauvegarde ( ) ; }
function choisirApp ( i ) { selApp = i ; dessiner ( ) ; }
function definirApplication ( champ , v ) {
const a = applications [ selApp ] ; if ( ! a ) return ;
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
poserChemin ( a , champ , v ) ; marquerApplicationsModifie ( ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
if ( champ == = ' id ' ) { const t = document . querySelector ( ' #tuile-app- ' + selApp + ' .carte-nom ' ) ; if ( t ) { t . textContent = v | | ' (sans id) ' ; t . className = ' carte-nom ' + ( v ? ' ' : ' sans ' ) ; } }
2026-07-22 21:32:42 -04:00
else if ( champ == = ' groupe ' ) {
const ok = Object . keys ( liensAcceptes [ a . groupe ] | | { } ) ;
( a . liens | | [ ] ) . forEach ( l = > { if ( l . role & & ! ok . includes ( l . role ) ) l . role = ' ' ; } ) ;
dessinerDetail ( ) ;
}
else if ( champ == = ' hote ' | | champ == = ' port ' ) dessinerDetail ( ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
}
function ajouterApplication ( ) {
2026-07-22 21:32:42 -04:00
applications . unshift ( { id : ' ' , groupe : ( groupes [ 0 ] | | ' ' ) , hote : ' ' , port : ' ' , requiert : ' ' , expose : ' ' , websocket : false , liens : [ ] } ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
selApp = 0 ; marquerApplicationsModifie ( ) ; dessiner ( ) ;
const c = document . querySelector ( ' #detail .grille input ' ) ; if ( c ) c . focus ( ) ;
}
function retirerApplication ( ) {
const a = applications [ selApp ] ; if ( ! a ) return ;
if ( a . id & & ! confirm ( ' Retirer l \' application « ' + a . id + ' » ? ' ) ) return ;
applications . splice ( selApp , 1 ) ;
selApp = applications . length ? Math . min ( selApp , applications . length - 1 ) : null ;
marquerApplicationsModifie ( ) ; dessiner ( ) ;
}
2026-07-22 21:32:42 -04:00
function ajouterLien ( ) {
const a = applications [ selApp ] ; if ( ! a ) return ;
if ( ! a . liens ) a . liens = [ ] ;
const roles = Object . keys ( liensAcceptes [ a . groupe ] | | { } ) ;
a . liens . push ( { vers : ' ' , role : roles . length == = 1 ? roles [ 0 ] : ' ' } ) ;
marquerApplicationsModifie ( ) ; dessinerDetail ( ) ;
}
function retirerLien ( i ) {
const a = applications [ selApp ] ; if ( ! a | | ! a . liens ) return ;
a . liens . splice ( i , 1 ) ; marquerApplicationsModifie ( ) ; dessinerDetail ( ) ;
}
function definirLien ( i , champ , v ) {
const a = applications [ selApp ] ; if ( ! a | | ! a . liens | | ! a . liens [ i ] ) return ;
a . liens [ i ] [ champ ] = v ; marquerApplicationsModifie ( ) ;
if ( champ == = ' role ' ) dessinerDetail ( ) ;
}
function blocLiens ( a ) {
const accepte = liensAcceptes [ a . groupe ] | | { } ;
const roles = Object . keys ( accepte ) ;
if ( ! roles . length ) {
return ` < div class = " ch-stub " > Le rôle « $ { echapper ( a . groupe | | ' — ' ) } » n ' accepte aucun lien
( aucun < span class = " mono " > roles / $ { echapper ( a . groupe | | ' ? ' ) } / meta / liens . yml < / span > ) . < / div > ` ;
}
const cibles = applications . map ( x = > x . id ) . filter ( x = > x & & x != = a . id ) ;
const lignes = ( a . liens | | [ ] ) . map ( ( l , i ) = > {
const spec = accepte [ l . role ] | | { } ;
const vars = Object . keys ( spec . variables | | { } ) ;
const optV = ' <option value= " " >— cible —</option> ' + cibles . map ( c = >
` < option value = " $ { echapper(c)} " $ { c == = l . vers ? ' selected ' : ' ' } > $ { echapper ( c ) } < / option > ` ) . join ( ' ' ) ;
const optR = ' <option value= " " >— rôle —</option> ' + roles . map ( r = >
` < option value = " $ { echapper(r)} " $ { r == = l . role ? ' selected ' : ' ' } > $ { echapper ( r ) } < / option > ` ) . join ( ' ' ) ;
const inconnue = l . vers & & ! cibles . includes ( l . vers ) ;
return ` < div class = " ch-base " style = " gap:8px;align-items:center " >
< select style = " max-width:12rem " onchange = " definirLien($ {i} , ' role ' , this.value) " > $ { optR } < / select >
< span class = " mono " > → < / span >
< select style = " max-width:12rem " onchange = " definirLien($ {i} , ' vers ' , this.value) " > $ { optV } < / select >
$ { inconnue ? ` < span class = " tag " style = " color:#b45309 " > cible « $ { echapper ( l . vers ) } » inconnue < / span > ` : ' ' }
$ { vars . length ? ` < span class = " tag " title = " Variables injectées sur cette application " > $ { echapper ( vars . join ( ' , ' ) ) } < / span > ` : ' ' }
< span class = " grandit " > < / span >
< button type = " button " class = " danger " title = " Retirer ce lien " onclick = " retirerLien($ {i} ) " > ✕ < / button >
< / div > ` ;
} ) . join ( ' ' ) ;
return ` $ { lignes | | ' <div class= " ch-stub " >aucun lien déclaré</div> ' }
< div style = " margin-top:6px " > < button type = " button " class = " fantome " onclick = " ajouterLien() " > + Lien < / button > < / div > ` ;
}
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
function carteApp ( a , i ) {
const exp = listeDepuisTexte ( a . expose ) ;
return ` < button type = " button " class = " carte $ { i === selApp ? ' selectionnee ' : ' ' } " id = " tuile-app-$ {i} " onclick = " choisirApp($ {i} ) " >
< div class = " carte-haut " > < span class = " badge-bd " > app < / span > $ { a . port ? ` < span class = " carte-vmid " > : $ { echapper ( a . port ) } < / span > ` : ' ' } < / div >
< div class = " carte-nom $ { a.id ? ' ' : ' sans ' } " > $ { echapper ( a . id | | ' (sans id) ' ) } < / div >
< div class = " carte-ip " > $ { echapper ( a . hote | | ' — hôte ? ' ) } < / div >
$ { exp . length ? ` < div class = " carte-grps " > < span class = " tag " > $ { echapper ( exp [ 0 ] ) } < / span > $ { exp . length > 1 ? ` < span class = " tag plus " > + $ { exp . length - 1 } < / span > ` : ' ' } < / div > ` : ' ' }
< / button > ` ;
2026-06-24 20:17:46 -04:00
}
function dessinerApplications ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
const tuiles = applications . length
? ` < div class = " grille-cartes " > $ { applications . map ( ( a , i ) = > carteApp ( a , i ) ) . join ( ' ' ) } < / div > `
: ' <div class= " vide " style= " padding:14px " >Aucune application. Une VM peut en porter plusieurs.</div> ' ;
cible . innerHTML = ` < div class = " section-grille " >
< div class = " section-tete " > Applications < span class = " compte " > $ { applications . length } < / span > < button type = " button " class = " fantome " style = " margin-left:auto " onclick = " ajouterApplication() " > + Application < / button > < / div >
$ { tuiles } < / div > ` ;
}
function inspecteurApplication ( ) {
const cible = document . getElementById ( ' detail ' ) ;
if ( selApp == null | | ! applications [ selApp ] ) {
cible . innerHTML = ' <div class= " detail " ><div class= " vide " ><b>Aucune application sélectionnée</b>Choisis une tuile à gauche, ou ajoute une application.</div></div> ' ;
return ;
}
const a = applications [ selApp ] ;
const dsns = basesDeApp ( a , a . hote ) ;
const liens = dsns . length
? ` < div class = " ch-bases " > $ { dsns . map ( b = > ` < div class = " ch-base " > < span class = " badge-bd " > $ { echapper ( b . portee | | ' groupe ' ) } < / span > < span class = " mono " > $ { echapper ( dsnBase ( b ) ) } < / span > < / div > ` ) . join ( ' ' ) } < / div > `
: ' <div class= " ch-stub " >aucune base liée</div> ' ;
cible . innerHTML = ` < article class = " detail " >
< div class = " detail-tete " > < div class = " detail-nom " > < span class = " detail-nom-txt " > $ { echapper ( a . id | | ' (sans id) ' ) } < / span > < / div >
< span class = " grandit " > < / span >
< button type = " button " class = " danger " title = " Retirer cette application " onclick = " retirerApplication() " > ✕ < / button > < / div >
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
$ { formulaireDepuisSchema ( ' applications ' , a , ' definirApplication ' , a . id , " definirApplication.bind(null, ' id ' ) " , ' id ' ) }
2026-07-22 21:32:42 -04:00
< div class = " section-tete " style = " margin:12px 2px 4px " > Liens ( bindings )
< span class = " compte " > $ { ( a . liens | | [ ] ) . length } < / span > < / div >
$ { blocLiens ( a ) }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
< div class = " section-tete " style = " margin:12px 2px 4px " > Bases consommées < / div >
$ { liens }
2026-07-01 15:58:32 -04:00
$ { ( ( ) = > {
const hA = hotes . find ( x = > x . nom == = a . hote ) ;
if ( hA & & hA . etat == = ' actif ' ) {
const hiA = hotes . indexOf ( hA ) ;
2026-07-04 14:40:02 -04:00
return ` < div class = " pied " > < span class = " hint-dep " > Déployer applique les rôles sur l ' hôte porteur « $ { echapper(a.hote)} ».</span><span class= " grandit " ></span>$ { estProduction ? ' < span class = " badge-prod " > PROD < / span > ' : ' ' }<button type= " button " class= " $ { estProduction ? ' danger ' : ' primaire ' } " onclick= " lancer( ' pousser ' , $ {hiA} ) " title= " Déployer l ' hôte porteur ( rend l ' application vivante) " >Déployer</button></div>`;
2026-07-01 15:58:32 -04:00
}
return ` < div class = " pied " > < span class = " hint-dep " > $ { a . hote ? ` Active + pousse d ' abord l ' hôte « $ { echapper ( a . hote ) } » . ` : ' Aucun hôte assigné. ' } < / span > < / div > ` ;
} ) ( ) }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
< / article > ` ;
2026-06-24 20:17:46 -04:00
}
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
function listeDepuisTexte ( v ) {
/ / Elle accepte desormais une VRAIE liste autant qu ' un texte separe par des virgules.
/ / Les chargeurs aplatissaient ` expose ` , ` requiert ` et ` integrations ` en chaine ;
/ / le formulaire genere , lui , attend le type que le schema declare . Plutot que de
/ / toucher aux douze sites d ' appel, la conversion vit ici, en un seul endroit.
if ( Array . isArray ( v ) ) return v . map ( x = > String ( x ) . trim ( ) ) . filter ( Boolean ) ;
return String ( v | | ' ' ) . split ( ' , ' ) . map ( s = > s . trim ( ) ) . filter ( Boolean ) ;
}
2026-06-24 20:17:46 -04:00
async function sauvegarderApplications ( ) {
const apps = { } ;
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
const propsA = ( ( schemaPlan . applications | | { } ) . entite | | { } ) . properties | | { } ;
2026-06-24 20:17:46 -04:00
applications . forEach ( a = > {
if ( ! String ( a . id ) . trim ( ) ) return ;
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
const o = { } ;
Object . keys ( propsA ) . forEach ( cle = > {
const spec = propsA [ cle ] ;
if ( cle == = ' liens ' ) {
/ / Un lien incomplet n ' est pas un lien : il ferait echouer le validateur du
/ / moteur a la sauvegarde suivante , sur une entree que personne n ' a voulue.
const lns = ( a . liens | | [ ] ) . filter ( l = > String ( l . vers | | ' ' ) . trim ( ) & & String ( l . role | | ' ' ) . trim ( ) )
. map ( l = > ( { vers : l . vers , role : l . role } ) ) ;
if ( lns . length ) o . liens = lns ;
return ;
}
if ( spec . type == = ' array ' ) { const l = listeDepuisTexte ( a [ cle ] ) ; if ( l . length ) o [ cle ] = l ; return ; }
if ( spec . type == = ' boolean ' ) { if ( a [ cle ] ) o [ cle ] = true ; return ; }
const v = a [ cle ] ;
if ( v == = undefined | | v == = null | | String ( v ) . trim ( ) == = ' ' ) return ;
o [ cle ] = ( spec . type == = ' integer ' ) ? ( parseInt ( v , 10 ) | | v ) : v ;
} ) ;
/ / ` groupe ` et ` hote ` sont REQUIS : on les ecrit meme vides , pour que le
/ / validateur du moteur refuse clairement plutot que de recevoir un objet muet .
if ( o . groupe == = undefined ) o . groupe = a . groupe | | ' ' ;
if ( o . hote == = undefined ) o . hote = a . hote | | ' ' ;
2026-06-24 20:17:46 -04:00
apps [ a . id ] = o ;
} ) ;
const rep = await fetch ( ' /api/applications ' , {
method : ' POST ' , headers : { ' Content-Type ' : ' application/json ' , ' X-Jeton ' : JETON } , body : JSON . stringify ( { applications : apps } )
} ) ;
const data = await rep . json ( ) ;
if ( ! rep . ok ) { message ( data . erreur | | ' Sauvegarde refusée. ' , ' erreur ' ) ; return ; }
chargerApplications ( data ) ;
dessinerApplications ( ) ;
message ( ' Applications sauvegardées. ' , ' ok ' ) ;
}
function chargerServeurs ( data ) {
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
serveurs = ( data . serveurs | | [ ] ) . map ( s = > ( { . . . s , integrations : s . integrations | | [ ] } ) ) ;
2026-06-24 20:17:46 -04:00
serveursModifie = false ;
}
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
function marquerServeursModifie ( ) { serveursModifie = true ; majIndicateurSauvegarde ( ) ; }
function choisirServeur ( i ) { selServeur = i ; dessiner ( ) ; }
function definirServeur ( champ , v ) {
const s = serveurs [ selServeur ] ; if ( ! s ) return ;
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
poserChemin ( s , champ , v ) ; marquerServeursModifie ( ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
if ( champ == = ' nom ' ) { const t = document . querySelector ( ' #tuile-srv- ' + selServeur + ' .carte-nom ' ) ; if ( t ) { t . textContent = v | | ' (sans nom) ' ; t . className = ' carte-nom ' + ( v ? ' ' : ' sans ' ) ; } }
else if ( champ == = ' fonction ' | | champ == = ' etat ' ) dessiner ( ) ;
2026-06-24 20:17:46 -04:00
}
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
function basculerIntegration ( role , actif ) {
const s = serveurs [ selServeur ] ; if ( ! s ) return ;
const set = new Set ( listeDepuisTexte ( s . integrations ) ) ;
2026-06-30 12:36:51 -04:00
if ( actif ) set . add ( role ) ; else set . delete ( role ) ;
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
s . integrations = Array . from ( set ) ; marquerServeursModifie ( ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
}
function ajouterServeur ( ) {
serveurs . unshift ( { nom : ' ' , fonction : ( Object . keys ( nomenclature . fonctions | | { } ) [ 0 ] | | ' ' ) , etat : ' planifie ' , noeud : ' ' , stockage : ' ' , disque : ' ' , memoire : ' ' , coeurs : ' ' , integrations : ' ' , statut : ' absent ' , vmid : ' ' , adresse_ip : ' ' , vlan : ' ' } ) ;
selServeur = 0 ; marquerServeursModifie ( ) ; dessiner ( ) ;
const c = document . querySelector ( ' #detail .grille input ' ) ; if ( c ) c . focus ( ) ;
}
function retirerServeur ( ) {
const s = serveurs [ selServeur ] ; if ( ! s ) return ;
if ( s . nom & & ! confirm ( ' Retirer le serveur « ' + s . nom + ' » du plan ? ' ) ) return ;
serveurs . splice ( selServeur , 1 ) ;
selServeur = serveurs . length ? Math . min ( selServeur , serveurs . length - 1 ) : null ;
marquerServeursModifie ( ) ; dessiner ( ) ;
}
function ouvrirApp ( id ) { const i = applications . findIndex ( a = > a . id == = id ) ; selApp = i > = 0 ? i : null ; setVue ( ' applications ' ) ; }
function ouvrirBaseServeur ( nom ) { const i = bdServeurs . findIndex ( x = > x . nom == = nom ) ; selBase = i > = 0 ? ' srv: ' + i : null ; setVue ( ' bases ' ) ; }
2026-07-01 17:39:44 -04:00
function pointVie ( nom ) {
const v = sondes [ nom ] ;
return v == = true ? ' <span class= " pt actif " title= " VM joignable (SSH) " ></span> '
: v == = false ? ' <span class= " pt bloq " title= " Injoignable " ></span> ' : ' ' ;
}
2026-10-11 14:43:01 -04:00
/ / L ' ETAT REEL, EN MOTS. Quatre axes ne se fondent jamais : plan, reel, inventaire,
/ / prerequis . Une seule pastille pour les quatre faisait lire « vert » la ou le plan
/ / etait actif et la machine morte .
function reelDe ( nom ) {
const v = sondes [ nom ] ;
if ( v == = true ) return ' <span class= " reel " ><span class= " pt actif " ></span>joignable</span> ' ;
if ( v == = false ) return ' <span class= " reel " style= " color:var(--rouge) " ><span class= " pt bloq " ></span>injoignable</span> ' ;
return ' <span class= " reel " ><span class= " pt ns " ></span>non sondé</span> ' ;
}
function etiquettePlan ( etat ) {
return etat == = ' actif ' ? ' <span class= " etiq e-actif " >actif</span> ' : ' <span class= " etiq e-plan " >planifié</span> ' ;
}
function etiquetteInventaire ( statut ) {
return statut == = ' divergence ' ? ' <span class= " etiq e-div " title= " Le plan diverge de l \' inventaire généré " >divergent</span> '
: statut == = ' absent ' ? ' <span class= " etiq e-plan " title= " Déclaré, pas encore instancié " >non instancié</span> '
: ' <span class= " etiq e-recon " >réconcilié</span> ' ;
}
/ / LE PARC SE LIT PAR CATEGORIE DE LA NOMENCLATURE , pas en mosaique : la zone porte le
/ / VLAN , et c ' est elle qu ' on cherche quand un flux ne passe pas . L ' ordre d ' affichage
/ / est aussi celui de j / k — une seule source pour les deux .
function serveursEnGroupes ( ) {
const groupes = { } ;
serveurs . forEach ( ( s , i ) = > {
if ( ! correspondServeur ( s ) ) return ;
const info = infoFonction ( s . fonction ) ;
const cle = info ? String ( info . categorie ) : ' ~ ' ;
if ( ! groupes [ cle ] ) groupes [ cle ] = { cle , libelle : info ? info . libelle : ' Sans catégorie ' ,
vlan : info ? info . vlan : ' ' , items : [ ] } ;
groupes [ cle ] . items . push ( [ s , i ] ) ;
} ) ;
return Object . values ( groupes )
. sort ( ( a , b ) = > ( a . cle == = ' ~ ' ) - ( b . cle == = ' ~ ' ) | | Number ( a . cle ) - Number ( b . cle ) )
. map ( g = > ( { . . . g , items : g . items . sort ( ( [ a ] , [ b ] ) = > String ( a . nom ) . localeCompare ( String ( b . nom ) ) ) } ) ) ;
}
function rangServeur ( s , i ) {
const h = hotes . find ( x = > x . nom == = s . nom ) ;
const rendus = applications . filter ( a = > a . hote == = s . nom ) . map ( a = > a . id ) . filter ( Boolean ) ;
const plan = etiquettePlan ( s . etat ) + ( h & & hoteBloque ( h ) ? ' <span class= " etiq e-bloq " title= " Un prérequis manque " >bloqué</span> ' : ' ' ) ;
return ` < button type = " button " class = " rang $ { i === selServeur ? ' selectionnee ' : ' ' } " id = " tuile-srv-$ {i} " onclick = " choisirServeur($ {i} ) " >
< span style = " display:flex;flex-direction:column;min-width:0 " > < span class = " carte-nom $ { s.nom ? ' ' : ' sans ' } " > $ { echapper ( s . nom | | ' (sans nom) ' ) } < / span > $ { s . vmid ? ` < span class = " carte-vmid " > #${echapper(s.vmid)}</span>` : ''}</span>
< span class = " ip " > $ { echapper ( s . adresse_ip | | ' — ' ) } < / span >
< span > $ { plan } < / span >
< span > $ { reelDe ( s . nom ) } < / span >
< span > $ { etiquetteInventaire ( s . statut ) } < / span >
< span class = " rendus " title = " $ { echapper(rendus.join( ' , ' ))} " > $ { echapper ( rendus . join ( ' · ' ) | | ' — ' ) } < / span >
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
< / button > ` ;
2026-06-30 12:36:51 -04:00
}
2026-10-11 14:43:01 -04:00
/ / Garde pour compatibilite : d ' autres vues peuvent encore demander une carte.
function carteServeur ( s , i ) { return rangServeur ( s , i ) ; }
/ / LA CONSOLE D ' UN SITE DESSINE SES MACHINES (2026-09-20), ET SA FABRIC (2026-10-11).
console de site : un vide doit avoir la forme de ce qu'il remplace
La console du runner de SITE-Chezlepro ne montrait rien. Elle a douze machines.
inventaire_api sert serveurs en LISTE, inventaire_api_du_site le servait en TABLE. Les deux
sont vides et la page ne les lit pas pareil : (data.serveurs || []).map trouve {} — truthy,
sans .map — et charger() leve. La console d'un site mourait avant sa premiere vue, et le
message accusait une methode manquante plutot qu'une forme qui ment.
Et la vue regardait au mauvais endroit : le registre vide est voulu, mais la page en tirait
le message d'un locataire sans serveurs et les douze machines servies dans hotes n'etaient
dessinees nulle part. Une console de site dessine desormais SES machines, en lecture seule,
avec leur adresse, leur VMID, leurs roles et leur point de vie ; le panneau droit dit ce
qu'elle peut et renvoie aux Assistants.
C'est le defaut du 16 septembre par une autre porte : la premiere fois l'inventaire etait
vide et la page dessinait ce vide comme un plan vide ; cette fois il est plein et c'est la
vue qui regarde ailleurs.
Ce qui a trouve la cause n'est pas une lecture mais le BANC : en donnant pour la premiere
fois a test_rendu_gui la charge d'une console de site, il a leve sur charger(). Sans lui
j'aurais livre une vue juste par dessus un charger() qui leve.
Valide : node --check sur le bloc script, 7 tests de rendu dont deux neufs, make test a 0
echec, runbooks.py verifier a 0 ecart, P81 et P83 vertes. P02 reste en echec pour la raison
anterieure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:36:00 -04:00
/ /
/ / ` inventaire_api_du_site ` sert volontairement ` serveurs : { } ` — « on ne fabrique pas
/ / un faux plan de tenant pour remplir l ' ecran ». C ' est juste : un site declare ses
/ / machines dans SON plan , avec ` ip ` , ` vmid ` et ` noeud ` ECRITS , la ou un locataire les
/ / derive de son seed . Mais la page tirait de ce registre vide le message d ' un
/ / locataire sans serveurs — « + Serveur » , ` make serveurs - bootstrap » — et les douze
/ / machines du site , servies dans ` hotes ` , n ' etaient dessinees nulle part.
/ /
2026-10-11 14:43:01 -04:00
/ / L ' inventaire du site porte aussi ses HYPERVISEURS : ils ne sont pas au plan des
/ / machines , ce sont les noeuds qui les portent . On les montre comme tels , avec ce
/ / qu ' ils portent, plutot que melanges aux machines comme des VM « planifiees ».
console de site : un vide doit avoir la forme de ce qu'il remplace
La console du runner de SITE-Chezlepro ne montrait rien. Elle a douze machines.
inventaire_api sert serveurs en LISTE, inventaire_api_du_site le servait en TABLE. Les deux
sont vides et la page ne les lit pas pareil : (data.serveurs || []).map trouve {} — truthy,
sans .map — et charger() leve. La console d'un site mourait avant sa premiere vue, et le
message accusait une methode manquante plutot qu'une forme qui ment.
Et la vue regardait au mauvais endroit : le registre vide est voulu, mais la page en tirait
le message d'un locataire sans serveurs et les douze machines servies dans hotes n'etaient
dessinees nulle part. Une console de site dessine desormais SES machines, en lecture seule,
avec leur adresse, leur VMID, leurs roles et leur point de vie ; le panneau droit dit ce
qu'elle peut et renvoie aux Assistants.
C'est le defaut du 16 septembre par une autre porte : la premiere fois l'inventaire etait
vide et la page dessinait ce vide comme un plan vide ; cette fois il est plein et c'est la
vue qui regarde ailleurs.
Ce qui a trouve la cause n'est pas une lecture mais le BANC : en donnant pour la premiere
fois a test_rendu_gui la charge d'une console de site, il a leve sur charger(). Sans lui
j'aurais livre une vue juste par dessus un charger() qui leve.
Valide : node --check sur le bloc script, 7 tests de rendu dont deux neufs, make test a 0
echec, runbooks.py verifier a 0 ecart, P81 et P83 vertes. P02 reste en echec pour la raison
anterieure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:36:00 -04:00
function dessinerMachinesDuSite ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
const nom = ( contexte & & contexte . fabric ) | | ' ce site ' ;
2026-10-11 14:43:01 -04:00
const entete = ` < div class = " vue-tete " > < div style = " display:flex;flex-direction:column;gap:2px " >
< h2 class = " vue-titre " > Machines de $ { echapper ( nom ) } < / h2 >
< span class = " vue-sous " > Déclarées dans le plan du site : < code > ip < / code > , < code > vmid < / code > et < code > noeud < / code >
y sont < b > écrits < / b > , pas dérivés d ' un seed. Cette console ne les édite pas ; elle les
< b > matérialise < / b > et leur applique des rôles — vue < b > Assistants < / b > . < / span > < / div > < / div > ` ;
console de site : un vide doit avoir la forme de ce qu'il remplace
La console du runner de SITE-Chezlepro ne montrait rien. Elle a douze machines.
inventaire_api sert serveurs en LISTE, inventaire_api_du_site le servait en TABLE. Les deux
sont vides et la page ne les lit pas pareil : (data.serveurs || []).map trouve {} — truthy,
sans .map — et charger() leve. La console d'un site mourait avant sa premiere vue, et le
message accusait une methode manquante plutot qu'une forme qui ment.
Et la vue regardait au mauvais endroit : le registre vide est voulu, mais la page en tirait
le message d'un locataire sans serveurs et les douze machines servies dans hotes n'etaient
dessinees nulle part. Une console de site dessine desormais SES machines, en lecture seule,
avec leur adresse, leur VMID, leurs roles et leur point de vie ; le panneau droit dit ce
qu'elle peut et renvoie aux Assistants.
C'est le defaut du 16 septembre par une autre porte : la premiere fois l'inventaire etait
vide et la page dessinait ce vide comme un plan vide ; cette fois il est plein et c'est la
vue qui regarde ailleurs.
Ce qui a trouve la cause n'est pas une lecture mais le BANC : en donnant pour la premiere
fois a test_rendu_gui la charge d'une console de site, il a leve sur charger(). Sans lui
j'aurais livre une vue juste par dessus un charger() qui leve.
Valide : node --check sur le bloc script, 7 tests de rendu dont deux neufs, make test a 0
echec, runbooks.py verifier a 0 ecart, P81 et P83 vertes. P02 reste en echec pour la raison
anterieure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:36:00 -04:00
if ( ! hotes . length ) {
cible . innerHTML = entete + ' <div class= " vide " style= " padding:14px " >Cet inventaire dynamique ne rend aucune machine. Ce n \' est pas un plan vide : c \' est une mesure vide, et elle mérite un regard — <span class= " mono " >make site-inventaire</span>.</div> ' ;
return ;
}
2026-10-11 14:43:01 -04:00
const declares = { } ;
serveurs . forEach ( s = > { declares [ s . nom ] = s ; } ) ;
console de site : un vide doit avoir la forme de ce qu'il remplace
La console du runner de SITE-Chezlepro ne montrait rien. Elle a douze machines.
inventaire_api sert serveurs en LISTE, inventaire_api_du_site le servait en TABLE. Les deux
sont vides et la page ne les lit pas pareil : (data.serveurs || []).map trouve {} — truthy,
sans .map — et charger() leve. La console d'un site mourait avant sa premiere vue, et le
message accusait une methode manquante plutot qu'une forme qui ment.
Et la vue regardait au mauvais endroit : le registre vide est voulu, mais la page en tirait
le message d'un locataire sans serveurs et les douze machines servies dans hotes n'etaient
dessinees nulle part. Une console de site dessine desormais SES machines, en lecture seule,
avec leur adresse, leur VMID, leurs roles et leur point de vie ; le panneau droit dit ce
qu'elle peut et renvoie aux Assistants.
C'est le defaut du 16 septembre par une autre porte : la premiere fois l'inventaire etait
vide et la page dessinait ce vide comme un plan vide ; cette fois il est plein et c'est la
vue qui regarde ailleurs.
Ce qui a trouve la cause n'est pas une lecture mais le BANC : en donnant pour la premiere
fois a test_rendu_gui la charge d'une console de site, il a leve sur charger(). Sans lui
j'aurais livre une vue juste par dessus un charger() qui leve.
Valide : node --check sur le bloc script, 7 tests de rendu dont deux neufs, make test a 0
echec, runbooks.py verifier a 0 ecart, P81 et P83 vertes. P02 reste en echec pour la raison
anterieure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:36:00 -04:00
const vus = hotes . map ( ( h , i ) = > [ h , i ] ) . filter ( ( [ h ] ) = >
! filtre | | ( h . nom | | ' ' ) . toLowerCase ( ) . includes ( filtre ) ) ;
2026-10-11 14:43:01 -04:00
const machines = vus . filter ( ( [ h ] ) = > declares [ h . nom ] ) ;
const noeuds = vus . filter ( ( [ h ] ) = > ! declares [ h . nom ] ) ;
const portes = { } ;
serveurs . forEach ( s = > { if ( s . noeud ) ( portes [ s . noeud ] = portes [ s . noeud ] | | [ ] ) . push ( s . nom ) ; } ) ;
const blocNoeuds = noeuds . length ? ` < div class = " section-tete " > Nœuds de la fabric < span class = " compte " > $ { noeuds . length } < / span > < / div >
< div class = " noeuds " > $ { noeuds . map ( ( [ h , i ] ) = > {
const p = portes [ h . nom ] | | [ ] ;
return ` < button type = " button " class = " noeud $ { i === selMachine ? ' selectionnee ' : ' ' } " id = " tuile-mach-$ {i} " onclick = " choisirMachine($ {i} ) " style = " text-align:left;font:inherit;color:inherit;cursor:pointer " >
< span class = " noeud-tete " > < b > $ { echapper ( h . nom ) } < / b > < span class = " vue-sous " style = " font-size:12px " > nœud < / span >
< span class = " grandit " > < / span > $ { reelDe ( h . nom ) } < / span >
< span class = " puces " > $ { p . map ( m = > ` < span class = " puce " > $ { echapper ( m ) } < / span > ` ) . join ( ' ' ) | | ' <span class= " vue-sous " style= " font-size:12px " >aucune machine du site</span> ' } < / span >
< / button > ` ; } ) . join ( ' ' ) } < / div > ` : ' ' ;
const parReseau = { } ;
machines . forEach ( ( [ h , i ] ) = > {
const r = ( declares [ h . nom ] | | { } ) . fonction | | ' sans réseau ' ;
( parReseau [ r ] = parReseau [ r ] | | [ ] ) . push ( [ h , i ] ) ;
} ) ;
const rangs = Object . keys ( parReseau ) . sort ( ) . map ( r = > {
const pref = ( parReseau [ r ] [ 0 ] [ 0 ] . adresse_ip | | ' ' ) . split ( ' . ' ) . slice ( 0 , 3 ) . join ( ' . ' ) ;
return ` < div class = " groupe-tete " > $ { echapper ( r ) } $ { pref ? ` < span class = " mono " > $ { echapper ( pref ) } .0 / 24 < / span > ` : ' ' } < / div > `
+ parReseau [ r ] . sort ( ( [ a ] , [ b ] ) = > a . nom . localeCompare ( b . nom ) ) . map ( ( [ h , i ] ) = > carteMachineDuSite ( h , i ) ) . join ( ' ' ) ;
} ) . join ( ' ' ) ;
const blocMachines = machines . length ? ` < div class = " section-tete " > Machines < span class = " compte " > $ { machines . length } < / span > < / div >
< div class = " tableau " > < div class = " tableau-corps " >
< div class = " rang site tete " > < span > Machine < / span > < span > Adresse < / span > < span > VMID < / span > < span > Nœud < / span > < span > Réel < / span > < span > Rôles < / span > < / div >
$ { rangs } < / div > < / div > ` : ' ' ;
cible . innerHTML = entete + blocNoeuds + blocMachines ;
console de site : un vide doit avoir la forme de ce qu'il remplace
La console du runner de SITE-Chezlepro ne montrait rien. Elle a douze machines.
inventaire_api sert serveurs en LISTE, inventaire_api_du_site le servait en TABLE. Les deux
sont vides et la page ne les lit pas pareil : (data.serveurs || []).map trouve {} — truthy,
sans .map — et charger() leve. La console d'un site mourait avant sa premiere vue, et le
message accusait une methode manquante plutot qu'une forme qui ment.
Et la vue regardait au mauvais endroit : le registre vide est voulu, mais la page en tirait
le message d'un locataire sans serveurs et les douze machines servies dans hotes n'etaient
dessinees nulle part. Une console de site dessine desormais SES machines, en lecture seule,
avec leur adresse, leur VMID, leurs roles et leur point de vie ; le panneau droit dit ce
qu'elle peut et renvoie aux Assistants.
C'est le defaut du 16 septembre par une autre porte : la premiere fois l'inventaire etait
vide et la page dessinait ce vide comme un plan vide ; cette fois il est plein et c'est la
vue qui regarde ailleurs.
Ce qui a trouve la cause n'est pas une lecture mais le BANC : en donnant pour la premiere
fois a test_rendu_gui la charge d'une console de site, il a leve sur charger(). Sans lui
j'aurais livre une vue juste par dessus un charger() qui leve.
Valide : node --check sur le bloc script, 7 tests de rendu dont deux neufs, make test a 0
echec, runbooks.py verifier a 0 ecart, P81 et P83 vertes. P02 reste en echec pour la raison
anterieure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:36:00 -04:00
}
/ / LE MEME GABARIT QUE LES SERVEURS , et pas un second qui lui ressemblerait : deux
2026-10-11 14:43:01 -04:00
/ / rangs qui se ressemblent finissent par ne plus se ressembler .
console de site : un vide doit avoir la forme de ce qu'il remplace
La console du runner de SITE-Chezlepro ne montrait rien. Elle a douze machines.
inventaire_api sert serveurs en LISTE, inventaire_api_du_site le servait en TABLE. Les deux
sont vides et la page ne les lit pas pareil : (data.serveurs || []).map trouve {} — truthy,
sans .map — et charger() leve. La console d'un site mourait avant sa premiere vue, et le
message accusait une methode manquante plutot qu'une forme qui ment.
Et la vue regardait au mauvais endroit : le registre vide est voulu, mais la page en tirait
le message d'un locataire sans serveurs et les douze machines servies dans hotes n'etaient
dessinees nulle part. Une console de site dessine desormais SES machines, en lecture seule,
avec leur adresse, leur VMID, leurs roles et leur point de vie ; le panneau droit dit ce
qu'elle peut et renvoie aux Assistants.
C'est le defaut du 16 septembre par une autre porte : la premiere fois l'inventaire etait
vide et la page dessinait ce vide comme un plan vide ; cette fois il est plein et c'est la
vue qui regarde ailleurs.
Ce qui a trouve la cause n'est pas une lecture mais le BANC : en donnant pour la premiere
fois a test_rendu_gui la charge d'une console de site, il a leve sur charger(). Sans lui
j'aurais livre une vue juste par dessus un charger() qui leve.
Valide : node --check sur le bloc script, 7 tests de rendu dont deux neufs, make test a 0
echec, runbooks.py verifier a 0 ecart, P81 et P83 vertes. P02 reste en echec pour la raison
anterieure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:36:00 -04:00
function carteMachineDuSite ( h , i ) {
2026-10-11 14:43:01 -04:00
const s = serveurs . find ( x = > x . nom == = h . nom ) | | { } ;
const roles = ( h . groupes | | [ ] ) . filter ( g = > g . startsWith ( ' serveur_ ' ) & & g != = ' serveur_debian ' & & g != = ' serveur_durci ' )
. map ( g = > g . replace ( / ^ serveur_ / , ' ' ) ) ;
return ` < button type = " button " class = " rang site $ { i === selMachine ? ' selectionnee ' : ' ' } " id = " tuile-mach-$ {i} " onclick = " choisirMachine($ {i} ) " >
< span class = " carte-nom " > $ { echapper ( h . nom ) } < / span >
< span class = " ip " > $ { echapper ( h . adresse_ip | | ' — ' ) } < / span >
< span class = " ip " > $ { echapper ( h . vmid | | s . vmid | | ' — ' ) } < / span >
< span > $ { echapper ( s . noeud | | h . noeud | | ' — ' ) } < / span >
< span > $ { reelDe ( h . nom ) } < / span >
< span class = " rendus " title = " $ { echapper((h.groupes || []).join( ' , ' ))} " > $ { echapper ( roles . join ( ' · ' ) | | ' — ' ) } < / span >
console de site : un vide doit avoir la forme de ce qu'il remplace
La console du runner de SITE-Chezlepro ne montrait rien. Elle a douze machines.
inventaire_api sert serveurs en LISTE, inventaire_api_du_site le servait en TABLE. Les deux
sont vides et la page ne les lit pas pareil : (data.serveurs || []).map trouve {} — truthy,
sans .map — et charger() leve. La console d'un site mourait avant sa premiere vue, et le
message accusait une methode manquante plutot qu'une forme qui ment.
Et la vue regardait au mauvais endroit : le registre vide est voulu, mais la page en tirait
le message d'un locataire sans serveurs et les douze machines servies dans hotes n'etaient
dessinees nulle part. Une console de site dessine desormais SES machines, en lecture seule,
avec leur adresse, leur VMID, leurs roles et leur point de vie ; le panneau droit dit ce
qu'elle peut et renvoie aux Assistants.
C'est le defaut du 16 septembre par une autre porte : la premiere fois l'inventaire etait
vide et la page dessinait ce vide comme un plan vide ; cette fois il est plein et c'est la
vue qui regarde ailleurs.
Ce qui a trouve la cause n'est pas une lecture mais le BANC : en donnant pour la premiere
fois a test_rendu_gui la charge d'une console de site, il a leve sur charger(). Sans lui
j'aurais livre une vue juste par dessus un charger() qui leve.
Valide : node --check sur le bloc script, 7 tests de rendu dont deux neufs, make test a 0
echec, runbooks.py verifier a 0 ecart, P81 et P83 vertes. P02 reste en echec pour la raison
anterieure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:36:00 -04:00
< / button > ` ;
}
function choisirMachine ( i ) { selMachine = i ; dessinerMachinesDuSite ( ) ; dessinerDetail ( ) ; }
2026-10-11 14:43:01 -04:00
function ouvrirAssistant ( id ) { rbSel = id ; setVue ( ' assistants ' ) ; }
console de site : un vide doit avoir la forme de ce qu'il remplace
La console du runner de SITE-Chezlepro ne montrait rien. Elle a douze machines.
inventaire_api sert serveurs en LISTE, inventaire_api_du_site le servait en TABLE. Les deux
sont vides et la page ne les lit pas pareil : (data.serveurs || []).map trouve {} — truthy,
sans .map — et charger() leve. La console d'un site mourait avant sa premiere vue, et le
message accusait une methode manquante plutot qu'une forme qui ment.
Et la vue regardait au mauvais endroit : le registre vide est voulu, mais la page en tirait
le message d'un locataire sans serveurs et les douze machines servies dans hotes n'etaient
dessinees nulle part. Une console de site dessine desormais SES machines, en lecture seule,
avec leur adresse, leur VMID, leurs roles et leur point de vie ; le panneau droit dit ce
qu'elle peut et renvoie aux Assistants.
C'est le defaut du 16 septembre par une autre porte : la premiere fois l'inventaire etait
vide et la page dessinait ce vide comme un plan vide ; cette fois il est plein et c'est la
vue qui regarde ailleurs.
Ce qui a trouve la cause n'est pas une lecture mais le BANC : en donnant pour la premiere
fois a test_rendu_gui la charge d'une console de site, il a leve sur charger(). Sans lui
j'aurais livre une vue juste par dessus un charger() qui leve.
Valide : node --check sur le bloc script, 7 tests de rendu dont deux neufs, make test a 0
echec, runbooks.py verifier a 0 ecart, P81 et P83 vertes. P02 reste en echec pour la raison
anterieure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:36:00 -04:00
function inspecteurMachineDuSite ( ) {
const cible = document . getElementById ( ' detail ' ) ;
const h = hotes [ selMachine ] ;
if ( ! h ) {
2026-10-11 14:43:01 -04:00
cible . innerHTML = ` < div class = " detail " > < div class = " section-fiche " > < div class = " section-tete " > Console du SITE < / div >
< div class = " hint-dep " > Choisis une machine ou un nœud à gauche . Cette console < b > matérialise < / b > le
console de site : un vide doit avoir la forme de ce qu'il remplace
La console du runner de SITE-Chezlepro ne montrait rien. Elle a douze machines.
inventaire_api sert serveurs en LISTE, inventaire_api_du_site le servait en TABLE. Les deux
sont vides et la page ne les lit pas pareil : (data.serveurs || []).map trouve {} — truthy,
sans .map — et charger() leve. La console d'un site mourait avant sa premiere vue, et le
message accusait une methode manquante plutot qu'une forme qui ment.
Et la vue regardait au mauvais endroit : le registre vide est voulu, mais la page en tirait
le message d'un locataire sans serveurs et les douze machines servies dans hotes n'etaient
dessinees nulle part. Une console de site dessine desormais SES machines, en lecture seule,
avec leur adresse, leur VMID, leurs roles et leur point de vie ; le panneau droit dit ce
qu'elle peut et renvoie aux Assistants.
C'est le defaut du 16 septembre par une autre porte : la premiere fois l'inventaire etait
vide et la page dessinait ce vide comme un plan vide ; cette fois il est plein et c'est la
vue qui regarde ailleurs.
Ce qui a trouve la cause n'est pas une lecture mais le BANC : en donnant pour la premiere
fois a test_rendu_gui la charge d'une console de site, il a leve sur charger(). Sans lui
j'aurais livre une vue juste par dessus un charger() qui leve.
Valide : node --check sur le bloc script, 7 tests de rendu dont deux neufs, make test a 0
echec, runbooks.py verifier a 0 ecart, P81 et P83 vertes. P02 reste en echec pour la raison
anterieure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:36:00 -04:00
terrain — créer et détruire des VM , poser le réseau et la frontière — et n ' entre
chez aucun locataire : elle n ' a la voûte d ' aucun d ' eux.</div>
2026-10-11 14:43:01 -04:00
< div class = " hint-dep " style = " margin:10px 0 12px " > Les séquences qui lui reviennent sont
dans la vue < b > Assistants < / b > . < / div > < / div > < / div > ` ;
console de site : un vide doit avoir la forme de ce qu'il remplace
La console du runner de SITE-Chezlepro ne montrait rien. Elle a douze machines.
inventaire_api sert serveurs en LISTE, inventaire_api_du_site le servait en TABLE. Les deux
sont vides et la page ne les lit pas pareil : (data.serveurs || []).map trouve {} — truthy,
sans .map — et charger() leve. La console d'un site mourait avant sa premiere vue, et le
message accusait une methode manquante plutot qu'une forme qui ment.
Et la vue regardait au mauvais endroit : le registre vide est voulu, mais la page en tirait
le message d'un locataire sans serveurs et les douze machines servies dans hotes n'etaient
dessinees nulle part. Une console de site dessine desormais SES machines, en lecture seule,
avec leur adresse, leur VMID, leurs roles et leur point de vie ; le panneau droit dit ce
qu'elle peut et renvoie aux Assistants.
C'est le defaut du 16 septembre par une autre porte : la premiere fois l'inventaire etait
vide et la page dessinait ce vide comme un plan vide ; cette fois il est plein et c'est la
vue qui regarde ailleurs.
Ce qui a trouve la cause n'est pas une lecture mais le BANC : en donnant pour la premiere
fois a test_rendu_gui la charge d'une console de site, il a leve sur charger(). Sans lui
j'aurais livre une vue juste par dessus un charger() qui leve.
Valide : node --check sur le bloc script, 7 tests de rendu dont deux neufs, make test a 0
echec, runbooks.py verifier a 0 ecart, P81 et P83 vertes. P02 reste en echec pour la raison
anterieure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:36:00 -04:00
return ;
}
2026-10-11 14:43:01 -04:00
const s = serveurs . find ( x = > x . nom == = h . nom ) ;
const tuileEcrite = ( v , l ) = > tuile ( v , l ) ;
const socle = [ ' serveur_debian ' , ' serveur_durci ' ] ;
const roles = ( h . groupes | | [ ] ) . filter ( g = > ! socle . includes ( g ) & & ! g . startsWith ( ' client_ ' ) ) ;
const clients = ( h . groupes | | [ ] ) . filter ( g = > g . startsWith ( ' client_ ' ) ) ;
const presents = ( h . groupes | | [ ] ) . filter ( g = > socle . includes ( g ) ) ;
cible . innerHTML = ` < div class = " detail " >
< div class = " detail-tete " > < div class = " detail-nom " > < span class = " detail-nom-txt " > $ { echapper ( h . nom ) } < / span >
$ { s ? ` < span class = " zone-hint " > réseau $ { echapper ( s . fonction | | ' — ' ) } < / span > ` : ' <span class= " zone-hint " >nœud de la fabric</span> ' } < / div >
< span class = " grandit " > < / span > $ { s ? etiquettePlan ( h . etat ) : ' ' } < / div >
< div class = " stats " > $ { tuileEcrite ( h . adresse_ip , ' adresse · écrite ' ) } $ { s ? tuileEcrite ( h . vmid | | s . vmid , ' VMID · écrit ' ) + tuileEcrite ( s . noeud | | h . noeud , ' nœud · écrit ' ) : ' ' } < / div >
< div class = " dsn " style = " padding:0 16px 4px " > < b > Réel < / b > : $ { reelDe ( h . nom ) } < / div >
< div class = " section-fiche " > < div class = " section-tete " > Rôles < / div >
< div class = " puces " > $ { roles . map ( g = > ` < span class = " puce " > $ { echapper ( g ) } < / span > ` ) . join ( ' ' ) }
$ { presents . map ( g = > ` < span class = " puce socle " title = " Le socle, posé sur toute machine du site " > $ { echapper ( g ) } < / span > ` ) . join ( ' ' ) }
$ { roles . length | | presents . length ? ' ' : ' <span class= " aide-ro " >aucun</span> ' } < / div >
$ { clients . length ? ` < div class = " hint-dep " style = " margin-top:8px " > Intégrations : $ { clients . map ( g = > echapper ( g . replace ( / ^ client_ / , ' ' ) ) ) . join ( ' · ' ) } < / div > ` : ' ' } < / div >
< div class = " gestes " >
< div class = " gestes-tete " > < span class = " section-tete " style = " margin:0 " > Gestes sur cette machine < / span > < / div >
< div class = " geste " > < div class = " quoi " > < b > Appliquer un rôle < / b > < span > Simulation d ' abord, toujours, puis l ' application . < / span > < / div >
< button type = " button " onclick = " ouvrirAssistant( ' site-tenir ' ) " > Ouvrir l ' assistant</button></div>
< div class = " gestes-note " > « Tenir un site en état » : < code > make site - appliquer GROUPE = … < / code > , < code > site - decrire < / code > .
Le plan de ce site vit dans son dépôt ; cette console ne l ' édite pas.</div>
< / div >
< / div > ` ;
console de site : un vide doit avoir la forme de ce qu'il remplace
La console du runner de SITE-Chezlepro ne montrait rien. Elle a douze machines.
inventaire_api sert serveurs en LISTE, inventaire_api_du_site le servait en TABLE. Les deux
sont vides et la page ne les lit pas pareil : (data.serveurs || []).map trouve {} — truthy,
sans .map — et charger() leve. La console d'un site mourait avant sa premiere vue, et le
message accusait une methode manquante plutot qu'une forme qui ment.
Et la vue regardait au mauvais endroit : le registre vide est voulu, mais la page en tirait
le message d'un locataire sans serveurs et les douze machines servies dans hotes n'etaient
dessinees nulle part. Une console de site dessine desormais SES machines, en lecture seule,
avec leur adresse, leur VMID, leurs roles et leur point de vie ; le panneau droit dit ce
qu'elle peut et renvoie aux Assistants.
C'est le defaut du 16 septembre par une autre porte : la premiere fois l'inventaire etait
vide et la page dessinait ce vide comme un plan vide ; cette fois il est plein et c'est la
vue qui regarde ailleurs.
Ce qui a trouve la cause n'est pas une lecture mais le BANC : en donnant pour la premiere
fois a test_rendu_gui la charge d'une console de site, il a leve sur charger(). Sans lui
j'aurais livre une vue juste par dessus un charger() qui leve.
Valide : node --check sur le bloc script, 7 tests de rendu dont deux neufs, make test a 0
echec, runbooks.py verifier a 0 ecart, P81 et P83 vertes. P02 reste en echec pour la raison
anterieure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:36:00 -04:00
}
2026-10-11 17:31:13 -04:00
/ / LES DEUX BOUTS DU CONTRAT ( lot 2 , 2026 - 10 - 11 ) .
/ /
/ / Entre un site et un locataire passent deux fichiers : la face reseau , que le
/ / locataire publie , et la fiche , que le site depose . Chaque console montre son bout ,
/ / avec le commit — c ' est le commit que l ' autre lit , pas le disque . Rien n ' est sonde :
/ / ce que ces fichiers ne disent pas , la page le dit absent au lieu de le deviner .
let lienCharge = null ; / / / api / lien - site
let locatairesCharge = null ; / / / api / locataires - heberges
let selLocataire = 0 ;
const aCote = ( c ) = > ! ! ( contexte & & contexte . contrat & & contexte . contrat [ c ] ) ;
function quand ( iso ) {
if ( ! iso ) return ' ' ;
const d = new Date ( iso ) ;
if ( isNaN ( d ) ) return iso ;
return d . toLocaleString ( ' fr-CA ' , { day : ' numeric ' , month : ' short ' , hour : ' 2-digit ' , minute : ' 2-digit ' } ) ;
}
function ligneTrace ( t ) {
if ( ! t ) return ' <span class= " trace " >aucune trace</span> ' ;
if ( ! t . commit ) return ` < span class = " trace " > jamais commité $ { t . erreur ? ' — ' + echapper ( t . erreur ) : ' ' } < / span > ` ;
return ` < span class = " trace " > commit < span class = " mono " > $ { echapper ( t . commit ) } < / span > · $ { echapper ( quand ( t . date ) ) } < / span > `
+ ( t . modifie ? ' <span class= " etiq e-plan " title= " Le fichier sur disque diffère du dernier commit : l \' autre bout lit le commit. " >modifié, pas encore commité</span> ' : ' ' ) ;
}
function lignesFlux ( liste , vide ) {
if ( ! liste | | ! liste . length ) return ` < div class = " note-contrat " > $ { echapper ( vide ) } < / div > ` ;
return ' <div class= " lignes " > ' + liste . map ( f = > ` < div class = " ligne " >
< span > $ { echapper ( f . hote ) } < / span > < span class = " mono " > $ { echapper ( f . proto ) } $ { echapper ( f . port ) } < / span >
< span class = " mono " title = " Le rôle qui déclare ce flux " > $ { echapper ( f . role ) } < / span > < / div > ` ) . join ( ' ' ) + ' </div> ' ;
}
async function lireContrat ( route ) {
const r = await fetch ( route , { headers : { ' X-Jeton ' : JETON } } ) ;
const d = await r . json ( ) ;
if ( ! r . ok ) throw new Error ( d . erreur | | ( ' HTTP ' + r . status ) ) ;
return d ;
}
/ / CE QUE LA FICHE OFFRE , EN MOTS . Une clef inconnue reste affichee telle quelle :
/ / la cacher parce qu ' on ne sait pas la nommer retirerait une information.
const SERVICES_DE_LA_FICHE = [
[ ' dns_amorcage ' , ' Résolution de noms ' , ' au premier démarrage, avant mon propre résolveur ' ] ,
[ ' artefacts_amorcage ' , ' Cache d \' artefacts ' , ' paquets apt ' ] ,
[ ' setops_depot_binaires ' , ' Dépôt des roues et collections ' , ' ce que le runner installe sans Internet ' ] ,
[ ' serveur_ops_forge_amont ' , ' Forge du génome ' , ' le moteur que mon runner suit ' ] ,
2026-10-11 20:33:38 -04:00
[ ' serveur_ops_forge_cle_hote ' , ' Clé d \' hôte SSH de la forge ' , ' épinglée par mon runner pour lire mon dépôt privé ' ] ,
2026-10-11 17:31:13 -04:00
[ ' client_backup_cible ' , ' Dépôt de sauvegarde ' , ' reçoit mon chiffré ; la vérification reste chez moi ' ] ,
[ ' dns_public_site ' , ' DNS public secondaire ' , ' ' ] ,
[ ' dns_public_nom ' , ' Nom du DNS public ' , ' ' ] ,
[ ' ip_publique_site ' , ' Adresse publique du site ' , ' ' ] ,
[ ' passerelle_sortie ' , ' Sortie vers Internet ' , ' seulement ce que mes rôles déclarent ' ] ,
[ ' nftables_admin_ssh ' , ' Réseaux d \' administration ' , ' ceux qui entrent en SSH chez moi ' ] ,
] ;
const IDENTITE_DE_LA_FICHE = [ ' domaine_site ' , ' index_site ' ] ;
async function dessinerLienSite ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
/ / Lu a l ' ouverture de la vue (`setVue` vide le cache), pas a chaque redessin.
if ( ! lienCharge ) {
cible . innerHTML = ' <div class= " vide " style= " padding:14px " >Lecture de la face réseau et de la fiche du site…</div> ' ;
try { lienCharge = await lireContrat ( ' /api/lien-site ' ) ; }
catch ( e ) { cible . innerHTML = ' <div class= " vide " style= " padding:14px " > ' + echapper ( e . message ) + ' </div> ' ; return ; }
}
const L = lienCharge , F = L . face , R = F . resume , Fi = L . fiche ;
const site = L . hebergeur | | Fi . site | | ' mon site ' ;
const entete = ` < div class = " vue-tete " > < div style = " display:flex;flex-direction:column;gap:2px " >
< h2 class = " vue-titre " > Lien avec le site < / h2 >
< span class = " vue-sous " > Ce que je publie à $ { echapper ( site ) } , et ce qu ' il m ' a déposé . Deux fichiers passent entre nous , rien d ' autre.</span></div></div>`;
/ / MA FACE RESEAU
let etatFace ;
if ( ! F . publiee ) etatFace = ' <span class= " etiq e-bloq " >jamais publiée</span> ' ;
else if ( ( F . ecarts | | [ ] ) . length ) etatFace = ' <span class= " etiq e-div " >à republier</span> ' ;
else etatFace = ' <span class= " etiq e-actif " >à jour avec mon plan</span> ' ;
const face = ! F . publiee ? ` < div class = " corps " > < div class = " note-contrat " > Le site ne peut rien créer ni ouvrir pour moi sans elle .
< span class = " mono " > make face - reseau - publier < / span > , puis commiter < span class = " mono " > face - reseau . yml < / span > . < / div > < / div > ` : `
< div class = " corps " >
< div > $ { ligneTrace ( F . trace ) } < / div >
$ { ( F . ecarts | | [ ] ) . length ? ` < ul class = " ecarts " > $ { F . ecarts . map ( e = > ` < li > $ { echapper ( e ) } < / li > ` ) . join ( ' ' ) } < / ul >
< div > < button type = " button " class = " fantome " onclick = " ouvrirAssistant( ' locataire-materialiser ' ) " > Assistant : republier la face < / button > < / div > ` : ' ' }
< div class = " chiffres " >
< div class = " chiffre " > < span class = " v " > $ { R . machines } < / span > < span class = " l " > machines $ { R . planifiees . length ? ` , dont $ { R . planifiees . length } pas encore actives ` : ' ' } < / span > < / div >
< div class = " chiffre " > < span class = " v " > $ { R . zones } < / span > < span class = " l " > zones $ { R . vlans . length ? ` · VLAN $ { R . vlans [ 0 ] } – $ { R . vlans [ 1 ] } ` : ' ' } < / span > < / div >
< div class = " chiffre " > < span class = " v " > $ { R . expositions . length } < / span > < span class = " l " > entrées depuis Internet < / span > < / div >
< div class = " chiffre " > < span class = " v " > $ { R . sorties . length } < / span > < span class = " l " > sorties vers Internet < / span > < / div >
< / div >
$ { R . planifiees . length ? ` < div class = " note-contrat " > Pas encore actives : < span class = " mono " > $ { echapper ( R . planifiees . join ( ' · ' ) ) } < / span > . Le site ne les crée que sur demande . < / div > ` : ' ' }
< p class = " sous-tete " > Ce que j ' expose à Internet</p>
$ { lignesFlux ( R . expositions , ' Aucune entrée depuis Internet. ' ) }
< p class = " sous-tete " > Ce que je demande de joindre sur Internet < / p >
$ { lignesFlux ( R . sorties , ' Aucune sortie vers Internet : tout passe par les services du site. ' ) }
$ { R . dns_public . length ? ` < div class = " note-contrat " > Zones publiques que le DNS du site sert pour moi : < span class = " mono " > $ { echapper ( R . dns_public . join ( ' , ' ) ) } < / span > . < / div > ` : ' ' }
< div style = " display:flex;align-items:center;gap:8px;flex-wrap:wrap;border-top:1px solid var(--ligne);padding-top:10px " >
< span class = " etiq e-recon " > appliquée par le site : non rapporté < / span >
< span class = " note-contrat " > Le site ne rapporte pas encore quand il pose cette face à sa frontière . Pour le savoir aujourd ' hui, lui demander son <span class= " mono " >make frontiere-plan</span>.</span>
< / div >
< / div > ` ;
/ / LA FICHE DU SITE
let etatFiche ;
if ( ! Fi . presente ) etatFiche = ' <span class= " etiq e-bloq " >absente</span> ' ;
else if ( ! Fi . jugeable ) etatFiche = ' <span class= " etiq e-recon " title= " La carte du site n \' est pas montée sur cette machine : seul le site, ou le poste du mainteneur, peut dire si elle est à jour. " >lue, non jugeable d \' ici</span> ' ;
else if ( ( Fi . ecarts | | [ ] ) . length ) etatFiche = ' <span class= " etiq e-div " >périmée</span> ' ;
else etatFiche = ' <span class= " etiq e-actif " >conforme à la carte du site</span> ' ;
const off = Fi . offre | | { } , gen = Fi . generation | | { } ;
const connus = new Set ( [ . . . SERVICES_DE_LA_FICHE . map ( x = > x [ 0 ] ) , . . . IDENTITE_DE_LA_FICHE ] ) ;
const valeur = ( v ) = > Array . isArray ( v ) ? v . join ( ' , ' ) : String ( v ) ;
const services = SERVICES_DE_LA_FICHE . filter ( ( [ k ] ) = > off [ k ] != null & & off [ k ] != = ' ' )
. map ( ( [ k , nom , usage ] ) = > ` < div class = " ligne " > < span title = " $ { echapper(usage)} " > $ { echapper ( nom ) } < / span >
< span class = " mono " style = " grid-column:span 2 " title = " $ { echapper(k)} " > $ { echapper ( valeur ( off [ k ] ) ) } < / span > < / div > ` )
. concat ( Object . keys ( off ) . filter ( k = > ! connus . has ( k ) ) . map ( k = > ` < div class = " ligne " > < span class = " mono " > $ { echapper ( k ) } < / span >
< span class = " mono " style = " grid-column:span 2 " > $ { echapper ( valeur ( off [ k ] ) ) } < / span > < / div > ` ) )
. concat ( ( gen . serveurs_temps | | [ ] ) . length ? [ ` < div class = " ligne " > < span > Heure < / span > < span class = " mono " style = " grid-column:span 2 " > $ { echapper ( gen . serveurs_temps . join ( ' , ' ) ) } < / span > < / div > ` ] : [ ] )
. concat ( ( gen . resolveurs_site | | [ ] ) . length ? [ ` < div class = " ligne " > < span > Résolveurs du site < / span > < span class = " mono " style = " grid-column:span 2 " > $ { echapper ( gen . resolveurs_site . join ( ' , ' ) ) } < / span > < / div > ` ] : [ ] ) ;
const fiche = ! Fi . presente ? ` < div class = " corps " > < div class = " note-contrat " > Mon inventaire se génère d ' après elle.
Le site la dépose ( < span class = " mono " > make fiches - site - deposer < / span > ) ; je la commite chez moi . < / div > < / div > ` : `
< div class = " corps " >
< div > $ { ligneTrace ( Fi . trace ) } < / div >
$ { ( Fi . ecarts | | [ ] ) . length ? ` < ul class = " ecarts " > $ { Fi . ecarts . map ( e = > ` < li > $ { echapper ( e ) } < / li > ` ) . join ( ' ' ) } < / ul > ` : ' ' }
< div class = " chiffres " >
< div class = " chiffre " > < span class = " v mono " > $ { echapper ( String ( ( Fi . attribue | | { } ) . index ? ? ' — ' ) ) } < / span > < span class = " l " > mon index , attribué < / span > < / div >
< div class = " chiffre " > < span class = " v mono " style = " font-size:14px " > $ { echapper ( ( Fi . attribue | | { } ) . ip_publique | | ' — ' ) } < / span > < span class = " l " > mon adresse publique < / span > < / div >
< div class = " chiffre " > < span class = " v mono " style = " font-size:14px " > $ { echapper ( gen . routage_tenants | | ' — ' ) } < / span > < span class = " l " > routage de mes zones < / span > < / div >
< / div >
< p class = " sous-tete " > Ce que le site m ' offre</p>
< div class = " lignes " > $ { services . join ( ' ' ) | | ' <div class= " ligne " >Aucun service déclaré.</div> ' } < / div >
< div class = " note-contrat " > $ { echapper ( Fi . site | | site ) } $ { off . index_site != null ? ' · index ' + echapper ( String ( off . index_site ) ) : ' ' } $ { off . domaine_site ? ' · <span class= " mono " > ' + echapper ( off . domaine_site ) + ' </span> ' : ' ' } $ { Fi . racine ? ` · racine de confiance < span class = " mono " > $ { echapper ( Fi . racine ) } … < / span > ` : ' ' } .
Ce que la fiche offre est déclaré , pas mesuré : c ' est la supervision qui dit si chaque service répond.</div>
< / div > ` ;
2026-10-11 22:47:06 -04:00
/ / DEMANDER AU SITE ( lot 3 b ) : un ticket dans mon depot prive , sur la forge du site , ecrit
/ / avec mon compte . Pre - rempli ici ; c ' est la forge qui l ' enregistre et qui le trace .
const D = L . demandes | | { } ;
const lienNouvelle = ( titre , corps ) = > D . nouvelle
? ` $ { D . nouvelle } ? title = $ { encodeURIComponent ( titre ) } & body = $ { encodeURIComponent ( corps ) } ` : ' ' ;
const commit = ( F . trace & & F . trace . commit ) | | ' ? ' ;
const pied = ` \n \nFace réseau publiée : commit $ { commit } . \nDemande ouverte depuis la console de $ { L . locataire } . ` ;
const demandesVm = ( R & & R . planifiees | | [ ] ) . map ( h = > {
const ip = ( ( R . adresses_planifiees | | { } ) [ h ] ) | | ' ? ' ;
const corps = ` Le locataire $ { L . locataire } demande au site de créer la machine \` $ { h } \` ( $ { ip } ) . `
+ ` \n \nElle est au plan , l ' inventaire est appliqué et la face republiée.`
+ ` \nGeste du site : \` make creer - vm TENANT = $ { L . locataire } HOTE = $ { h } \` ` + pied ;
return ` < div class = " ligne deux " > < span > Créer < span class = " mono " > $ { echapper ( h ) } < / span > < span class = " mono " > $ { echapper ( ip ) } < / span > < / span >
< span style = " text-align:right " > < a class = " btn-lien " href = " $ { echapper(lienNouvelle( ' Créer la machine ' + h, corps))} " target = " _blank " rel = " noopener " > Demander … < / a > < / span > < / div > ` ;
} ) . join ( ' ' ) ;
const demander = ! D . nouvelle ? ' ' : `
< section class = " carte-contrat " > < div class = " tete " > < h3 > Ce que je demande au site < / h3 > < span class = " fichier " > tickets · $ { echapper ( D . depot ) } < / span > < span class = " grandit " > < / span >
< a class = " btn-lien " href = " $ { echapper(D.liste)} " target = " _blank " rel = " noopener " > Mes demandes < / a > < / div >
< div class = " corps " >
$ { demandesVm ? ` < p class = " sous-tete " > Machines prévues , à faire créer < / p > < div class = " lignes " > $ { demandesVm } < / div > ` : ' ' }
< div > < a class = " btn-lien " href = " $ { echapper(lienNouvelle( ' ' , ' Le locataire ' + L.locataire + ' demande au site : \n \n ' + pied))} " target = " _blank " rel = " noopener " > Nouvelle demande au site … < / a > < / div >
< div class = " note-contrat " > La demande s ' écrit sur la forge du site, avec ton compte, dans le dépôt privé de ce locataire : seuls lui et le site la lisent. Cette console ne lit pas encore les réponses ; « Mes demandes » les montre.</div>
< / div > < / section > ` ;
2026-10-11 17:31:13 -04:00
cible . innerHTML = entete + ` < div class = " contrat " >
< section class = " carte-contrat " > < div class = " tete " > < h3 > Ma face réseau < / h3 > < span class = " fichier " > face - reseau . yml < / span > < span class = " grandit " > < / span > $ { etatFace } < / div > $ { face } < / section >
< section class = " carte-contrat " > < div class = " tete " > < h3 > Ce que le site m ' a déposé</h3><span class= " fichier " >fiche-site.yml</span><span class= " grandit " ></span>$ {etatFiche} </div>$ {fiche} </section>
2026-10-11 22:47:06 -04:00
$ { demander }
2026-10-11 17:31:13 -04:00
< / div > ` ;
dessinerDetail ( ) ;
}
function inspecteurLien ( ) {
const site = ( lienCharge & & ( lienCharge . hebergeur | | lienCharge . fiche . site ) ) | | ' le site ' ;
document . getElementById ( ' detail ' ) . innerHTML = ` < div class = " detail " > < div class = " section-fiche " >
< div class = " section-tete " > Qui fait quoi < / div >
< div class = " qui-fait " >
< div class = " qui " > < b > Le site < / b > < span > $ { echapper ( site ) } matérialise : les VM , le réseau , le pare - feu Proxmox , la frontière , l ' adresse publique. Il n ' a pas ma voûte : il n ' entre pas dans mes services.</span></div>
< div class = " qui " > < b > Moi < / b > < span > Je configure : le plan , les rôles , les services , les comptes . Je n ' ai pas la carte de la fabric : je la demande, je ne la pose pas.</span></div>
< / div >
< div class = " hint-dep " style = " margin-top:12px " > Entre le site et moi passent la face réseau , commitée dans mon dépôt , et la fiche qu ' il y dépose. Le site ne lit rien d ' autre de mon côté . < / div >
< div class = " hint-dep " style = " margin-top:10px " > Une machine à créer , une ouverture à poser : je republie ma face , puis je le demande au site par sa forge . < / div >
< / div > < / div > ` ;
}
async function dessinerLocataires ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
if ( ! locatairesCharge ) {
cible . innerHTML = ' <div class= " vide " style= " padding:14px " >Lecture des faces publiées…</div> ' ;
try { locatairesCharge = await lireContrat ( ' /api/locataires-heberges ' ) ; }
catch ( e ) { cible . innerHTML = ' <div class= " vide " style= " padding:14px " > ' + echapper ( e . message ) + ' </div> ' ; return ; }
}
const D = locatairesCharge , liste = D . locataires | | [ ] ;
if ( selLocataire > = liste . length ) selLocataire = 0 ;
dessinerNavigation ( ) ;
const entete = ` < div class = " vue-tete " > < div style = " display:flex;flex-direction:column;gap:2px " >
< h2 class = " vue-titre " > Locataires hébergés < / h2 >
< span class = " vue-sous " > Ce que chacun m ' a publié, et la fiche que je lui ai déposée. Le miroir de leur « Lien avec le site ».</span></div>
< span class = " grandit " > < / span >
< button type = " button " class = " fantome " onclick = " ouvrirAssistant( ' site-tenir ' ) " > Assistant : tenir un site en état < / button > < / div > ` ;
const rang = ( l , i ) = > {
const R = l . face . resume ;
const rel = l . relation == = ' actif ' ? ' <span class= " etiq e-actif " title= " Sa face me nomme comme hébergeur. " >hébergeur actif</span> '
: l . relation == = ' reprise ' ? ` < span class = " etiq e-recon " title = " Sa face nomme $ { echapper((R && R.hebergeur) || ' ' )} ; je le déclare comme site de reprise. " > reprise < / span > `
: ' <span class= " etiq e-bloq " >face absente</span> ' ;
const idx = ( R & & R . index != = l . index_attribue )
? ` < span class = " etiq e-div " title = " Sa face publie l ' index $ { echapper(String(R.index))} " > $ { echapper ( String ( l . index_attribue ) ) } ≠ $ { echapper ( String ( R . index ) ) } < / span > `
: ` < span > $ { echapper ( String ( l . index_attribue ? ? ' — ' ) ) } < / span > $ { R & & R . vlans . length ? ` < span class = " bas " > VLAN $ { R . vlans [ 0 ] } – $ { R . vlans [ 1 ] } < / span > ` : ' ' } ` ;
const fiche = l . relation != = ' actif ' ? ' <span class= " etiq e-recon " title= " Sa fiche vient de son hébergeur actif. " >de son hébergeur</span> '
: ! l . fiche . deposee ? ' <span class= " etiq e-bloq " >jamais déposée</span> '
: ( l . fiche . ecarts | | [ ] ) . length ? ' <span class= " etiq e-div " >à redéposer</span> '
: ' <span class= " etiq e-actif " >à jour</span> ' ;
return ` < button type = " button " class = " rang loc $ { i === selLocataire ? ' selectionnee ' : ' ' } " onclick = " choisirLocataire($ {i} ) " >
< span class = " pile " > < span class = " carte-nom " > $ { echapper ( l . nom ) } < / span > $ { rel } < / span >
< span class = " pile ip " > $ { idx } < / span >
< span class = " pile ip " > $ { l . face . trace & & l . face . trace . commit ? ` < span > $ { echapper ( l . face . trace . commit ) } < / span > < span class = " bas " > $ { echapper ( quand ( l . face . trace . date ) ) } < / span > ` : ' — ' } < / span >
< span > $ { R ? ` $ { R . actives } / $ { R . machines } ` : ' — ' } < / span >
< span class = " ip " > $ { echapper ( l . ip_publique | | ' — ' ) } < / span >
< span > $ { fiche } < / span > < / button > ` ;
} ;
const absents = ( D . absents | | [ ] ) . length
? ` < div class = " ecarts " style = " list-style:none;padding-left:10px;margin-bottom:14px " > Déclarés dans < span class = " mono " > underlay . yml < / span > sans dépôt ici : $ { echapper ( D . absents . join ( ' , ' ) ) } . Le site ne peut rien lire d ' eux.</div>` : ' ' ;
cible . innerHTML = entete + absents + ( liste . length ? ` < div class = " tableau " > < div class = " tableau-corps loc " >
< div class = " rang tete loc " > < span > Locataire < / span > < span > Index · VLAN < / span > < span > Face lue < / span > < span > Actives < / span > < span > Adresse publique < / span > < span > Fiche déposée < / span > < / div >
$ { liste . map ( rang ) . join ( ' ' ) } < / div > < / div > ` : ' <div class= " vide " style= " padding:14px " >Ce site ne déclare aucun locataire.</div> ' )
+ ` < div class = " contrat " style = " grid-template-columns:repeat(auto-fit,minmax(240px,1fr)) " >
< div class = " note-contrat " > < b style = " color:var(--muted) " > Face lue < / b > — le dernier commit de < span class = " mono " > face - reseau . yml < / span > présent ici . C ' est la seule chose de lui que le site lit, et ce que <span class= " mono " >locataire-creer</span>, <span class= " mono " >creer-vm</span> et la frontière liront.</div>
< div class = " note-contrat " > < b style = " color:var(--muted) " > Fiche déposée < / b > — comparée à ce que ce site calcule aujourd ' hui. Périmée, son inventaire se génère sur d ' anciennes valeurs . < / div >
< div class = " note-contrat " > < b style = " color:var(--muted) " > Pas ici < / b > — la frontière posée , le pare - feu Proxmox , le dépôt de sauvegarde reçu : ils se mesurent par leurs assistants , pas par ces fichiers . < / div >
< / div > ` ;
dessinerDetail ( ) ;
}
function choisirLocataire ( i ) { selLocataire = i ; dessinerLocataires ( ) ; }
function inspecteurLocataire ( ) {
const cible = document . getElementById ( ' detail ' ) ;
const l = locatairesCharge & & ( locatairesCharge . locataires | | [ ] ) [ selLocataire ] ;
if ( ! l ) { cible . innerHTML = ' <div class= " detail " ><div class= " vide " ><b>Aucun locataire</b>Choisis une ligne à gauche.</div></div> ' ; return ; }
const R = l . face . resume ;
const geste = ( cle , titre , sous , cls ) = > ` < div class = " geste " > < span class = " quoi " > < b > $ { echapper ( titre ) } < / b > < span > $ { sous } < / span > < / span >
< button type = " button " class = " $ { cls || ' fantome ' } " onclick = " ouvrirAssistant( ' site-tenir ' ) " > Ouvrir < / button > < / div > ` ;
const gestes = [ ] ;
if ( l . relation == = ' actif ' & & ( l . fiche . ecarts | | [ ] ) . length )
gestes . push ( geste ( ' fiches-site-deposer ' , ' Redéposer sa fiche ' , ' <span class= " mono " >make fiches-site-deposer</span>, puis la commiter chez lui ' ) ) ;
if ( R & & R . planifiees . length )
gestes . push ( geste ( ' creer-vm ' , ' Créer une machine, s \' il la demande ' , ` < span class = " mono " > make creer - vm TENANT = $ { echapper ( l . nom ) } HOTE = … < / span > — pas encore actives : $ { echapper ( R . planifiees . join ( ' , ' ) ) } ` ) ) ;
gestes . push ( geste ( ' frontiere ' , ' Mesurer sa frontière ' , ' <span class= " mono " >make frontiere-plan</span> : l \' écart entre la frontière et ce que les faces demandent ' ) ) ;
cible . innerHTML = ` < div class = " detail " >
< div class = " section-fiche " > < div class = " section-tete " > $ { echapper ( l . nom ) } < / div >
< div class = " hint-dep " > $ { l . relation == = ' actif ' ? ' Ce site est son hébergeur actif. ' : l . relation == = ' reprise ' ? ` Site de reprise : sa face nomme $ { echapper ( ( R & & R . hebergeur ) | | ' ' ) } . ` : ' Aucune face publiée : le site ne sait rien de lui. ' }
$ { l . ip_publique ? ` Adresse publique attribuée : < span class = " mono " > $ { echapper ( l . ip_publique ) } < / span > . ` : ' ' } < / div > < / div >
$ { R ? ` < div class = " section-fiche " > < div class = " section-tete " > Ce qu ' il demande</div>
< p class = " sous-tete " style = " margin-top:4px " > Entrées depuis Internet < / p > $ { lignesFlux ( R . expositions , ' Aucune. ' ) }
< p class = " sous-tete " style = " margin-top:10px " > Sorties vers Internet < / p > $ { lignesFlux ( R . sorties , ' Aucune. ' ) }
$ { R . dns_public . length ? ` < div class = " note-contrat " style = " margin-top:8px " > DNS public : < span class = " mono " > $ { echapper ( R . dns_public . join ( ' , ' ) ) } < / span > < / div > ` : ' ' }
< div class = " note-contrat " style = " margin-top:8px " > Administration : < span class = " mono " > $ { echapper ( ( R . administration . reseaux | | [ ] ) . concat ( R . administration . tunnel | | [ ] ) . join ( ' , ' ) | | ' — ' ) } < / span > $ { R . administration . pairs ? ` · $ { R . administration . pairs } pair ( s ) VPN ` : ' ' } $ { R . sauvegarde ? ' · clé de sauvegarde publiée ' : ' ' } < / div > < / div > ` : ' ' }
< div class = " gestes " > < div class = " gestes-tete " > < span class = " section-tete " style = " margin:0 " > Gestes pour ce locataire < / span > < / div >
$ { gestes . join ( ' ' ) }
< div class = " gestes-note " > Ils vivent dans l ' assistant « Tenir un site en état ». Raser un locataire y est aussi, nom écrit en toutes lettres.</div></div>
< div class = " section-fiche " > < div class = " section-tete " > Ce que je ne vois pas < / div >
< div class = " hint-dep " > Ses services , ses comptes , son plan au - delà de sa face , la validité de ses sauvegardes . Je n ' ai pas sa voûte, et c ' est voulu . < / div > < / div >
< / div > ` ;
}
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
/ / A TRAITER ( lot 3 a , 2026 - 10 - 11 ) . Ce que la console sait deja , au meme endroit :
/ / la veille du serveur ( genome , face , fiche , plan , locataires ) et ce que la page
/ / mesure elle - meme ( joignabilite , prerequis ) . Pas encore les demandes ni la supervision .
let aTraiterCharge = null ;
let selVeille = 0 ;
let filtreVeille = ' ' ;
const RANG_GRAVITE = { agir : 0 , savoir : 1 , ok : 2 } ;
function veilleDeLaPage ( ) {
const out = [ ] ;
const actives = hotes . filter ( h = > h . etat == = ' actif ' ) ;
if ( ! Object . keys ( sondes ) . length ) {
out . push ( { source : ' MACHINES ' , gravite : ' savoir ' , titre : ' Joignabilité pas encore mesurée ' ,
impact : ' La sonde tourne en arrière-plan après le chargement. ' , detail : ' ' , geste : null } ) ;
} else {
const muettes = actives . filter ( h = > sondes [ h . nom ] == = false ) . map ( h = > h . nom ) ;
out . push ( muettes . length
? { source : ' MACHINES ' , gravite : ' agir ' , titre : ` $ { muettes . length } machine ( s ) active ( s ) injoignable ( s ) ` ,
impact : ' Ni vérification ni déploiement ne passeront ; ce qu \' elles rendent est peut-être arrêté. ' ,
detail : ' sonde SSH depuis cette console · ' + muettes . join ( ' , ' ) , geste : { libelle : ' Parc ' , vue : ' serveurs ' } }
: { source : ' MACHINES ' , gravite : ' ok ' , titre : ` $ { actives . length } machine ( s ) active ( s ) joignables ` ,
impact : ' ' , detail : ' sonde SSH depuis cette console ' , geste : null } ) ;
}
const bloquees = hotes . filter ( h = > hoteBloque ( h ) ) . map ( h = > h . nom ) ;
if ( bloquees . length ) out . push ( { source : ' PRÉREQUIS ' , gravite : ' savoir ' , titre : ` $ { bloquees . length } machine ( s ) attendent un prérequis ` ,
impact : ' Un groupe qu \' elles portent en suppose un autre, pas encore actif. ' , detail : bloquees . join ( ' , ' ) ,
geste : { libelle : ' Parc ' , vue : ' serveurs ' } } ) ;
return out ;
}
function elementsVeille ( ) {
const tous = ( ( aTraiterCharge & & aTraiterCharge . elements ) | | [ ] ) . concat ( veilleDeLaPage ( ) ) ;
return tous . map ( ( e , i ) = > ( { . . . e , _i : i } ) )
. sort ( ( a , b ) = > ( RANG_GRAVITE [ a . gravite ] ? ? 1 ) - ( RANG_GRAVITE [ b . gravite ] ? ? 1 ) | | a . _i - b . _i ) ;
}
function compteAgir ( ) {
return aTraiterCharge ? elementsVeille ( ) . filter ( e = > e . gravite == = ' agir ' ) . length : ' ' ;
}
async function prechargerATraiter ( ) {
if ( ! aCote ( ' a_traiter ' ) | | aTraiterCharge ) return ;
try { aTraiterCharge = await lireContrat ( ' /api/a-traiter ' ) ; dessinerNavigation ( ) ; } catch ( e ) { / * la vue le dira * / }
}
function etiquetteGravite ( g ) {
return { agir : ' <span class= " etiq e-div " >à prendre</span> ' , savoir : ' <span class= " etiq e-recon " >à savoir</span> ' ,
ok : ' <span class= " etiq e-actif " >vérifié</span> ' } [ g ] | | ' ' ;
}
function lancerGeste ( i ) {
const e = elementsVeille ( ) [ i ] ;
if ( ! e | | ! e . geste ) return ;
if ( e . geste . runbook ) ouvrirAssistant ( e . geste . runbook ) ;
else if ( e . geste . vue ) setVue ( e . geste . vue ) ;
2026-10-11 22:47:06 -04:00
else if ( e . geste . url ) window . open ( e . geste . url , ' _blank ' , ' noopener ' ) ;
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
}
function copierCommande ( texte ) {
try { navigator . clipboard . writeText ( texte ) ; message ( ' Commande copiée. ' , ' ok ' ) ; } catch ( e ) { / * rien * / }
}
async function dessinerATraiter ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
if ( ! aTraiterCharge ) {
cible . innerHTML = ' <div class= " vide " style= " padding:14px " >Lecture de la veille : génome, contrat, plan…</div> ' ;
try { aTraiterCharge = await lireContrat ( ' /api/a-traiter ' ) ; }
catch ( e ) { cible . innerHTML = ' <div class= " vide " style= " padding:14px " > ' + echapper ( e . message ) + ' </div> ' ; return ; }
}
const tous = elementsVeille ( ) ;
const sources = [ . . . new Set ( tous . filter ( e = > e . gravite != = ' ok ' ) . map ( e = > e . source ) ) ] ;
if ( filtreVeille & & ! sources . includes ( filtreVeille ) ) filtreVeille = ' ' ;
const vus = tous . filter ( e = > ! filtreVeille | | e . source == = filtreVeille ) ;
const aFaire = vus . filter ( e = > e . gravite != = ' ok ' ) , faits = vus . filter ( e = > e . gravite == = ' ok ' ) ;
if ( selVeille > = tous . length ) selVeille = 0 ;
const n = ( g ) = > tous . filter ( e = > e . gravite == = g ) . length ;
const sousTitre = contexte & & contexte . portee == = ' site '
? ' Le génome, ce que les locataires ont publié, et les machines du site — du plus pressant au moins pressant. '
: ' Le génome, le contrat avec le site, le plan et les machines — du plus pressant au moins pressant. ' ;
const rang = ( e ) = > ` < button type = " button " class = " veille-rang g-$ {e.gravite} $ { tous.indexOf(e) === selVeille ? ' selectionnee ' : ' ' } " onclick = " choisirVeille($ { tous.indexOf(e)}) " >
< span class = " barre " > < / span >
< span class = " quoi " > < span class = " ligne1 " > < span class = " src " > $ { echapper ( e . source ) } < / span > < span class = " titre " > $ { echapper ( e . titre ) } < / span > < / span >
$ { e . impact ? ` < span class = " impact " > $ { echapper ( e . impact ) } < / span > ` : ' ' }
$ { e . detail ? ` < span class = " tech " title = " $ { echapper(e.detail)} " > $ { echapper ( e . detail ) } < / span > ` : ' ' } < / span >
< span class = " etat " > $ { etiquetteGravite ( e . gravite ) } < / span > < / button > ` ;
cible . innerHTML = ` < div class = " vue-tete " > < div style = " display:flex;flex-direction:column;gap:2px " >
< h2 class = " vue-titre " > À traiter < / h2 > < span class = " vue-sous " > $ { echapper ( sousTitre ) } < / span > < / div > < / div >
< div class = " veille-chiffres " >
< div class = " chiffre " > < span class = " v " > $ { n ( ' agir ' ) } < / span > < span class = " l " > à prendre < / span > < / div >
< div class = " chiffre " > < span class = " v " > $ { n ( ' savoir ' ) } < / span > < span class = " l " > à savoir < / span > < / div >
< div class = " chiffre " > < span class = " v " > $ { n ( ' ok ' ) } < / span > < span class = " l " > vérifiés , rien à faire < / span > < / div >
< / div >
$ { sources . length > 1 ? ` < div class = " veille-filtres " > < button type = " button " class = " fantome $ { filtreVeille ? ' ' : ' on ' } " onclick = " filtrerVeille( ' ' ) " > Tout < / button > `
+ sources . map ( s = > ` < button type = " button " class = " fantome $ { filtreVeille === s ? ' on ' : ' ' } " onclick = " filtrerVeille( ' $ { echapper(s)} ' ) " > $ { echapper ( s ) } < / button > ` ) . join ( ' ' ) + ' </div> ' : ' ' }
$ { aFaire . length ? ` < div class = " veille-liste " > $ { aFaire . map ( rang ) . join ( ' ' ) } < / div > `
: ' <div class= " vide " style= " padding:14px " >Rien à prendre ni à savoir ' + ( filtreVeille ? ' pour ce filtre ' : ' ' ) + ' .</div> ' }
$ { faits . length ? ` < details class = " veille-ok " $ { aFaire . length ? ' ' : ' open ' } > < summary > Vérifié , rien à faire ( $ { faits . length } ) < / summary >
< div class = " veille-liste " > $ { faits . map ( rang ) . join ( ' ' ) } < / div > < / details > ` : ' ' }
< div class = " note-contrat " > Pas encore ici : les demandes entre locataire et site ( par la forge ) , la supervision , les sauvegardes et les échéances . < / div > ` ;
dessinerNavigation ( ) ;
dessinerDetail ( ) ;
}
function choisirVeille ( i ) { selVeille = i ; dessinerATraiter ( ) ; }
function filtrerVeille ( s ) { filtreVeille = s ; dessinerATraiter ( ) ; }
function inspecteurVeille ( ) {
const cible = document . getElementById ( ' detail ' ) ;
const e = aTraiterCharge ? elementsVeille ( ) [ selVeille ] : null ;
if ( ! e ) { cible . innerHTML = ' <div class= " detail " ><div class= " vide " ><b>Rien de choisi</b>Choisis une ligne à gauche.</div></div> ' ; return ; }
const g = e . geste ;
const geste = ! g ? ' ' : g . commande
? ` < div class = " commande " > < span > $ { echapper ( g . commande ) } < / span > < button type = " button " class = " fantome " onclick = " copierCommande($ { echapper(JSON.stringify(g.commande))}) " > Copier < / button > < / div >
< div class = " gestes-note " > $ { echapper ( g . libelle ) } < / div > `
2026-10-11 22:47:06 -04:00
: g . url
? ` < div class = " geste " > < span class = " quoi " > < b > $ { echapper ( g . libelle ) } < / b > < span class = " mono " > $ { echapper ( g . url ) } < / span > < / span > < a class = " btn-lien " href = " $ { echapper(g.url)} " target = " _blank " rel = " noopener " > Ouvrir < / a > < / div > `
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
: ` < div class = " geste " > < span class = " quoi " > < b > $ { echapper ( g . libelle ) } < / b > < / span > < button type = " button " class = " primaire " onclick = " lancerGeste($ {selVeille} ) " > Ouvrir < / button > < / div > ` ;
cible . innerHTML = ` < div class = " detail " >
< div class = " section-fiche " > < div class = " section-tete " > $ { echapper ( e . source ) } < / div >
< div style = " display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:8px " > < b style = " font-size:15px " > $ { echapper ( e . titre ) } < / b > $ { etiquetteGravite ( e . gravite ) } < / div >
$ { e . impact ? ` < div class = " hint-dep " > $ { echapper ( e . impact ) } < / div > ` : ' ' }
$ { e . detail ? ` < div class = " note-contrat " style = " margin-top:8px;font-family:ui-monospace,monospace;overflow-wrap:anywhere " > $ { echapper ( e . detail ) } < / div > ` : ' ' } < / div >
$ { geste ? ` < div class = " gestes " > < div class = " gestes-tete " > < span class = " section-tete " style = " margin:0 " > Le geste < / span > < / div > $ { geste } < / div > ` : ' ' }
< / div > ` ;
}
2026-06-24 20:17:46 -04:00
function dessinerServeurs ( ) {
console de site : un vide doit avoir la forme de ce qu'il remplace
La console du runner de SITE-Chezlepro ne montrait rien. Elle a douze machines.
inventaire_api sert serveurs en LISTE, inventaire_api_du_site le servait en TABLE. Les deux
sont vides et la page ne les lit pas pareil : (data.serveurs || []).map trouve {} — truthy,
sans .map — et charger() leve. La console d'un site mourait avant sa premiere vue, et le
message accusait une methode manquante plutot qu'une forme qui ment.
Et la vue regardait au mauvais endroit : le registre vide est voulu, mais la page en tirait
le message d'un locataire sans serveurs et les douze machines servies dans hotes n'etaient
dessinees nulle part. Une console de site dessine desormais SES machines, en lecture seule,
avec leur adresse, leur VMID, leurs roles et leur point de vie ; le panneau droit dit ce
qu'elle peut et renvoie aux Assistants.
C'est le defaut du 16 septembre par une autre porte : la premiere fois l'inventaire etait
vide et la page dessinait ce vide comme un plan vide ; cette fois il est plein et c'est la
vue qui regarde ailleurs.
Ce qui a trouve la cause n'est pas une lecture mais le BANC : en donnant pour la premiere
fois a test_rendu_gui la charge d'une console de site, il a leve sur charger(). Sans lui
j'aurais livre une vue juste par dessus un charger() qui leve.
Valide : node --check sur le bloc script, 7 tests de rendu dont deux neufs, make test a 0
echec, runbooks.py verifier a 0 ecart, P81 et P83 vertes. P02 reste en echec pour la raison
anterieure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:36:00 -04:00
/ / Une console qui n ' edite aucun plan n ' a pas de registre ` serveurs ` — et elle a
/ / pourtant des machines . On dessine CE QU ' ELLE A.
if ( ! peut ( ' editer_le_plan ' ) ) return dessinerMachinesDuSite ( ) ;
2026-06-24 20:17:46 -04:00
const cible = document . getElementById ( ' grilles ' ) ;
2026-10-11 14:43:01 -04:00
const entete = ` < div class = " vue-tete " > < div style = " display:flex;flex-direction:column;gap:2px " >
< h2 class = " vue-titre " > Parc < / h2 >
< span class = " vue-sous " > Les serveurs du plan , à côté de ce que la flotte montre vraiment . < / span > < / div >
< span class = " grandit " > < / span >
< button type = " button " class = " fantome " onclick = " ajouterServeur() " > + Serveur < / button >
< button type = " button " class = " fantome " onclick = " appliquerPlan() " > ⚙ Appliquer le plan < / button > < / div > ` + BANNIERE_MODELE ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
if ( ! serveurs . length ) { cible . innerHTML = entete + ' <div class= " vide " style= " padding:14px " >Aucun serveur. « + Serveur » ou <span class= " mono " >make serveurs-bootstrap</span>.</div> ' ; return ; }
2026-10-11 14:43:01 -04:00
const groupes = serveursEnGroupes ( ) ;
if ( ! groupes . length ) { cible . innerHTML = entete + ' <div class= " vide " style= " padding:14px " >Aucun serveur ne correspond au filtre.</div> ' ; return ; }
cible . innerHTML = entete + ` < div class = " tableau " > < div class = " tableau-corps " >
< div class = " rang tete " > < span > Serveur < / span > < span > Adresse < / span > < span > Plan < / span > < span > Réel < / span > < span > Inventaire < / span > < span > Services rendus < / span > < / div >
$ { groupes . map ( g = > ` < div class = " groupe-tete " > $ { echapper ( g . libelle ) } $ { g . vlan ? ` < span class = " mono " > VLAN $ { echapper ( g . vlan ) } < / span > ` : ' ' } < span class = " compte " > $ { g . items . length } < / span > < / div > `
+ g . items . map ( ( [ s , i ] ) = > rangServeur ( s , i ) ) . join ( ' ' ) ) . join ( ' ' ) }
< / div > < / div > ` ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
}
function inspecteurServeur ( ) {
console de site : un vide doit avoir la forme de ce qu'il remplace
La console du runner de SITE-Chezlepro ne montrait rien. Elle a douze machines.
inventaire_api sert serveurs en LISTE, inventaire_api_du_site le servait en TABLE. Les deux
sont vides et la page ne les lit pas pareil : (data.serveurs || []).map trouve {} — truthy,
sans .map — et charger() leve. La console d'un site mourait avant sa premiere vue, et le
message accusait une methode manquante plutot qu'une forme qui ment.
Et la vue regardait au mauvais endroit : le registre vide est voulu, mais la page en tirait
le message d'un locataire sans serveurs et les douze machines servies dans hotes n'etaient
dessinees nulle part. Une console de site dessine desormais SES machines, en lecture seule,
avec leur adresse, leur VMID, leurs roles et leur point de vie ; le panneau droit dit ce
qu'elle peut et renvoie aux Assistants.
C'est le defaut du 16 septembre par une autre porte : la premiere fois l'inventaire etait
vide et la page dessinait ce vide comme un plan vide ; cette fois il est plein et c'est la
vue qui regarde ailleurs.
Ce qui a trouve la cause n'est pas une lecture mais le BANC : en donnant pour la premiere
fois a test_rendu_gui la charge d'une console de site, il a leve sur charger(). Sans lui
j'aurais livre une vue juste par dessus un charger() qui leve.
Valide : node --check sur le bloc script, 7 tests de rendu dont deux neufs, make test a 0
echec, runbooks.py verifier a 0 ecart, P81 et P83 vertes. P02 reste en echec pour la raison
anterieure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:36:00 -04:00
if ( ! peut ( ' editer_le_plan ' ) ) return inspecteurMachineDuSite ( ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
const cible = document . getElementById ( ' detail ' ) ;
if ( selServeur == null | | ! serveurs [ selServeur ] ) {
hoteOpCourant = ' ' ;
2026-10-11 14:43:01 -04:00
cible . innerHTML = ' <div class= " detail " ><div class= " vide " ><b>Aucun serveur sélectionné</b>Choisis une ligne à gauche, ou ajoute un serveur.</div></div> ' ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
return ;
}
const s = serveurs [ selServeur ] ;
const set = new Set ( listeDepuisTexte ( s . integrations ) ) ;
const tousRoles = [ . . . new Set ( [ . . . integrationsDispo , . . . set ] ) ] ;
const cases = tousRoles . length
? tousRoles . map ( role = > ` < label class = " integ-case " title = " $ { echapper(role)} " > < input type = " checkbox " $ { set . has ( role ) ? ' checked ' : ' ' } onchange = " basculerIntegration( ' $ { echapper(role)} ' , this.checked) " > < span > $ { echapper ( role . replace ( / ^ client_ / , ' ' ) ) } < / span > < / label > ` ) . join ( ' ' )
: ' <span class= " dsn " >aucun rôle client_* disponible</span> ' ;
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
/ / Integrations universelles : montrees , jamais cochables . Sans cet affichage ,
/ / un plan silencieux se lirait « cet hote n ' est pas supervise » — l ' inverse
/ / exact de la politique . L ' exemption `sauf_role` s ' evalue ici comme au moteur .
const rendus = new Set ( applications . filter ( a = > a . hote == = s . nom ) . map ( a = > a . groupe ) ) ;
const univ = Object . entries ( integrationsUniv )
. filter ( ( [ role , p ] ) = > ! ( p . sauf_role & & rendus . has ( p . sauf_role ) ) )
. map ( ( [ role , p ] ) = > ` < span class = " integ-case integ-univ " title = " $ { echapper(p.raison || ' ' )} " > ✓ $ { echapper ( role . replace ( / ^ client_ / , ' ' ) ) } < / span > ` ) . join ( ' ' ) ;
const exempts = Object . entries ( integrationsUniv )
. filter ( ( [ role , p ] ) = > p . sauf_role & & rendus . has ( p . sauf_role ) )
. map ( ( [ role , p ] ) = > ` < span class = " integ-case integ-exempt " title = " Exempté : cet hôte rend le service $ { echapper(p.sauf_role)} " > — $ { echapper ( role . replace ( / ^ client_ / , ' ' ) ) } < / span > ` ) . join ( ' ' ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
const info = infoFonction ( s . fonction ) ;
const zoneHint = info ? ` < span class = " zone-hint " title = " Dérivé de la nomenclature " > $ { echapper ( info . libelle ) } · VLAN $ { info . vlan } < / span > ` : ' ' ;
const h = hotes . find ( x = > x . nom == = s . nom ) ;
const apps = applications . filter ( a = > a . hote == = s . nom ) ;
const appsHtml = apps . length
2026-10-11 14:43:01 -04:00
? ` < div class = " ch-bases " style = " margin-left:0 " > $ { apps . map ( a = > ` < div class = " ch-base " > < span class = " badge-bd " > app < / span > < button type = " button " class = " lien-objet " onclick = " ouvrirApp( ' $ { echapper(a.id)} ' ) " > $ { echapper ( a . id | | ' (sans id) ' ) } < / button > $ { a . expose ? ` < span class = " mono " > → $ { echapper ( listeDepuisTexte ( a . expose ) . join ( ' , ' ) ) } < / span > ` : ' ' } < / div > ` ) . join ( ' ' ) } < / div > `
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
: ' <div class= " ch-stub " >aucune application sur cet hôte</div> ' ;
const bds = bdServeurs . filter ( x = > x . hote == = s . nom ) ;
const bdHtml = bds . length
2026-10-11 14:43:01 -04:00
? ` < div class = " ch-bases " style = " margin-left:0 " > $ { bds . map ( x = > ` < div class = " ch-base " > < span class = " badge-bd " > $ { echapper ( x . type ) } < / span > < button type = " button " class = " lien-objet " onclick = " ouvrirBaseServeur( ' $ { echapper(x.nom)} ' ) " > $ { echapper ( x . nom ) } < / button > < span class = " mono " > : $ { echapper ( x . port ) } < / span > < / div > ` ) . join ( ' ' ) } < / div > `
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
: ' <div class= " ch-stub " >aucun serveur de base ici</div> ' ;
hoteOpCourant = s . nom ;
2026-07-01 15:58:32 -04:00
const hi = h ? hotes . indexOf ( h ) : - 1 ;
2026-09-16 14:57:39 -04:00
/ / CE QUE LA PORTEE INTERDIT NE SE PROPOSE PAS COMME LE RESTE . Le bouton demeure —
/ / le retirer ferait croire que le geste n ' existe pas — mais il est inerte et porte
/ / sa raison en infobulle . Meme geste que les integrations universelles , affichees
/ / en lecture seule plutot qu ' omises.
const inerte = ( pouvoir ) = > peut ( pouvoir ) ? ' '
: ` disabled title = " $ { echapper(pourquoiPas(pouvoir))} " style = " opacity:.45 " ` ;
2026-10-11 14:43:01 -04:00
/ / LES GESTES EN TETE DE FICHE , DANS LEUR ORDRE ( 2026 - 10 - 11 ) . Ils etaient en pied ,
/ / sous le formulaire : on les cherchait . L ' ordre Verifier → Deployer est celui que
/ / ` majBoutonsDeploiement ` impose deja ; la fiche le montre au lieu de le cacher .
let gestes = ' ' ;
if ( h ) {
const bloque = h . etat == = ' actif ' & & hoteBloque ( h ) ;
const creer = ` < div class = " geste " > < span class = " num " > ⊕ < / span > < div class = " quoi " > < b > Créer la VM < / b > < span > Cloner depuis le gabarit doré ( Proxmox ) . < / span > < / div >
< button type = " button " $ { peut ( ' materialiser ' ) ? ' title= " Cloner la VM depuis le golden template (Proxmox) " ' : inerte ( ' materialiser ' ) } onclick = " lancer( ' creer ' , $ {hi} ) " > 🖥 Créer la VM < / button > < / div > ` ;
let suite = ' ' ;
if ( h . etat == = ' actif ' & & ! bloque ) {
const clsDep = estProduction ? ' prod ' : ' primaire ' ;
suite = ` < div class = " geste " > < span class = " num " > 1 < / span > < div class = " quoi " > < b > Vérifier < / b > < span > Simulation < code > - - check - - diff < / code > , n ' écrit rien.</span></div>
< button type = " button " id = " btn-verifier " $ { inerte ( ' configurer ' ) } onclick = " lancer( ' verifier ' , $ {hi} ) " > Vérifier < / button > < / div >
< div class = " geste " > < span class = " num " > 2 < / span > < div class = " quoi " > < b > Déployer < / b > < span > S ' ouvre après une vérification réussie dans cette session.</span></div>
< button type = " button " id = " btn-deployer " class = " $ {clsDep} " $ { inerte ( ' configurer ' ) } onclick = " lancer( ' deployer ' , $ {hi} ) " > Déployer < / button > < / div > ` ;
} else if ( bloque ) {
suite = ` < div class = " gestes-note " style = " border:0;padding:0;color:var(--rouge) " > Prérequis manquant : $ { echapper ( [ . . . new Set ( ( h . groupes | | [ ] ) . flatMap ( g = > manquantsPourGroupe ( g ) ) ) ] . join ( ' , ' ) ) } < / div > ` ;
} else {
suite = ' <div class= " gestes-note " style= " border:0;padding:0 " >Planifié : passe-le à « actif » au plan pour le vérifier et le déployer.</div> ' ;
}
gestes = ` < div class = " gestes " >
< div class = " gestes-tete " > < span class = " section-tete " style = " margin:0 " > Gestes sur ce serveur < / span > < span class = " grandit " > < / span > $ { estProduction ? ' <span class= " badge-prod " >PROD</span> ' : ' ' } < / div >
$ { suite }
$ { peut ( ' materialiser ' ) ? creer : ` < div class = " gestes-note " title = " $ { echapper(pourquoiPas( ' materialiser ' ))} " > Créer la VM relève de la console du site : celle - ci n ' a pas sa voûte.</div>`}
< / div > ` ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
} else {
2026-10-11 14:43:01 -04:00
gestes = ` < div class = " gestes " > < div class = " gestes-note " style = " border:0;padding:0 " > Applique le plan pour instancier cet hôte ( ⚙ Appliquer le plan ) . < / div > < / div > ` ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
}
2026-10-11 14:43:01 -04:00
const statBadge = etiquetteInventaire ( s . statut ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
cible . innerHTML = ` < article class = " detail " >
< div class = " detail-tete " > < div class = " detail-nom " > < span class = " detail-nom-txt " > $ { echapper ( s . nom | | ' (sans nom) ' ) } < / span > $ { zoneHint } < / div >
2026-10-11 14:43:01 -04:00
< span class = " grandit " > < / span > $ { etiquettePlan ( s . etat ) } $ { statBadge }
< button type = " button " class = " danger " title = " Retirer ce serveur " aria - label = " Retirer ce serveur du plan " onclick = " retirerServeur() " > ✕ < / button > < / div >
$ { gestes }
< div class = " stats " > $ { tuile ( s . vmid , ' VMID · dérivé ' ) } $ { tuile ( s . adresse_ip , ' IP · dérivée ' ) } $ { tuile ( s . vlan , ' VLAN · dérivé ' ) } < / div >
< div class = " dsn " style = " padding:0 16px 4px " > < b > Plan < / b > : $ { echapper ( s . etat ) } & nbsp ; · & nbsp ; < b > Réel < / b > : $ { reelDe ( s . nom ) } < / div >
< div class = " section-fiche " > < div class = " section-tete " > Ce qu ' il rend</div>$ {appsHtml} $ {bdHtml} </div>
< div class = " section-fiche " > < div class = " section-tete " > Intégrations < / div >
< div class = " integ-cases " > $ { univ } $ { exempts } $ { cases } < / div >
< span class = " dsn " style = " font-size:11px " > Les ✓ sont la politique des rôles : elles s ' appliquent à tout hôte et ne se décochent pas.</span></div>
< div class = " section-fiche " > < div class = " section-tete " > Identité au plan < span class = " compte " > vide = défaut < / span > < / div >
$ { formulaireDepuisSchema ( ' serveurs ' , s , ' definirServeur ' , s . nom , " definirServeur.bind(null, ' nom ' ) " , ' nom ' ) } < / div >
< div class = " dsn " style = " padding:10px 16px 16px;color:#94a3b8 " > VMID / IP / VLAN sont < b > dérivés < / b > . Sauvegarde ( Ctrl + S ) puis « Appliquer le plan » pour régénérer l ' inventaire.</div>
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
< / article > ` ;
majBoutonsDeploiement ( ) ;
2026-06-24 20:17:46 -04:00
}
2026-06-30 12:19:26 -04:00
function serveurValeurValide ( cle , v ) {
const s = String ( v == null ? ' ' : v ) . trim ( ) ;
if ( s == = ' ' ) return true ; / / vide = dérivé / défaut , autorisé
if ( cle == = ' nom ' ) return / ^ [ a - z0 - 9 ] + ( - [ a - z0 - 9 ] + ) * - \d + $ / . test ( s ) ;
if ( cle == = ' memoire ' ) return / ^ \d + $ / . test ( s ) & & + s > = 64 & & + s < = 1048576 ;
if ( cle == = ' coeurs ' ) return / ^ \d + $ / . test ( s ) & & + s > = 1 & & + s < = 256 ;
if ( cle == = ' disque ' ) return / ^ \d + \s * [ GMgm ] ? $ / . test ( s ) ;
return true ;
}
function validerChampServeur ( el ) {
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
const ok = serveurValeurValide ( el . getAttribute ( ' data-champ ' ) , el . value ) ;
2026-06-30 12:19:26 -04:00
el . classList . toggle ( ' invalide ' , ! ok ) ;
return ok ;
}
2026-06-24 20:17:46 -04:00
async function sauvegarderServeurs ( ) {
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
const champs = Array . from ( document . querySelectorAll ( ' #detail [data-champ] ' ) ) ;
const invalides = champs . filter ( el = > ! validerChampServeur ( el ) ) ;
/ / Un controle qui n ' inspecte AUCUN champ n ' est pas un controle : si le formulaire
/ / cesse un jour de marquer ses contröles , on le dit plutot que de laisser passer .
if ( ! champs . length ) { message ( ' Formulaire non marqué : sauvegarde refusée. ' , ' erreur ' ) ; return ; }
2026-06-30 12:19:26 -04:00
if ( invalides . length ) {
message ( ' Corrige les ' + invalides . length + ' champ(s) en rouge avant de sauvegarder. ' , ' erreur ' ) ;
invalides [ 0 ] . focus ( ) ;
return ;
}
2026-06-24 20:17:46 -04:00
const srv = { } ;
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
const propsS = ( ( schemaPlan . serveurs | | { } ) . entite | | { } ) . properties | | { } ;
2026-06-24 20:17:46 -04:00
serveurs . forEach ( s = > {
if ( ! String ( s . nom ) . trim ( ) ) return ;
GUI : les six registres ont un formulaire genere, et la sauvegarde aussi
CHAMPS_ECRITS_A_LA_MAIN est vide. Serveurs et applications, les deux plus
gros, sont passes au generateur — chargement, rendu et sauvegarde.
L EPREUVE QUI COMPTE. Ouvrir chaque vue et enregistrer sans rien toucher
doit renvoyer exactement le plan qu on vient de lire : 14 serveurs, 25
applications, 2 domaines, 4 bases, IDENTIQUE partout. C est ce qui separe
un formulaire genere d un formulaire qui en a l air — un champ visible a
l ecran et perdu en silence a l enregistrement serait le pire des deux
mondes. test_rendu_gui.py le mesure a chaque make prouver.
TROIS DEFAUTS TROUVES EN CHEMIN.
Le formulaire annoncait des defauts INVENTES : 2048 Mo, 2 coeurs, 16G. Il
n existe aucun defaut fixe — deriver_ressources calcule depuis les roles
portes (1024 et 1 pour infra-pki-01, 5632 et 4 pour collab-01). Un repere
faux fait croire qu on connait la valeur. Le schema nomme le champ derive,
et l ecran montre la valeur reelle de cet hote. L option vide d un select
dit desormais ce qu elle produira : « (defaut : asgard) ».
Une SECONDE occurrence du defaut d hier dormait dans sourceDeValeurs :
elle lisait encore data.nomenclature. Elle n avait jamais leve parce que
la vue Serveurs, seule a emprunter cette source, avait un formulaire ecrit
a la main. Elle a leve a la seconde ou le generateur l a prise. Le banc ne
voit que les chemins vivants : verifier_gui.py fait donc aussi une
verification STATIQUE, qui voit ce qui dort.
La validation client s accrochait a data-v, pose a la main sur trois
champs. Le formulaire genere l aurait perdu et la validation serait passee
au vert sur ZERO champ. Le generateur marque chaque controle, et la
sauvegarde refuse si elle n en inspecte aucun.
DEUX CHAMPS GARDENT LEUR EDITEUR, et le schema le dit (x-editeur) : la
matrice des integrations montre les universelles et les exemptions, et
l editeur de liens contraint le role a meta/liens.yml. Le generateur s
efface plutot que de remplacer un editeur qui en sait plus que lui.
LIMITE : je n ai toujours pas ouvert ces pages dans un navigateur.
make prouver : CONFORME, 61 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 18:26:33 -04:00
const o = { } ;
Object . keys ( propsS ) . forEach ( cle = > {
const spec = propsS [ cle ] ;
if ( spec . type == = ' array ' ) {
const l = listeDepuisTexte ( s [ cle ] ) ; if ( l . length ) o [ cle ] = l ; return ;
}
const v = s [ cle ] ;
if ( v == = undefined | | v == = null | | String ( v ) . trim ( ) == = ' ' ) return ;
o [ cle ] = ( spec . type == = ' integer ' ) ? ( parseInt ( v , 10 ) | | v ) : v ;
} ) ;
/ / ` etat ` a un defaut METIER , pas un defaut de schema : un serveur sans etat est
/ / planifie , jamais actif . Le declarer ici plutot qu ' au schema evite de le poser
/ / par ecrit dans chaque plan .
if ( ! o . etat ) o . etat = ' planifie ' ;
2026-06-24 20:17:46 -04:00
srv [ s . nom ] = o ;
} ) ;
const rep = await fetch ( ' /api/serveurs ' , {
method : ' POST ' , headers : { ' Content-Type ' : ' application/json ' , ' X-Jeton ' : JETON } , body : JSON . stringify ( { serveurs : srv } )
} ) ;
const data = await rep . json ( ) ;
if ( ! rep . ok ) { message ( data . erreur | | ' Sauvegarde refusée. ' , ' erreur ' ) ; return ; }
chargerServeurs ( data ) ; dessinerServeurs ( ) ;
message ( ' Serveurs (plan) sauvegardés. Pense à « Appliquer le plan ». ' , ' ok ' ) ;
}
async function appliquerPlan ( ) {
2026-06-30 12:19:26 -04:00
const t = message ( ' Application du plan… ' , ' ' , { persistant : true } ) ;
try {
const rep = await fetch ( ' /api/instancier ' , { method : ' POST ' , headers : { ' Content-Type ' : ' application/json ' , ' X-Jeton ' : JETON } , body : ' {} ' } ) ;
const data = await rep . json ( ) ;
const derniere = ( data . sortie | | ' ' ) . split ( ' \n ' ) . pop ( ) ;
majToast ( t , ( data . ok ? ' Plan appliqué : ' : ' Échec : ' ) + derniere , data . ok ? ' ok ' : ' erreur ' , data . ok ? 4000 : 8000 ) ;
if ( data . ok ) charger ( ) ;
} catch ( e ) {
majToast ( t , ' Erreur réseau : ' + e , ' erreur ' , 8000 ) ;
}
2026-06-24 20:17:46 -04:00
}
function dessinerDependances ( ) {
const cible = document . getElementById ( ' dependances ' ) ;
const noms = Object . keys ( dependances ) . sort ( ) ;
const bloques = noms . filter ( g = > statutDependance ( g ) == = ' bloque ' ) . length ;
const resume = document . getElementById ( ' dep-resume ' ) ;
resume . textContent = bloques ? bloques + ' bloqués ' : noms . length + ' suivis ' ;
if ( ! noms . length ) { cible . innerHTML = ' <div class= " vide " style= " padding:14px " >Aucune dépendance.</div> ' ; return ; }
cible . innerHTML = noms . map ( groupe = > {
const config = dependances [ groupe ] | | { } ;
const requis = config . requiert_groupes_actifs | | [ ] ;
const statut = statutDependance ( groupe ) ;
const manquants = manquantsPourGroupe ( groupe ) ;
const detail = requis . length ? ` requiert $ { requis . join ( ' , ' ) } ` : ' aucun prérequis ' ;
const manque = requis . length & & manquants . length ? ` manquant : $ { manquants . join ( ' , ' ) } ` : ' ' ;
return `
< div class = " carte-dep $ {statut} " title = " $ { echapper(config.raison || ' ' )} " >
< div class = " haut " > < span class = " nom " > $ { echapper ( groupe ) } < / span > < span class = " chip " > < span class = " pt $ { statut === ' ok ' ? ' actif ' : statut === ' bloque ' ? ' bloq ' : ' plan ' } " > < / span > $ { libelleStatut ( statut ) } < / span > < / div >
< div class = " txt " > $ { echapper ( detail ) } < / div >
$ { manque ? ` < div class = " txt " > $ { echapper ( manque ) } < / div > ` : ' ' }
< / div > ` ;
} ) . join ( ' ' ) ;
}
function tuile ( valeur , libelle ) {
const v = ( valeur == = ' ' | | valeur == null ) ? ' — ' : String ( valeur ) ;
return ` < div class = " stat " > < div class = " v $ { v === ' — ' ? ' vide ' : ' ' } " > $ { echapper ( v ) } < / div > < div class = " l " > $ { echapper ( libelle ) } < / div > < / div > ` ;
}
function dessinerDetail ( ) {
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
/ / Le panneau de droite reflète la sélection de la VUE COURANTE .
2026-07-07 15:57:47 -04:00
if ( vuePrincipale == = ' flux ' | | vuePrincipale == = ' couches ' | | vuePrincipale == = ' reseau ' ) return legendeLecture ( ) ;
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
if ( vuePrincipale == = ' assistants ' ) return inspecteurRunbook ( ) ;
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
if ( vuePrincipale == = ' a-traiter ' ) return inspecteurVeille ( ) ;
2026-10-11 17:31:13 -04:00
if ( vuePrincipale == = ' lien ' ) return inspecteurLien ( ) ;
if ( vuePrincipale == = ' locataires ' ) return inspecteurLocataire ( ) ;
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
if ( vuePrincipale == = ' integrations ' ) return legendeIntegrations ( ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
if ( vuePrincipale == = ' applications ' ) return inspecteurApplication ( ) ;
if ( vuePrincipale == = ' bases ' ) return inspecteurBase ( ) ;
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
if ( vuePrincipale == = ' domaines ' ) return inspecteurDomaine ( ) ;
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
if ( vuePrincipale == = ' nomenclature ' ) return inspecteurNomenclature ( ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
return inspecteurServeur ( ) ;
2026-06-24 20:17:46 -04:00
}
function majBoutonsDeploiement ( ) {
const bv = document . getElementById ( ' btn-verifier ' ) ;
const bd = document . getElementById ( ' btn-deployer ' ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
const hote = hoteOpCourant | | ' ' ;
2026-06-30 12:19:26 -04:00
if ( bv ) { bv . disabled = occupe ; bv . title = occupe ? ' Exécution en cours ' : ' Dry-run (--check --diff), aucune modification ' ; }
if ( bd ) { bd . disabled = occupe | | ! verifie [ hote ] ; bd . title = ! verifie [ hote ] ? ' Vérifiez (dry-run) d \' abord ' : ' Applique les playbooks sur la VM réelle ' ; }
2026-06-24 20:17:46 -04:00
}
function fonctionDe ( nom ) {
const m = ( nom | | ' ' ) . match ( / ^ ( . + ) - ( \d + ) $ / ) ;
return m ? { fonction : m [ 1 ] , seq : parseInt ( m [ 2 ] , 10 ) } : { fonction : nom | | ' ' , seq : null } ;
}
function infoFonction ( fonction ) {
const d = ( nomenclature . fonctions | | { } ) [ fonction ] ;
if ( ! d ) return null ;
const c = ( nomenclature . categories | | { } ) [ d . categorie ] ;
if ( ! c ) return null ;
Adressage derive du seul seed index (rupture, mode compact retire)
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>
2026-07-23 02:58:15 -04:00
/ / VLAN derive du seed index ( miroir de inventory_rules . vlan_de ) : 1000 + index * 10 + zone .
const idx = ( nomenclature . index != null ) ? nomenclature . index : 9 ;
const vlan = 1000 + idx * 10 + Number ( d . categorie ) ;
return { categorie : d . categorie , service : d . service , libelle : c . libelle , vlan } ;
2026-06-24 20:17:46 -04:00
}
function echapper ( valeur ) {
return String ( valeur ) . replace ( / [ & < > " ' ]/g, c => ( { ' & ' : ' & ' , ' < ' : ' < ' , ' > ' : ' > ' , ' " ' : ' & quot ; ' , " ' " : ' ' ' }[c]));
}
function ouvrirConsole ( titre , hote ) {
document . getElementById ( ' console-titre ' ) . textContent = titre + ' · ' + hote ;
document . getElementById ( ' console-sortie ' ) . textContent = ' ' ;
2026-07-07 13:31:02 -04:00
const res = document . getElementById ( ' console-resume ' ) ; res . hidden = true ; res . innerHTML = ' ' ;
2026-06-24 20:17:46 -04:00
const etat = document . getElementById ( ' console-etat ' ) ;
etat . className = ' console-etat actif ' ; etat . textContent = ' ● en cours ' ;
document . getElementById ( ' btn-fermer-console ' ) . disabled = true ;
document . getElementById ( ' console ' ) . hidden = false ;
2026-06-30 12:19:26 -04:00
consoleDebut = Date . now ( ) ;
clearInterval ( chronoConsole ) ;
chronoConsole = setInterval ( ( ) = > {
const s = Math . round ( ( Date . now ( ) - consoleDebut ) / 1000 ) ;
etat . textContent = ' ● en cours · ' + s + ' s ' ;
} , 1000 ) ;
2026-06-24 20:17:46 -04:00
}
function ecrireConsole ( t ) { const p = document . getElementById ( ' console-sortie ' ) ; p . textContent + = t ; p . scrollTop = p . scrollHeight ; }
2026-07-07 13:31:02 -04:00
let dernierResumeTexte = ' ' ;
function afficherResumeErreur ( resume ) {
const el = document . getElementById ( ' console-resume ' ) ;
const echecs = ( resume & & resume . echecs ) | | [ ] ;
if ( ! echecs . length ) { el . hidden = true ; return ; }
const lignes = echecs . map ( e = >
` < li > < b > $ { echapper ( e . tache ) } < / b > — < span class = " re-hote " > $ { echapper ( e . hote ) } < / span > < span class = " re-type " > $ { echapper ( e . type ) } < / span >
< div class = " re-msg " > $ { echapper ( e . message ) } < / div > < / li > ` ) . join ( ' ' ) ;
dernierResumeTexte = echecs . map ( e = > ` • [ $ { e . type } ] $ { e . tache } @ $ { e . hote } : $ { e . message } ` ) . join ( ' \n ' ) ;
el . innerHTML = ` < div class = " re-tete " > ❌ Échec — $ { echecs . length } tâche ( s ) en erreur
< button type = " button " class = " fantome " onclick = " copierResume() " title = " Copier un résumé clair à transmettre " > Copier < / button > < / div >
< ul class = " re-liste " > $ { lignes } < / ul > ` ;
el . hidden = false ;
}
function copierResume ( ) {
if ( navigator . clipboard ) navigator . clipboard . writeText ( dernierResumeTexte )
. then ( ( ) = > message ( ' Résumé copié — à transmettre au mainteneur. ' , ' ok ' ) , ( ) = > { } ) ;
}
2026-06-24 20:17:46 -04:00
function finirConsole ( rc ) {
2026-06-30 12:19:26 -04:00
clearInterval ( chronoConsole ) ; chronoConsole = null ;
const s = Math . round ( ( Date . now ( ) - consoleDebut ) / 1000 ) ;
2026-06-24 20:17:46 -04:00
const etat = document . getElementById ( ' console-etat ' ) ;
etat . className = ' console-etat ' + ( rc == = 0 ? ' ok ' : ' echec ' ) ;
2026-06-30 12:19:26 -04:00
etat . textContent = ( rc == = 0 ? ' ✓ succès ' : ' ✗ échec (rc= ' + rc + ' ) ' ) + ' · ' + s + ' s ' ;
2026-06-24 20:17:46 -04:00
document . getElementById ( ' btn-fermer-console ' ) . disabled = false ;
}
function fermerConsole ( ) { if ( ! occupe ) document . getElementById ( ' console ' ) . hidden = true ; }
function ouvrirModale ( titre , texte ) {
document . getElementById ( ' modale-titre ' ) . textContent = titre ;
document . getElementById ( ' modale-texte ' ) . textContent = texte ;
const champ = document . getElementById ( ' modale-vault ' ) ;
champ . value = motVault | | ' ' ;
document . getElementById ( ' modale ' ) . hidden = false ;
setTimeout ( ( ) = > champ . focus ( ) , 0 ) ;
return new Promise ( res = > { modaleResolveur = res ; } ) ;
}
function confirmerModale ( ) {
const v = document . getElementById ( ' modale-vault ' ) . value ;
document . getElementById ( ' modale ' ) . hidden = true ;
const r = modaleResolveur ; modaleResolveur = null ;
if ( r ) r ( v ) ;
}
function annulerModale ( ) {
document . getElementById ( ' modale ' ) . hidden = true ;
const r = modaleResolveur ; modaleResolveur = null ;
if ( r ) r ( null ) ;
}
2026-06-30 10:07:17 -04:00
function ouvrirIntrants ( ) {
if ( ! intrants ) { message ( ' Intrants indisponibles (rechargez). ' , ' erreur ' ) ; return ; }
rendreIntrants ( ) ;
document . getElementById ( ' intrants ' ) . hidden = false ;
}
function fermerIntrants ( ) { document . getElementById ( ' intrants ' ) . hidden = true ; }
function rendreIntrants ( ) {
const corps = document . getElementById ( ' intrants-corps ' ) ;
const schema = intrants . schema | | [ ] ;
const valeurs = intrants . valeurs | | { } ;
const sections = [ ] ;
const parSection = { } ;
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
const proprioDe = { } ;
2026-06-30 10:07:17 -04:00
schema . forEach ( ch = > {
if ( ! parSection [ ch . section ] ) { parSection [ ch . section ] = [ ] ; sections . push ( ch . section ) ; }
parSection [ ch . section ] . push ( ch ) ;
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
proprioDe [ ch . section ] = ch . proprietaire | | ' tenant ' ;
2026-06-30 10:07:17 -04:00
} ) ;
let html = ' ' ;
sections . forEach ( sec = > {
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
/ / Le panneau melangeait deux proprietaires sans le dire . Un tenant et son
/ / hebergeur n ' ont ni le meme depot, ni le meme cycle de vie : editer une
/ / section « hébergeur » touche TOUS les tenants du cluster .
const heb = proprioDe [ sec ] == = ' hebergeur ' ;
const chip = heb
? ' <span class= " intr-proprio heb " title= " Propriété de l \' HÉBERGEUR — écrit hors du dépôt du tenant ; la modification vaut pour tous ses tenants " >hébergeur</span> '
: ' <span class= " intr-proprio ten " title= " Propriété du TENANT — écrit dans son dépôt ; n \' affecte que lui " >tenant</span> ' ;
html + = ' <div class= " intr-section " ><div class= " intr-tete " > ' + echapper ( sec ) + chip + ' </div><div class= " intr-grille " > ' ;
2026-06-30 10:07:17 -04:00
parSection [ sec ] . forEach ( ch = > {
const v = valeurs [ ch . variable ] ;
2026-06-30 12:36:51 -04:00
let badge ;
if ( ch . classe == = ' constante ' ) badge = ' <span class= " badge-classe const " title= " Valeur unique, non surchargeable " >Constante</span> ' ;
else if ( ch . classe == = ' catalogue ' ) badge = ' <span class= " badge-classe cat " title= " Liste de valeurs autorisées, proposée dans les menus déroulants " >Catalogue</span> ' ;
else badge = ' <span class= " badge-classe def " title= " Valeur par défaut, surchargeable dans les instances " >Défaut</span> ' ;
2026-06-30 10:07:17 -04:00
const lib = echapper ( ch . libelle ) + badge ;
if ( ch . type == = ' bool ' ) {
const coche = ( v == = true | | v == = ' true ' | | v == = ' True ' ) ? ' checked ' : ' ' ;
html + = ' <label class= " champ " ><span> ' + lib + ' </span> '
+ ' <input type= " checkbox " data-intrant= " ' + echapper ( ch . variable ) + ' " data-type= " bool " ' + coche + ' ></label> ' ;
2026-06-30 12:36:51 -04:00
} else if ( ch . type == = ' liste ' ) {
const txt = Array . isArray ( v ) ? v . join ( ' , ' ) : ( v == null ? ' ' : v ) ;
html + = ' <label class= " champ " style= " grid-column:1/-1 " ><span> ' + lib + ' <span class= " unite " >· séparés par des virgules</span></span> '
+ ' <input type= " text " data-intrant= " ' + echapper ( ch . variable ) + ' " data-type= " liste " '
+ ' placeholder= " ex. asgard, pve-02, pve-03 " value= " ' + echapper ( txt ) + ' " ></label> ' ;
2026-06-30 10:07:17 -04:00
} else {
const t = ch . type == = ' int ' ? ' number ' : ' text ' ;
html + = ' <label class= " champ " ><span> ' + lib + ' </span> '
+ ' <input type= " ' + t + ' " data-intrant= " ' + echapper ( ch . variable ) + ' " data-type= " ' + echapper ( ch . type ) + ' " '
+ ' value= " ' + echapper ( v == null ? ' ' : v ) + ' " ></label> ' ;
}
} ) ;
html + = ' </div></div> ' ;
} ) ;
/ / Nomenclature en lecture seule ( lue ailleurs : vue Nomenclature )
const n = nomenclature | | { } ;
if ( Object . keys ( n ) . length ) {
html + = ' <div class= " intr-section " ><div class= " intr-tete " >Nomenclature (lecture seule)</div> '
+ ' <div class= " intr-ro " > ' ;
if ( n . supernet ) html + = ' Supernet : <b> ' + echapper ( n . supernet ) + ' </b><br> ' ;
if ( n . domaine | | n . domaine_interne ) html + = ' Domaine : <b> ' + echapper ( n . domaine | | n . domaine_interne ) + ' </b><br> ' ;
const nbCat = n . categories ? Object . keys ( n . categories ) . length : 0 ;
const nbFon = n . fonctions ? Object . keys ( n . fonctions ) . length : 0 ;
html + = nbCat + ' catégorie(s), ' + nbFon + ' fonction(s) déclarée(s). ' ;
html + = ' <br><span style= " font-style:italic " >Modifiable dans le plan (nomenclature.yml), pas ici.</span> ' ;
html + = ' </div></div> ' ;
}
/ / Secrets attendus : informatif , jamais saisis ici
const secrets = intrants . secrets_attendus | | [ ] ;
if ( secrets . length ) {
html + = ' <div class= " intr-section " ><div class= " intr-tete " >Secrets attendus (Vault — CLI)</div> '
+ ' <div class= " intr-ro " >Ces clés ne sont jamais saisies ni stockées ici. Édite-les via <b>ansible-vault</b>. '
+ ' <div class= " intr-secrets " > '
+ secrets . map ( s = > ' <code> ' + echapper ( s ) + ' </code> ' ) . join ( ' ' )
+ ' </div></div></div> ' ;
}
const f = intrants . fichiers | | { } ;
const chemins = Object . values ( f ) . map ( x = > echapper ( x ) ) . join ( ' , ' ) ;
if ( chemins ) html + = ' <div class= " intr-ro " style= " margin-top:2px " >Écrit dans : <b> ' + chemins + ' </b></div> ' ;
corps . innerHTML = html ;
}
async function enregistrerIntrants ( ) {
const valeurs = { } ;
document . querySelectorAll ( ' #intrants-corps [data-intrant] ' ) . forEach ( el = > {
const k = el . getAttribute ( ' data-intrant ' ) ;
valeurs [ k ] = el . type == = ' checkbox ' ? el . checked : el . value ;
} ) ;
const ancienDomaine = ( intrants . valeurs | | { } ) . domaine_interne ;
const nouveauDomaine = valeurs . domaine_interne ;
if ( nouveauDomaine != = undefined & & nouveauDomaine != = ancienDomaine ) {
if ( ! confirm ( ' Changer le domaine interne (« ' + ( ancienDomaine | | ' ∅ ' ) + ' » → « ' + ( nouveauDomaine | | ' ∅ ' )
+ ' ») impacte tout l \' écosystème (DNS, certificats, noms d \' hôtes). Continuer ? ' ) ) return ;
}
try {
const reponse = await fetch ( ' /api/intrants ' , {
method : ' POST ' ,
headers : { ' Content-Type ' : ' application/json ' , ' X-Jeton ' : JETON } ,
body : JSON . stringify ( { valeurs } ) ,
} ) ;
if ( ! reponse . ok ) {
let detail = ' ' ;
try { detail = ( await reponse . json ( ) ) . erreur | | ' ' ; } catch ( e ) { detail = await reponse . text ( ) ; }
message ( ' Échec de l \' enregistrement des intrants : ' + detail , ' erreur ' ) ;
return ;
}
const data = await reponse . json ( ) ;
intrants = data . intrants | | intrants ;
nomenclature = data . nomenclature | | nomenclature ;
fermerIntrants ( ) ;
message ( ' Intrants de base enregistrés. ' , ' ok ' ) ;
} catch ( e ) {
message ( ' Erreur réseau : ' + e , ' erreur ' ) ;
}
}
2026-06-24 20:17:46 -04:00
async function lancer ( mode , index ) {
const hote = hotes [ index ] ? hotes [ index ] . nom : ' ' ;
2026-06-30 12:19:26 -04:00
if ( ! hote ) { message ( ' Hôte introuvable. ' , ' erreur ' ) ; return ; }
2026-06-24 20:17:46 -04:00
if ( occupe ) return ;
2026-09-16 14:57:39 -04:00
/ / Un seul entonnoir pour les quatre gestes : la coupure se dit ici , et elle nomme
/ / sa raison . « Interdit » sans raison envoie chercher une panne qui n ' existe pas.
const pouvoirMode = POUVOIR_PAR_MODE [ mode ] ;
if ( pouvoirMode & & ! peut ( pouvoirMode ) ) {
message ( pourquoiPas ( pouvoirMode ) , ' erreur ' , { duree : 9000 } ) ;
return ;
}
2026-06-24 20:17:46 -04:00
let vault = ' ' ;
2026-07-04 14:40:02 -04:00
if ( ( mode == = ' deployer ' | | mode == = ' pousser ' ) & & ! verifie [ hote ] ) { message ( ' Vérifiez (dry-run) d \' abord. ' , ' erreur ' ) ; return ; }
2026-07-01 21:18:10 -04:00
if ( mode == = ' creer ' | | mode == = ' deployer ' | | mode == = ' pousser ' | | mode == = ' verifier ' ) {
2026-07-01 15:58:32 -04:00
if ( estProduction & & ( mode == = ' deployer ' | | mode == = ' pousser ' ) ) {
const saisi = prompt ( ' ⚠️ PRODUCTION — action réelle sur « ' + hote + ' ». \n '
2026-06-30 16:20:18 -04:00
+ ' Tape le nom exact de l \' hôte pour confirmer : ' ) ;
if ( saisi == = null ) return ;
if ( saisi . trim ( ) != = hote ) { message ( ' Confirmation annulée : le nom ne concorde pas. ' , ' erreur ' ) ; return ; }
}
2026-07-01 15:58:32 -04:00
const av = estProduction ? ' ⚠️ PRODUCTION. ' : ' ' ;
const txt = mode == = ' creer '
? ' Clone la VM « ' + hote + ' » depuis le golden template sur Proxmox (création réelle). '
: mode == = ' pousser '
? av + ' Déploie l \' hôte « ' + hote + ' » (applique ses playbooks, en root). Non transactionnel. '
2026-07-01 21:18:10 -04:00
: mode == = ' verifier '
? ' Dry-run (--check), AUCUNE modification. Mot de passe vault pour déchiffrer les secrets — laisse VIDE si cet hôte n \' a pas de secrets. '
2026-07-01 15:58:32 -04:00
: av + ' Applique les playbooks de groupes sur la VM réelle, en root (sudo). Non transactionnel. ' ;
2026-07-04 14:40:02 -04:00
const titre = ( mode == = ' creer ' ? ' Créer la VM — ' : mode == = ' pousser ' ? ' Déployer — ' : mode == = ' verifier ' ? ' Vérifier — ' : ' Déployer — ' ) + hote ;
2026-07-01 15:58:32 -04:00
vault = await ouvrirModale ( titre , txt ) ;
2026-06-24 20:17:46 -04:00
if ( vault == = null ) return ;
motVault = vault ;
}
occupe = true ; majBoutonsDeploiement ( ) ;
2026-07-04 14:40:02 -04:00
const titreConsole = { verifier : ' Vérification (dry-run) ' , deployer : ' Déploiement ' , creer : ' Création VM (clone) ' , pousser : ' Déploiement ' } [ mode ] | | mode ;
2026-07-01 15:58:32 -04:00
ouvrirConsole ( titreConsole , hote ) ;
2026-06-24 20:17:46 -04:00
try {
const rep = await fetch ( ' /api/ ' + mode , {
method : ' POST ' , headers : { ' Content-Type ' : ' application/json ' , ' X-Jeton ' : JETON } ,
body : JSON . stringify ( vault ? { hote , vault } : { hote } )
} ) ;
if ( ! rep . ok | | ! rep . body ) {
const d = await rep . json ( ) . catch ( ( ) = > ( { } ) ) ;
ecrireConsole ( ( d . erreur | | ' Refusé. ' ) + ' \n ' ) ; finirConsole ( 1 ) ;
occupe = false ; majBoutonsDeploiement ( ) ; return ;
}
const lecteur = rep . body . getReader ( ) ;
const dec = new TextDecoder ( ) ;
2026-07-07 13:31:02 -04:00
let rc = null ; let brut = ' ' ;
2026-06-24 20:17:46 -04:00
while ( true ) {
const { value , done } = await lecteur . read ( ) ;
if ( done ) break ;
let texte = dec . decode ( value , { stream : true } ) ;
2026-07-07 13:31:02 -04:00
brut + = texte ;
2026-06-24 20:17:46 -04:00
const m = texte . match ( / __FIN__ rc = ( - ? \d + ) / ) ;
2026-07-07 13:31:02 -04:00
if ( m ) { rc = parseInt ( m [ 1 ] , 10 ) ; }
texte = texte . replace ( / \n ? __RESUME__ . * \n ? / , ' ' ) . replace ( / \n ? __FIN__ rc = - ? \d + \n ? / , ' ' ) ;
2026-06-24 20:17:46 -04:00
if ( texte ) ecrireConsole ( texte ) ;
}
if ( rc == = null ) rc = 1 ;
2026-07-07 13:31:02 -04:00
const mr = brut . match ( / __RESUME__ ( . + ) / ) ;
if ( rc != = 0 & & mr ) { try { afficherResumeErreur ( JSON . parse ( mr [ 1 ] ) ) ; } catch ( e ) { } }
2026-06-24 20:17:46 -04:00
finirConsole ( rc ) ;
if ( mode == = ' verifier ' & & rc == = 0 ) { verifie [ hote ] = true ; message ( ' Vérification réussie — déploiement débloqué pour ' + hote + ' . ' , ' ok ' ) ; }
2026-07-01 17:52:39 -04:00
else if ( mode == = ' creer ' ) { message ( rc == = 0 ? ' VM « ' + hote + ' » créée — hôte marqué actif. ' : ' Échec de la création. ' , rc == = 0 ? ' ok ' : ' erreur ' ) ; if ( rc == = 0 ) appliquerPlan ( ) ; else sonder ( ) ; }
2026-07-04 14:40:02 -04:00
else if ( mode == = ' deployer ' | | mode == = ' pousser ' ) { verifie [ hote ] = false ; message ( rc == = 0 ? ' Déploiement terminé — hôte marqué actif. ' : ' Déploiement en échec. ' , rc == = 0 ? ' ok ' : ' erreur ' ) ; if ( rc == = 0 ) appliquerPlan ( ) ; else sonder ( ) ; }
2026-06-24 20:17:46 -04:00
} catch ( e ) {
ecrireConsole ( ' Erreur client : ' + e . message + ' \n ' ) ; finirConsole ( 1 ) ;
}
occupe = false ; majBoutonsDeploiement ( ) ;
}
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
/ / ═ ═ ═ LES ASSISTANTS — un runbook , son ordre , et pourquoi cet ordre - la ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═
/ /
/ / CE QUE CETTE VUE AJOUTE . Les autres vues editent le PLAN ; celle - ci conduit des
/ / GESTES . Le Makefile porte 132 cibles qui disent chacune ce qu ' elle fait, et aucune
/ / ne dit dans quel ordre . Cette connaissance vivait en prose dans les documents :
/ / l ' exploitant devait la lire ailleurs, puis revenir cliquer ici.
/ /
/ / RIEN N ' EST ECRIT EN DUR ICI. La liste, les libelles, les natures et les variables
/ / viennent de ` / api / runbooks ` , qui joint ` docs / runbooks - construction . yml ` au Makefile
/ / au moment de servir . Une cible renommee se voit donc ici , elle ne s ' invente pas.
let runbooks = [ ] , rbSel = null , rbEcarts = [ ] , rbEtats = { } ;
let etapeResolveur = null ;
async function chargerRunbooks ( ) {
try {
const r = await fetch ( ' /api/runbooks ' , { headers : { ' X-Jeton ' : JETON } } ) ;
const d = await r . json ( ) ;
runbooks = d . runbooks | | [ ] ;
rbEcarts = d . ecarts | | [ ] ;
if ( d . erreur ) message ( ' Registre des runbooks : ' + d . erreur , ' erreur ' , { duree : 9000 } ) ;
if ( ! rbSel & & runbooks . length ) rbSel = ( runbooks . find ( x = > x . conduisible ) | | runbooks [ 0 ] ) . id ;
} catch ( e ) { message ( ' Assistants indisponibles : ' + e . message , ' erreur ' ) ; }
}
function rbCourant ( ) { return runbooks . find ( x = > x . id == = rbSel ) | | null ; }
function rbEtat ( id , i ) { return ( rbEtats [ id ] | | { } ) [ i ] | | null ; }
/ / LA SEQUENCE EST UN GARDE - FOU , PAS UNE DECORATION . Une etape qui ECRIT suppose que la
/ / precedente a reussi — sinon on batit sur un terrain qu ' on n ' a pas verifie . Une
/ / etape qui MESURE reste toujours offerte : mesurer apres un echec est exactement ce
/ / qu ' on fait pour comprendre. Les etapes facultatives ne bloquent personne.
function etapeBloquee ( rb , i ) {
assistants : la portee se pese a l'etape, pas a la sequence
Mesure sur la console de TechnoLibre, portee tenant : 6 runbooks conduisibles sur 17, et
parmi les onze fermes, locataire-deployer et machine-une — c'est-a-dire le travail
quotidien d'un locataire. La cause : flotte-creer et creer-vm engendrent des VM et
exigent la fabric, et une portee declaree pour toute la sequence faisait basculer avec eux
des etapes voisines qui ne demandent que ce que le locataire possede deja.
Le runbook ne donne plus que le defaut ; l'etape qui exige davantage le declare. Le
locataire conduit sa sequence et bute precisement la ou il faut : sur la machine a
engendrer, pas sur le deploiement qui suit. La page ferme l'etape seule avec sa raison, et
la garde de la route lit la portee de l'etape visee par son index.
Un droit calcule sur l'ensemble se trompe toujours dans le meme sens : il refuse a
quelqu'un ce qu'il a le droit de faire, et le refus parait fonde puisqu'il nomme un vrai
manque. Il a fallu une console de locataire reelle pour le voir — sur le poste, qui porte
les deux liens, les dix-sept sequences s'affichaient conduisibles.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec (4 tests neufs, dont un qui
nomme le cas exact), P83 verte. P02 reste en echec pour la raison anterieure deja consignee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 19:55:02 -04:00
/ / Hors de portee de CETTE console : l ' etape se lit, elle ne se lance pas.
if ( rb . etapes [ i ] . conduisible == = false ) return true ;
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
if ( rb . etapes [ i ] . nature == = ' mesure ' ) return false ;
for ( let j = i - 1 ; j > = 0 ; j - - ) {
if ( rb . etapes [ j ] . facultative ) continue ;
return rbEtat ( rb . id , j ) != = ' ok ' ;
}
return false ;
}
function dessinerAssistants ( ) {
const g = document . getElementById ( ' grilles ' ) ;
if ( ! runbooks . length ) {
g . innerHTML = ' <div class= " hint-dep " >Chargement des assistants…</div> ' ;
chargerRunbooks ( ) . then ( ( ) = > {
2026-10-11 14:43:01 -04:00
dessinerNavigation ( ) ;
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
if ( vuePrincipale == = ' assistants ' ) { dessinerAssistants ( ) ; dessinerDetail ( ) ; }
} ) ;
return ;
}
2026-10-11 14:43:01 -04:00
/ / CE QUE CETTE CONSOLE CONDUIT D ' ABORD, LE RESTE REPLIE (2026-10-11). Les dix-sept
/ / runbooks s ' affichaient pele-mele, les hors-portee simplement plus pales. Ils restent
/ / lisibles — savoir chez qui un geste se fait fait partie du metier — mais ne
/ / disputent plus la place a ceux qu ' on peut lancer.
if ( ! rbSel | | ! runbooks . some ( x = > x . id == = rbSel ) ) rbSel = ( runbooks . find ( x = > x . conduisible ) | | runbooks [ 0 ] ) . id ;
const puce = rb = > {
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
const n = rb . etapes . length ;
return ' <button type= " button " class= " rb-puce ' + ( rb . id == = rbSel ? ' on ' : ' ' )
+ ( rb . conduisible ? ' ' : ' hors ' ) + ' " onclick= " choisirRunbook( \' ' + rb . id + ' \' ) " '
+ ' title= " ' + echapper ( rb . conduisible ? rb . but : rb . raison ) + ' " > '
+ echapper ( rb . titre ) + ' <span class= " chip " > ' + n + ' </span></button> ' ;
2026-10-11 14:43:01 -04:00
} ;
const ici = runbooks . filter ( x = > x . conduisible ) , ailleurs = runbooks . filter ( x = > ! x . conduisible ) ;
const puces = ici . map ( puce ) . join ( ' ' ) ;
const autres = ailleurs . length
? ' <details class= " rb-autres " ' + ( ailleurs . some ( x = > x . id == = rbSel ) ? ' open ' : ' ' ) + ' ><summary> '
+ ailleurs . length + ' assistant(s) relèvent d \' une autre console — à lire, pas à lancer ici</summary> '
+ ' <div class= " rb-liste " > ' + ailleurs . map ( puce ) . join ( ' ' ) + ' </div></details> '
: ' ' ;
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
const rb = rbCourant ( ) ;
let corps = ' ' ;
if ( rb ) {
2026-10-11 14:43:01 -04:00
const faites = rb . etapes . filter ( ( e , i ) = > rbEtat ( rb . id , i ) == = ' ok ' ) . length ;
const ratees = rb . etapes . filter ( ( e , i ) = > rbEtat ( rb . id , i ) == = ' echec ' ) . length ;
const total = rb . etapes . length | | 1 ;
corps + = ' <div class= " rb-entete " ><h2 class= " vue-titre " > ' + echapper ( rb . titre ) + ' </h2> '
+ ' <div class= " vue-sous " > ' + echapper ( rb . but ) + ' </div> '
+ ' <div class= " rb-progres " ><div class= " rb-barre " ><span class= " ok " style= " width: ' + ( 100 * faites / total ) + ' % " ></span> '
+ ' <span class= " ko " style= " width: ' + ( 100 * ratees / total ) + ' % " ></span></div> '
+ faites + ' réussie(s) · ' + ratees + ' en échec · ' + ( rb . etapes . length - faites - ratees ) + ' à faire</div></div> ' ;
corps + = rb . etapes . map ( ( e , i ) = > {
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
const etat = rbEtat ( rb . id , i ) ;
assistants : la portee se pese a l'etape, pas a la sequence
Mesure sur la console de TechnoLibre, portee tenant : 6 runbooks conduisibles sur 17, et
parmi les onze fermes, locataire-deployer et machine-une — c'est-a-dire le travail
quotidien d'un locataire. La cause : flotte-creer et creer-vm engendrent des VM et
exigent la fabric, et une portee declaree pour toute la sequence faisait basculer avec eux
des etapes voisines qui ne demandent que ce que le locataire possede deja.
Le runbook ne donne plus que le defaut ; l'etape qui exige davantage le declare. Le
locataire conduit sa sequence et bute precisement la ou il faut : sur la machine a
engendrer, pas sur le deploiement qui suit. La page ferme l'etape seule avec sa raison, et
la garde de la route lit la portee de l'etape visee par son index.
Un droit calcule sur l'ensemble se trompe toujours dans le meme sens : il refuse a
quelqu'un ce qu'il a le droit de faire, et le refus parait fonde puisqu'il nomme un vrai
manque. Il a fallu une console de locataire reelle pour le voir — sur le poste, qui porte
les deux liens, les dix-sept sequences s'affichaient conduisibles.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec (4 tests neufs, dont un qui
nomme le cas exact), P83 verte. P02 reste en echec pour la raison anterieure deja consignee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 19:55:02 -04:00
const bloquee = etapeBloquee ( rb , i ) ;
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
const cls = etat == = ' ok ' ? ' faite ' : etat == = ' echec ' ? ' ratee ' : bloquee ? ' bloquee ' : ' ' ;
const vars = ( e . variables | | [ ] ) . map ( v = > v . nom + ' =… ' ) . join ( ' ' ) ;
const fixes = Object . entries ( e . fixes | | { } ) . map ( ( [ k , v ] ) = > k + ' = ' + v ) . join ( ' ' ) ;
const meta = [ e . duree ? ' durée observée ' + e . duree : ' ' ,
e . facultative ? ' facultative ' : ' ' ] . filter ( Boolean ) . join ( ' · ' ) ;
return ' <div class= " rb-etape ' + cls + ' " > '
2026-10-11 14:43:01 -04:00
+ ' <div class= " rb-num " > ' + ( etat == = ' ok ' ? ' ✓ ' : etat == = ' echec ' ? ' ✗ ' : ( i + 1 ) ) + ' </div> '
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
+ ' <div><div><span class= " rb-cible " >make ' + echapper ( e . cible )
+ ( vars ? ' ' + echapper ( vars ) : ' ' ) + ( fixes ? ' ' + echapper ( fixes ) : ' ' )
+ ' </span><span class= " rb-nature ' + e . nature + ' " > ' + e . nature + ' </span></div> '
+ ' <div class= " rb-libelle " > ' + echapper ( e . libelle | | ' (aucun libellé au Makefile) ' ) + ' </div> '
+ ' <div class= " rb-pourquoi " > ' + echapper ( e . pourquoi ) + ' </div> '
+ ( meta ? ' <div class= " rb-meta " > ' + echapper ( meta ) + ' </div> ' : ' ' )
+ ( etat == = ' ok ' ? ' <div class= " rb-meta " >✔ réussie dans cette session</div> ' : ' ' )
+ ( etat == = ' echec ' ? ' <div class= " rb-meta " >✘ échouée — la suite reste bloquée</div> ' : ' ' )
assistants : la portee se pese a l'etape, pas a la sequence
Mesure sur la console de TechnoLibre, portee tenant : 6 runbooks conduisibles sur 17, et
parmi les onze fermes, locataire-deployer et machine-une — c'est-a-dire le travail
quotidien d'un locataire. La cause : flotte-creer et creer-vm engendrent des VM et
exigent la fabric, et une portee declaree pour toute la sequence faisait basculer avec eux
des etapes voisines qui ne demandent que ce que le locataire possede deja.
Le runbook ne donne plus que le defaut ; l'etape qui exige davantage le declare. Le
locataire conduit sa sequence et bute precisement la ou il faut : sur la machine a
engendrer, pas sur le deploiement qui suit. La page ferme l'etape seule avec sa raison, et
la garde de la route lit la portee de l'etape visee par son index.
Un droit calcule sur l'ensemble se trompe toujours dans le meme sens : il refuse a
quelqu'un ce qu'il a le droit de faire, et le refus parait fonde puisqu'il nomme un vrai
manque. Il a fallu une console de locataire reelle pour le voir — sur le poste, qui porte
les deux liens, les dix-sept sequences s'affichaient conduisibles.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec (4 tests neufs, dont un qui
nomme le cas exact), P83 verte. P02 reste en echec pour la raison anterieure deja consignee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 19:55:02 -04:00
+ ( e . conduisible == = false ? ' <div class= " rb-meta " >⊘ hors de portée de cette console — '
+ echapper ( e . raison | | ' ' ) + ' </div> ' : ' ' )
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
+ ' </div> '
2026-10-11 14:43:01 -04:00
+ ' <div><button type= " button " class= " ' + ( e . nature == = ' destructif ' ? ' destructif ' : e . nature == = ' ecriture ' ? ' primaire ' : ' ' )
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
+ ' " ' + ( bloquee ? ' disabled ' : ' ' ) + ' onclick= " lancerEtape( ' + i + ' ) " > '
+ ( e . nature == = ' mesure ' ? ' Mesurer ' : e . nature == = ' destructif ' ? ' Détruire… ' : ' Lancer ' )
+ ' </button></div></div> ' ;
} ) . join ( ' ' ) ;
}
const alerte = rbEcarts . length
? ' <div class= " hint-dep " style= " border-color:#8a3b3b " >Le registre des runbooks porte '
+ rbEcarts . length + ' écart(s) avec le Makefile : <code>python3 scripts/runbooks.py verifier</code></div> '
: ' ' ;
2026-10-11 14:43:01 -04:00
g . innerHTML = alerte + ' <div class= " rb-liste " > ' + puces + ' </div> ' + autres + corps ;
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
}
function choisirRunbook ( id ) { rbSel = id ; dessinerAssistants ( ) ; dessinerDetail ( ) ; }
function inspecteurRunbook ( ) {
const d = document . getElementById ( ' detail ' ) ;
const rb = rbCourant ( ) ;
if ( ! rb ) { d . innerHTML = ' <div class= " detail " ><div class= " hint-dep " >Aucun assistant.</div></div> ' ; return ; }
const compte = rb . etapes . filter ( ( e , i ) = > rbEtat ( rb . id , i ) == = ' ok ' ) . length ;
2026-10-11 14:43:01 -04:00
/ / Le titre et le but sont deja en tete de la vue : ce panneau dit le reste — la
/ / portee , ce qui est fait , ce qui se lit ailleurs , et d ' ou viennent les libelles.
d . innerHTML = ' <div class= " detail " ><div class= " section-fiche " style= " padding-bottom:16px " ><div class= " section-tete " >Cet assistant</div> '
+ ' <div class= " hint-dep " >Portée : <code> ' + echapper ( rb . portee )
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
+ ' </code> · ' + compte + ' / ' + rb . etapes . length + ' étape(s) réussie(s) dans cette session.</div> '
assistants : la portee se pese a l'etape, pas a la sequence
Mesure sur la console de TechnoLibre, portee tenant : 6 runbooks conduisibles sur 17, et
parmi les onze fermes, locataire-deployer et machine-une — c'est-a-dire le travail
quotidien d'un locataire. La cause : flotte-creer et creer-vm engendrent des VM et
exigent la fabric, et une portee declaree pour toute la sequence faisait basculer avec eux
des etapes voisines qui ne demandent que ce que le locataire possede deja.
Le runbook ne donne plus que le defaut ; l'etape qui exige davantage le declare. Le
locataire conduit sa sequence et bute precisement la ou il faut : sur la machine a
engendrer, pas sur le deploiement qui suit. La page ferme l'etape seule avec sa raison, et
la garde de la route lit la portee de l'etape visee par son index.
Un droit calcule sur l'ensemble se trompe toujours dans le meme sens : il refuse a
quelqu'un ce qu'il a le droit de faire, et le refus parait fonde puisqu'il nomme un vrai
manque. Il a fallu une console de locataire reelle pour le voir — sur le poste, qui porte
les deux liens, les dix-sept sequences s'affichaient conduisibles.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec (4 tests neufs, dont un qui
nomme le cas exact), P83 verte. P02 reste en echec pour la raison anterieure deja consignee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 19:55:02 -04:00
+ ( function ( ) {
const fermees = rb . etapes . filter ( e = > e . conduisible == = false ) ;
if ( ! fermees . length ) return ' ' ;
return ' <div class= " hint-dep " style= " margin-top:10px;border-color:#8a3b3b " > '
+ fermees . length + ' étape(s) sur ' + rb . etapes . length + ' hors de portée ici : '
+ echapper ( fermees . map ( e = > e . cible ) . join ( ' , ' ) ) + ' . ' + echapper ( fermees [ 0 ] . raison | | ' ' )
+ ' </div> ' ;
} ) ( )
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
+ ( rb . doc ? ' <div class= " hint-dep " style= " margin-top:10px " >Le récit complet : <code> '
+ echapper ( rb . doc ) + ' </code></div> ' : ' ' )
+ ' <div class= " hint-dep " style= " margin-top:10px " >Les libellés viennent du <code>Makefile</code>, '
+ ' lus à chaque affichage ; l \' ordre et le « pourquoi » viennent de '
+ ' <code>docs/runbooks-construction.yml</code>. Une étape d \' écriture attend que la '
2026-10-11 14:43:01 -04:00
+ ' précédente ait réussi ; une mesure reste toujours offerte.</div></div></div> ' ;
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
}
/ / La modale d ' etape : les champs que le REGISTRE declare, et rien d ' autre .
function ouvrirModaleEtape ( rb , e , i ) {
document . getElementById ( ' etape-titre ' ) . textContent = ( i + 1 ) + ' . make ' + e . cible ;
const avert = e . nature == = ' destructif '
? ' ⚠️ GESTE DESTRUCTIF. ' : e . nature == = ' ecriture ' ? ' Ce geste écrit. ' : ' Aucune écriture. ' ;
document . getElementById ( ' etape-texte ' ) . textContent = avert + e . pourquoi ;
const champs = document . getElementById ( ' etape-champs ' ) ;
let html = ( e . variables | | [ ] ) . map ( v = > {
if ( v . valeurs ) {
return ' <label class= " champ " ><span> ' + echapper ( v . invite | | v . nom )
+ ( v . facultatif ? ' (facultatif) ' : ' ' ) + ' </span><select id= " var- ' + v . nom + ' " > '
+ ( v . facultatif ? ' <option value= " " ></option> ' : ' ' )
+ v . valeurs . map ( x = > ' <option> ' + echapper ( x ) + ' </option> ' ) . join ( ' ' ) + ' </select></label> ' ;
}
return ' <label class= " champ " ><span> ' + echapper ( v . invite | | v . nom )
+ ( v . facultatif ? ' (facultatif) ' : ' ' ) + ' </span><input id= " var- ' + v . nom
+ ' " autocomplete= " off " placeholder= " ' + echapper ( v . exemple | | ' ' ) + ' " ></label> ' ;
} ) . join ( ' ' ) ;
/ / LA CONFIRMATION ECRITE EST LE SEUL GARDE - FOU QUI RESISTE A UN CLIC DISTRAIT .
if ( e . nature == = ' destructif ' ) {
html + = ' <label class= " champ " ><span>Pour confirmer, écris <b>DETRUIRE</b></span> '
+ ' <input id= " var-__confirmation " autocomplete= " off " ></label> ' ;
}
champs . innerHTML = html ;
document . getElementById ( ' etape-vault ' ) . value = motVault | | ' ' ;
document . getElementById ( ' etape ' ) . hidden = false ;
return new Promise ( res = > { etapeResolveur = res ; } ) ;
}
function confirmerEtape ( ) {
const rb = rbCourant ( ) ; if ( ! rb ) return ;
const r = etapeResolveur ;
const boite = document . getElementById ( ' etape-champs ' ) ;
const valeurs = { } ;
let confirmation = null ;
boite . querySelectorAll ( ' input,select ' ) . forEach ( el = > {
const nom = el . id . replace ( / ^ var - / , ' ' ) ;
if ( nom == = ' __confirmation ' ) confirmation = el . value . trim ( ) ;
else valeurs [ nom ] = el . value . trim ( ) ;
} ) ;
document . getElementById ( ' etape ' ) . hidden = true ;
etapeResolveur = null ;
if ( r ) r ( { valeurs , vault : document . getElementById ( ' etape-vault ' ) . value , confirmation } ) ;
}
function annulerEtape ( ) {
document . getElementById ( ' etape ' ) . hidden = true ;
const r = etapeResolveur ; etapeResolveur = null ;
if ( r ) r ( null ) ;
}
async function lancerEtape ( i ) {
const rb = rbCourant ( ) ; if ( ! rb | | occupe ) return ;
const e = rb . etapes [ i ] ;
assistants : la portee se pese a l'etape, pas a la sequence
Mesure sur la console de TechnoLibre, portee tenant : 6 runbooks conduisibles sur 17, et
parmi les onze fermes, locataire-deployer et machine-une — c'est-a-dire le travail
quotidien d'un locataire. La cause : flotte-creer et creer-vm engendrent des VM et
exigent la fabric, et une portee declaree pour toute la sequence faisait basculer avec eux
des etapes voisines qui ne demandent que ce que le locataire possede deja.
Le runbook ne donne plus que le defaut ; l'etape qui exige davantage le declare. Le
locataire conduit sa sequence et bute precisement la ou il faut : sur la machine a
engendrer, pas sur le deploiement qui suit. La page ferme l'etape seule avec sa raison, et
la garde de la route lit la portee de l'etape visee par son index.
Un droit calcule sur l'ensemble se trompe toujours dans le meme sens : il refuse a
quelqu'un ce qu'il a le droit de faire, et le refus parait fonde puisqu'il nomme un vrai
manque. Il a fallu une console de locataire reelle pour le voir — sur le poste, qui porte
les deux liens, les dix-sept sequences s'affichaient conduisibles.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec (4 tests neufs, dont un qui
nomme le cas exact), P83 verte. P02 reste en echec pour la raison anterieure deja consignee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 19:55:02 -04:00
if ( e . conduisible == = false ) { message ( e . raison | | rb . raison , ' erreur ' , { duree : 9000 } ) ; return ; }
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
if ( etapeBloquee ( rb , i ) ) { message ( ' L \' étape précédente n \' a pas réussi. ' , ' erreur ' ) ; return ; }
const rep0 = await ouvrirModaleEtape ( rb , e , i ) ;
if ( rep0 == = null ) return ;
if ( e . nature == = ' destructif ' & & rep0 . confirmation != = ' DETRUIRE ' ) {
message ( ' Confirmation annulée : le mot ne concorde pas. ' , ' erreur ' ) ; return ;
}
motVault = rep0 . vault ;
occupe = true ; majBoutonsDeploiement ( ) ;
ouvrirConsole ( rb . titre + ' — étape ' + ( i + 1 ) , e . cible ) ;
try {
const rep = await fetch ( ' /api/runbook-etape ' , {
method : ' POST ' , headers : { ' Content-Type ' : ' application/json ' , ' X-Jeton ' : JETON } ,
body : JSON . stringify ( { runbook : rb . id , cible : e . cible , index : i ,
valeurs : rep0 . valeurs , vault : rep0 . vault } )
} ) ;
if ( ! rep . ok | | ! rep . body ) {
const d = await rep . json ( ) . catch ( ( ) = > ( { } ) ) ;
ecrireConsole ( ( d . erreur | | ' Refusé. ' ) + ' \n ' ) ; finirConsole ( 1 ) ;
occupe = false ; majBoutonsDeploiement ( ) ; return ;
}
const lecteur = rep . body . getReader ( ) ;
const dec = new TextDecoder ( ) ;
let rc = null , brut = ' ' ;
while ( true ) {
const { value , done } = await lecteur . read ( ) ;
if ( done ) break ;
let texte = dec . decode ( value , { stream : true } ) ;
brut + = texte ;
const m = texte . match ( / __FIN__ rc = ( - ? \d + ) / ) ;
if ( m ) rc = parseInt ( m [ 1 ] , 10 ) ;
texte = texte . replace ( / \n ? __RESUME__ . * \n ? / , ' ' ) . replace ( / \n ? __FIN__ rc = - ? \d + \n ? / , ' ' ) ;
if ( texte ) ecrireConsole ( texte ) ;
}
if ( rc == = null ) rc = 1 ;
const mr = brut . match ( / __RESUME__ ( . + ) / ) ;
if ( rc != = 0 & & mr ) { try { afficherResumeErreur ( JSON . parse ( mr [ 1 ] ) ) ; } catch ( err ) { } }
finirConsole ( rc ) ;
rbEtats [ rb . id ] = rbEtats [ rb . id ] | | { } ;
rbEtats [ rb . id ] [ i ] = rc == = 0 ? ' ok ' : ' echec ' ;
message ( rc == = 0 ? ' Étape ' + ( i + 1 ) + ' réussie. ' : ' Étape ' + ( i + 1 ) + ' en échec. ' ,
rc == = 0 ? ' ok ' : ' erreur ' ) ;
} catch ( err ) {
ecrireConsole ( ' Erreur client : ' + err . message + ' \n ' ) ; finirConsole ( 1 ) ;
rbEtats [ rb . id ] = rbEtats [ rb . id ] | | { } ; rbEtats [ rb . id ] [ i ] = ' echec ' ;
}
occupe = false ; majBoutonsDeploiement ( ) ;
if ( vuePrincipale == = ' assistants ' ) { dessinerAssistants ( ) ; dessinerDetail ( ) ; }
}
2026-06-30 12:19:26 -04:00
function sauvegarderVue ( ) {
if ( occupe ) return ;
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur
Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants.
1. Intégrations universelles (D-33/D-34, P26)
Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à
quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être
oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01
n'étaient ni supervisés, ni journalisés, ni certifiés.
Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le
plan ne garde que les vrais choix et refuse la recopie. Les exemptions se
dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle
pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace.
Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la
voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations
universelles auraient cessé d'être exigés et P18 serait passé au vert sur une
voûte incomplète.
Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41
lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas
client_pki sur infra-pki-01.
2. Vue Intégrations : la matrice
La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas
été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x
intégrations : colonnes de politique en lecture seule, facultatives cochables sur
place, ligne de couverture n/N qui rend le motif visible sans le juger.
3. Propriété des intrants (D-35/D-36, P27)
Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière.
Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de
stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont
dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive —
l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et
ses défauts de placement.
Le panneau nomme désormais le propriétaire de chaque section : éditer une section
« hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas.
26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
if ( vuePrincipale == = ' serveurs ' | | vuePrincipale == = ' integrations ' ) sauvegarderServeurs ( ) ;
2026-06-30 12:19:26 -04:00
else if ( vuePrincipale == = ' applications ' ) sauvegarderApplications ( ) ;
else if ( vuePrincipale == = ' bases ' ) sauvegarderBases ( ) ;
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
else if ( vuePrincipale == = ' domaines ' ) sauvegarderDomaines ( ) ;
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
else if ( vuePrincipale == = ' nomenclature ' ) sauvegarderNomenclature ( ) ;
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
else message ( ' Vue en lecture seule (Flux / Couches) — édite dans Serveurs / Applications / Bases / Domaines. ' ) ;
2026-06-30 12:19:26 -04:00
}
2026-10-11 14:43:01 -04:00
/ / j / k SUIT L ' ORDRE AFFICHE — par categorie, puis par nom. Deux ordres pour une meme
/ / liste , c ' est un j qui saute une ligne qu ' on a sous les yeux .
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
function ordreServeursVisibles ( ) {
2026-10-11 14:43:01 -04:00
return serveursEnGroupes ( ) . flatMap ( g = > g . items . map ( ( [ , i ] ) = > i ) ) ;
2026-06-30 12:19:26 -04:00
}
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
function naviguerServeurs ( delta ) {
const ordre = ordreServeursVisibles ( ) ;
2026-06-30 12:19:26 -04:00
if ( ! ordre . length ) return ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
let pos = ordre . indexOf ( selServeur ) ;
2026-06-30 12:19:26 -04:00
pos = pos < 0 ? ( delta > 0 ? 0 : ordre . length - 1 )
: Math . min ( ordre . length - 1 , Math . max ( 0 , pos + delta ) ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
choisirServeur ( ordre [ pos ] ) ;
const c = document . getElementById ( ' tuile-srv- ' + ordre [ pos ] ) ;
2026-06-30 12:19:26 -04:00
if ( c ) c . scrollIntoView ( { block : ' nearest ' , behavior : ' smooth ' } ) ;
2026-06-24 20:17:46 -04:00
}
document . addEventListener ( ' keydown ' , e = > {
if ( e . key == = ' Escape ' & & modaleResolveur ) { annulerModale ( ) ; return ; }
2026-06-30 12:19:26 -04:00
if ( ( e . ctrlKey | | e . metaKey ) & & ( e . key == = ' s ' | | e . key == = ' S ' ) ) { e . preventDefault ( ) ; sauvegarderVue ( ) ; return ; }
const dansSaisie = / ^ ( INPUT | SELECT | TEXTAREA ) $ / . test ( ( document . activeElement | | { } ) . tagName | | ' ' ) ;
if ( dansSaisie | | e . ctrlKey | | e . metaKey | | e . altKey ) return ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
const vues = { ' 1 ' : ' serveurs ' , ' 2 ' : ' applications ' , ' 3 ' : ' bases ' } ;
2026-06-30 12:19:26 -04:00
if ( vues [ e . key ] ) { setVue ( vues [ e . key ] ) ; return ; }
if ( e . key == = ' / ' ) { e . preventDefault ( ) ; const f = document . getElementById ( ' filtre ' ) ; if ( f ) f . focus ( ) ; return ; }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
if ( vuePrincipale != = ' serveurs ' ) return ;
if ( e . key == = ' j ' ) { naviguerServeurs ( 1 ) ; return ; }
if ( e . key == = ' k ' ) { naviguerServeurs ( - 1 ) ; return ; }
if ( e . key == = ' v ' ) { const b = document . getElementById ( ' btn-verifier ' ) ; if ( b & & ! b . disabled ) b . click ( ) ; return ; }
if ( e . key == = ' d ' ) { const b = document . getElementById ( ' btn-deployer ' ) ; if ( b & & ! b . disabled ) b . click ( ) ; return ; }
2026-06-30 12:19:26 -04:00
} ) ;
/ / Validation inline des champs du plan ( vue Serveurs ) — liseré rouge avant l ' envoi.
document . addEventListener ( ' input ' , e = > {
if ( e . target & & e . target . matches & & e . target . matches ( ' [data-v] ' ) ) validerChampServeur ( e . target ) ;
} ) ;
/ / Garde - fou : prévenir la perte d ' éditions de plan non sauvegardées.
window . addEventListener ( ' beforeunload ' , e = > {
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
if ( serveursModifie | | applicationsModifie | | basesModifie | | domainesModifie ) { e . preventDefault ( ) ; e . returnValue = ' ' ; }
2026-06-24 20:17:46 -04:00
} ) ;
2026-06-30 10:07:17 -04:00
/ / - - - - - - Info - bulles d ' aide au survol de la description d ' un champ - - - - - -
( function ( ) {
const AIDES = {
' Adresse IP ' : " Adresse IPv4 de la VM sur son VLAN. Ex. 10.10.12.11. Souvent dérivée — clic « Proposer ». " ,
' CIDR ' : " Masque réseau en bits. 24 = /24 (255.255.255.0). " ,
' Passerelle ' : " Routeur de sortie du VLAN. En général le .1 du sous-réseau. " ,
' VLAN ' : " Identifiant 802.1Q (1– 4094). Dérivé de la fonction via la nomenclature. " ,
' Pont ' : " Pont réseau Proxmox. Ex. vmbr0. " ,
' DNS ' : " Résolveur DNS de la VM. En général le DNS interne de l ' écosystème. " ,
' Utilisateur Ansible ' : " Compte technique utilisé par Ansible. Par défaut : ansible. " ,
' VMID ' : " Identifiant unique Proxmox. Dérivé (catégorie + service + rang). " ,
' Nœud ' : " Hôte Proxmox cible. Ex. pve-01. " ,
' Stockage ' : " Pool de stockage Proxmox du disque. Ex. local-zfs. " ,
' Disque ' : " Taille du disque système. Ex. 32G. Dérivée des logiciels hébergés + socle. " ,
' Mémoire ' : " RAM en Mo. Dérivée des logiciels hébergés + socle (avec marge). " ,
' Cœurs ' : " vCPU alloués. Dérivés des logiciels hébergés (maximum, pas additif). " ,
' Nom ' : " Identifiant unique du serveur de base, sans espace. " ,
' Type ' : " Moteur de la base. Ex. postgres, redis. " ,
' Hôte ' : " VM qui héberge ce service (nom du serveur). " ,
' Hôte (VM) ' : " VM cible où l ' application est déployée. " ,
' Port ' : " Port d ' écoute du service. " ,
' Groupe Ansible ' : " Groupe/rôle Set-OPS appliqué. Ex. serveur_postgresql. " ,
' Groupe (rôle) ' : " Rôle Set-OPS appliqué à l ' application. " ,
' Identifiant ' : " Clé courte et unique, sans espace. " ,
' Portée ' : " À qui la base est offerte : groupe, hôte ou application. " ,
' Consommateur ' : " L ' entité (groupe / hôte / application) qui utilise la base. " ,
' Usage ' : " Étiquette libre. Ex. principale. " ,
' Serveur ' : " Serveur de base de données qui héberge cette base. " ,
' Base ' : " Nom de la base de données. " ,
' Propriétaire ' : " Rôle SQL propriétaire de la base. " ,
' Secret (Vault) ' : " Nom de la variable Ansible Vault contenant le mot de passe. " ,
' Requiert (applis) ' : " Applications prérequises, séparées par des virgules. " ,
' Expose (FQDN) ' : " Nom(s) public(s) exposé(s). Ex. forge.exemple.ca. " ,
' Serveur (fonction-NN) ' : " Nom = fonction + rang. Ex. web-frontal-03. " ,
} ;
const bulle = document . createElement ( ' div ' ) ;
bulle . id = ' infobulle ' ;
bulle . setAttribute ( ' role ' , ' tooltip ' ) ;
document . body . appendChild ( bulle ) ;
function libelleDe ( el ) {
const n = el . firstChild ;
return ( ( n & & n . nodeType == = 3 ? n . textContent : el . textContent ) | | ' ' ) . trim ( ) ;
}
function aideDe ( el ) {
if ( ! el | | ! el . matches | | ! el . matches ( ' .champ > span ' ) ) return null ;
return AIDES [ libelleDe ( el ) ] | | null ;
}
function placer ( el ) {
const r = el . getBoundingClientRect ( ) ;
bulle . style . left = Math . max ( 8 , Math . min ( r . left , innerWidth - bulle . offsetWidth - 8 ) ) + ' px ' ;
let haut = r . top - bulle . offsetHeight - 8 ;
if ( haut < 8 ) haut = r . bottom + 8 ; / / bascule sous le champ si pas la place
bulle . style . top = haut + ' px ' ;
}
document . addEventListener ( ' mouseover ' , e = > {
const aide = aideDe ( e . target ) ;
if ( ! aide ) return ;
bulle . textContent = aide ;
bulle . classList . add ( ' on ' ) ;
placer ( e . target ) ;
} ) ;
document . addEventListener ( ' mouseout ' , e = > {
if ( e . target & & e . target . matches & & e . target . matches ( ' .champ > span ' ) ) bulle . classList . remove ( ' on ' ) ;
} ) ;
/ / Affordance : souligné pointillé sur les descriptions qui ont une aide .
function marquer ( racine ) {
if ( ! racine . querySelectorAll ) return ;
racine . querySelectorAll ( ' .champ > span ' ) . forEach ( s = > {
if ( AIDES [ libelleDe ( s ) ] ) s . classList . add ( ' a-aide ' ) ;
} ) ;
}
new MutationObserver ( muts = > muts . forEach ( m = >
m . addedNodes . forEach ( n = > { if ( n . nodeType == = 1 ) marquer ( n ) ; } )
) ) . observe ( document . body , { childList : true , subtree : true } ) ;
marquer ( document ) ;
} ) ( ) ;
2026-06-24 20:17:46 -04:00
charger ( ) . catch ( err = > message ( err . message , ' erreur ' ) ) ;
< / script >
< / body >
< / html >
"""
2026-07-23 12:12:29 -04:00
def basculer_instance ( nom : str ) - > str :
""" Repointe le symlink `instance/` vers un depot frere. Renvoie la nouvelle cible.
Memes garde - fous que ` make instance - utiliser ` , plus une validation STRICTE : ` nom `
doit etre une instance DECOUVERTE ( dossier frere avec plan / nomenclature . yml ) , ce
qui interdit toute traversee de chemin ( pas de ' / ' , ' .. ' , cible arbitraire ) .
"""
import instances as _inst
connues = { i [ " nom " ] for i in _inst . decouvrir ( ) }
if nom not in connues :
raise ValueError ( f " Instance inconnue : { nom !r} (attendu : { ' , ' . join ( sorted ( connues ) ) or ' aucune ' } ). " )
cible = RACINE . parent / nom
if not cible . is_dir ( ) :
raise ValueError ( f " Dossier introuvable : ../ { nom } " )
lien = RACINE / " instance "
if lien . exists ( ) and not lien . is_symlink ( ) :
raise ValueError ( " ' instance ' existe et n ' est pas un symlink — refus. " )
if lien . is_symlink ( ) or lien . exists ( ) :
lien . unlink ( )
lien . symlink_to ( f " ../ { nom } " )
return os . readlink ( lien )
2026-06-24 20:17:46 -04:00
class Gestionnaire ( BaseHTTPRequestHandler ) :
2026-07-23 12:12:29 -04:00
# Inventaire de l'instance ACTIVE, resolu DYNAMIQUEMENT depuis le symlink
# `instance/` a chaque acces : ainsi la bascule d'instance (POST
# /api/instance-utiliser) prend effet sans redemarrer le serveur. Les chemins
# de plan (FICHIER_*) sont deja relatifs au symlink et suivent de meme.
# SETOPS_INVENTAIRE force un inventaire fixe (CI / usage explicite).
@property
def inventaire ( self ) - > Path :
force = os . environ . get ( " SETOPS_INVENTAIRE " )
return Path ( force ) if force else _inventaire ( INSTANCE , " principal " , " production " )
2026-06-24 20:17:46 -04:00
def log_message ( self , * args ) - > None : # silence par defaut
pass
def repondre ( self , status : int , contenu : bytes , content_type : str ) - > None :
try :
self . send_response ( status )
self . send_header ( " Content-Type " , content_type )
self . send_header ( " Content-Length " , str ( len ( contenu ) ) )
self . end_headers ( )
self . wfile . write ( contenu )
except ( BrokenPipeError , ConnectionResetError ) :
pass # le client a ferme la connexion : rien a faire
def repondre_json ( self , status : int , data : dict ) - > None :
self . repondre ( status , json . dumps ( data , ensure_ascii = False ) . encode ( " utf-8 " ) , " application/json; charset=utf-8 " )
def _ecrire_flux ( self , texte : str ) - > bool :
try :
self . wfile . write ( texte . encode ( " utf-8 " , " replace " ) )
self . wfile . flush ( )
return True
except ( BrokenPipeError , ConnectionResetError ) :
return False
def executer_flux ( self , hote : str , mode : str , vault : str | None = None ) - > None :
if not MOTIF_HOTE . match ( hote or " " ) :
self . repondre_json ( 400 , { " erreur " : " Nom d ' hote invalide. " } )
return
2026-07-01 15:58:32 -04:00
tous_hotes = { h . get ( " nom " ) for h in liste_hotes ( charger_yaml ( self . inventaire ) ) }
if hote not in tous_hotes :
self . repondre_json ( 409 , { " erreur " : f " { hote } est inconnu de l ' inventaire (applique le plan d ' abord). " } )
return
# 'creer' (clone) est permis meme sur un hote planifie ; les autres exigent actif.
if mode in ( " verifier " , " deployer " , " pousser " ) and hote not in hotes_actifs_fichier ( self . inventaire ) :
2026-06-24 20:17:46 -04:00
self . repondre_json ( 409 , { " erreur " : f " { hote } n ' est pas un hote actif sauvegarde. " } )
return
if mode == " deployer " and not VERIF_OK . get ( hote ) :
self . repondre_json ( 409 , { " erreur " : " Verification (dry-run) requise avant de deployer. " } )
return
if not VERROU . acquire ( blocking = False ) :
self . repondre_json ( 409 , { " erreur " : " Une execution est deja en cours. " } )
return
fichier_vault = None
2026-07-07 12:49:17 -04:00
journal = None
2026-06-24 20:17:46 -04:00
try :
2026-07-01 15:58:32 -04:00
cible = { " deployer " : " deployer " , " pousser " : " deployer " ,
" creer " : " creer-vm " } . get ( mode , " verifier-deploiement " )
2026-06-24 20:17:46 -04:00
env = dict ( os . environ , PYTHONUNBUFFERED = " 1 " , ANSIBLE_FORCE_COLOR = " 0 " )
if vault :
plan : l ecriture des registres devient atomique — tout, ou rien
`path.open("w")` TRONQUE avant d ecrire : entre les deux, le fichier est vide.
Une exception dans yaml.safe_dump, un disque plein, un Ctrl-C, et
instance/plan/serveurs.yml reste mutile.
L asymetrie fait la gravite : hosts.yml se regenere d un
make instancier-appliquer, le PLAN ne se regenere de rien. C est la source
unique de verite. Git est le filet, mais encore faut-il savoir qu on est tombe.
TREIZE SITES, UNE SEULE FONCTION
Le defaut n etait pas dans le GUI seul : douze sites dans sept fichiers, dont
les miroirs CLI des MEMES registres. Corriger le GUI seul aurait recree la
divergence que P41 garde depuis les neuf resolutions d instance. La fonction
vit donc dans inventory_rules.py, que les sept importaient deja. Une source,
pas douze.
TROIS DETAILS QUI FONT LA DIFFERENCE ENTRE « CA MARCHE » ET « CA TIENT »
temporaire dans le MEME dossier os.replace n est atomique qu au sein d un
meme systeme de fichiers ; un /tmp sur une
autre partition casserait la garantie sans
rien dire
fsync AVANT le rename sinon le renommage peut atteindre le disque
avant le contenu : au retour d une coupure
brutale, un fichier neuf et VIDE — le defaut
qu on ferme, deplace d un cran
report des droits mkstemp cree en 0600, le plan est en 0664 et
doit rester lisible par le groupe sur les
runners
LE TEST PORTE SON PROPRE CONTROLE NEGATIF
scripts/tests/test_ecriture_atomique.py rejoue D ABORD l ancienne forme et
verifie qu elle DETRUIT. Sans ce controle, « le fichier est intact » ne
prouverait rien — il pourrait l etre parce que rien n a ete ecrit du tout. Une
garantie qu on n a jamais vue echouer n est pas une garantie, c est une
habitude.
Branche sur P02, dont le titre annoncait « inventory_host » alors qu il lance
maintenant trois tests. Corrige au passage.
LA VOUTE DU GUI : VERIFIEE, PAS DE DEFAUT
Le soupcon etait qu executer_flux pose ANSIBLE_VAULT_PASSWORD_FILE (un seul mot
de passe) alors que creer une VM ouvre DEUX voutes depuis « une voute, une cle ».
Eprouve contre deux voutes JETABLES a mots de passe distincts — jamais les
vraies. Les deux variables se CUMULENT : Ansible essaie tous les secrets, et un
PASSWORD_FILE errone n empeche rien. Confirme en sondant l environnement qu une
recette make recoit reellement : le mot de passe saisi ET les cinq cles
calculees par voutes.py.
Ce qui sauve ce chemin n est donc pas le mot de passe saisi, c est
l IDENTITY_LIST que make pose par-dessus. Chacun couvre ce que l autre ne
couvre pas — le PASSWORD_FILE sert le runner qui n a que sa cle, l IDENTITY_LIST
le poste qui les a toutes. Ecrit au-dessus du code, pour que personne ne
« simplifie » en retirant l un des deux.
make prouver : CONFORME, 59 OK, 0 echec, 1 saute.
make instancier : DIFF VIDE, quatre registres relus, droits 664 preserves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 14:57:57 -04:00
# LE MOT DE PASSE SAISI S'AJOUTE AUX CLES, IL NE LES REMPLACE PAS.
#
# Verifie empiriquement le 2026-09-08, contre deux voutes jetables a mots
# de passe distincts : `ANSIBLE_VAULT_PASSWORD_FILE` et
# `ANSIBLE_VAULT_IDENTITY_LIST` se CUMULENT — Ansible essaie tous les
# secrets, et un PASSWORD_FILE errone n'empeche rien.
#
# Ca compte depuis « une voute, une cle » (2026-08-28) : creer une VM
# ouvre DEUX voutes dans la meme execution — celle du tenant et
# `underlay.vault.yml`. Un seul mot de passe ne pourrait pas les ouvrir
# toutes les deux. Ce qui sauve ce chemin, c'est que `make` calcule
# `ANSIBLE_VAULT_IDENTITY_LIST` par-dessus (Makefile, `?=`), et que les
# deux coexistent dans l'environnement de la recette.
#
# NE PAS « SIMPLIFIER » EN RETIRANT L'UN DES DEUX. Le PASSWORD_FILE sert
# le cas ou la cle n'est pas sur la machine (un runner qui n'a que la
# sienne) ; l'IDENTITY_LIST sert le poste de l'exploitant, qui les a
# toutes. Chacun couvre ce que l'autre ne couvre pas.
2026-06-24 20:17:46 -04:00
fd , fichier_vault = tempfile . mkstemp ( prefix = " setops-vault- " ) # 0600 par defaut
os . write ( fd , str ( vault ) . encode ( " utf-8 " ) )
os . close ( fd )
env [ " ANSIBLE_VAULT_PASSWORD_FILE " ] = fichier_vault
self . send_response ( 200 )
self . send_header ( " Content-Type " , " text/plain; charset=utf-8 " )
self . send_header ( " Cache-Control " , " no-cache " )
self . end_headers ( )
2026-07-07 12:49:17 -04:00
# Persistance : un journal horodaté par exécution, en plus du streaming live.
dossier_logs = self . inventaire . parents [ 2 ] / " logs "
dossier_logs . mkdir ( parents = True , exist_ok = True )
chemin_log = dossier_logs / f " { hote } - { cible } - { time . strftime ( ' % Y % m %d T % H % M % S ' ) } .log "
journal = open ( chemin_log , " w " , encoding = " utf-8 " )
entete = f " $ make { cible } HOTE= { hote } \n \n "
journal . write ( entete )
self . _ecrire_flux ( entete )
2026-07-07 13:31:02 -04:00
tache_courante = " "
echecs : list [ dict ] = [ ]
2026-06-24 20:17:46 -04:00
proc = subprocess . Popen (
[ " make " , cible , f " HOTE= { hote } " ] ,
cwd = str ( RACINE ) , env = env ,
stdin = subprocess . DEVNULL ,
stdout = subprocess . PIPE , stderr = subprocess . STDOUT , text = True , bufsize = 1 ,
)
try :
for ligne in proc . stdout :
2026-07-07 12:49:17 -04:00
journal . write ( ligne )
2026-07-07 13:31:02 -04:00
if ligne . startswith ( " TASK [ " ) :
deb , fin = ligne . find ( " [ " ) , ligne . find ( " ] " )
if - 1 < deb < fin :
tache_courante = ligne [ deb + 1 : fin ]
elif ( " FAILED! " in ligne or " UNREACHABLE! " in ligne ) and \
( " fatal: " in ligne or ligne . lstrip ( ) . startswith ( " failed: " ) ) :
echec = _extraire_echec ( ligne , tache_courante )
if echec :
echecs . append ( echec )
2026-06-24 20:17:46 -04:00
if not self . _ecrire_flux ( ligne ) :
2026-07-07 12:49:17 -04:00
# Navigateur déconnecté : on continue à journaliser jusqu'au bout.
for reste in proc . stdout :
journal . write ( reste )
2026-06-24 20:17:46 -04:00
break
finally :
rc = proc . wait ( )
if mode == " verifier " and rc == 0 :
VERIF_OK [ hote ] = True
2026-07-01 15:58:32 -04:00
elif mode in ( " deployer " , " pousser " ) :
2026-06-24 20:17:46 -04:00
VERIF_OK . pop ( hote , None )
2026-07-01 17:39:44 -04:00
# Auto-actif : un hote qu'on vient de materialiser (clone reussi) ou de
# deployer passe 'actif' dans le plan. Reflet fidele : materialise = actif.
if rc == 0 and mode in ( " creer " , " deployer " , " pousser " ) :
try :
marquer_serveur_actif ( hote )
except Exception :
pass
2026-07-07 13:31:02 -04:00
if rc != 0 and echecs :
self . _ecrire_flux ( " __RESUME__ " + json . dumps ( { " echecs " : echecs [ : 6 ] } , ensure_ascii = False ) + " \n " )
2026-07-07 12:49:17 -04:00
journal . write ( f " \n __FIN__ rc= { rc } \n " )
self . _ecrire_flux ( f " \n [journal : { os . path . relpath ( chemin_log , RACINE ) } ] \n __FIN__ rc= { rc } \n " )
2026-06-24 20:17:46 -04:00
finally :
2026-07-07 12:49:17 -04:00
if journal :
try :
journal . close ( )
except OSError :
pass
2026-06-24 20:17:46 -04:00
if fichier_vault :
try :
os . remove ( fichier_vault )
except OSError :
pass
VERROU . release ( )
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
def executer_runbook ( self , runbook : str , cible : str , valeurs : dict ,
vault : str | None = None , index : int | None = None ) - > None :
""" Une etape DECLAREE d ' un runbook — et rien d ' autre.
LE NAVIGATEUR NE NOMME PAS UNE COMMANDE , IL NOMME UNE PLACE . On ne lance pas
« la cible que la page demande » : on lance ce que le registre declare a cette
place - la , avec les seules variables qu ' il declare. Une page compromise, ou
simplement perimee , ne peut donc pas reclamer ` raser ` depuis un runbook de mesure .
C ' est la meme discipline que `POUVOIR_REQUIS` : ce qui protege n ' est pas un bouton
grise , c ' est un serveur qui refuse — et qui DIT pourquoi.
"""
try :
import runbooks as _rb
etape = _rb . etape_declaree ( runbook , cible , RACINE , index )
except Exception as exc : # noqa: BLE001
self . repondre_json ( 500 , { " erreur " : f " Registre des runbooks illisible : { exc } " } )
return
if etape is None :
self . repondre_json ( 409 , { " erreur " : f " « { cible } » n ' est pas une etape declaree "
f " du runbook « { runbook } ». Rien n ' a ete lance. " } )
return
# Les variables : celles du registre, dans l'ordre du registre.
args : list [ str ] = [ ]
for v in etape [ " variables " ] :
brut = str ( valeurs . get ( v [ " nom " ] , " " ) ) . strip ( )
if not brut :
if v . get ( " facultatif " ) :
continue
self . repondre_json ( 400 , { " erreur " : f " { v [ ' nom ' ] } est requis pour cette etape. " } )
return
if not MOTIF_VALEUR . match ( brut ) :
self . repondre_json ( 400 , { " erreur " : f " { v [ ' nom ' ] } : valeur refusee (caracteres "
f " interdits, ou commence par un tiret). " } )
return
if v . get ( " valeurs " ) and brut not in v [ " valeurs " ] :
self . repondre_json ( 400 , { " erreur " : f " { v [ ' nom ' ] } : « { brut } » n ' est pas une "
f " valeur admise ( { ' , ' . join ( v [ ' valeurs ' ] ) } ). " } )
return
args . append ( f " { v [ ' nom ' ] } = { brut } " )
# Les valeurs IMPOSEES par l'etape (CONFIRMER=true et compagnie) ne viennent
# jamais du navigateur : une confirmation qu'on peut envoyer soi-meme n'en est pas
# une. Le garde-fou reel est la confirmation ECRITE exigee cote page, plus la
# garde propre a la cible.
for clef , val in ( etape [ " fixes " ] or { } ) . items ( ) :
args . append ( f " { clef } = { val } " )
if not VERROU . acquire ( blocking = False ) :
self . repondre_json ( 409 , { " erreur " : " Une execution est deja en cours. " } )
return
fichier_vault = None
journal = None
try :
env = dict ( os . environ , PYTHONUNBUFFERED = " 1 " , ANSIBLE_FORCE_COLOR = " 0 " )
if vault :
fd , fichier_vault = tempfile . mkstemp ( prefix = " setops-vault- " )
os . write ( fd , str ( vault ) . encode ( " utf-8 " ) )
os . close ( fd )
env [ " ANSIBLE_VAULT_PASSWORD_FILE " ] = fichier_vault
self . send_response ( 200 )
self . send_header ( " Content-Type " , " text/plain; charset=utf-8 " )
self . send_header ( " Cache-Control " , " no-cache " )
self . end_headers ( )
# Un journal par execution. Chez un locataire il vit dans l'instance ; sur un
# runner de site il n'y a pas d'instance, et `tmp/` est deja hors depot.
dossier_logs = ( INSTANCE / " logs " ) if INSTANCE . exists ( ) else ( RACINE / " tmp " / " logs " )
dossier_logs . mkdir ( parents = True , exist_ok = True )
chemin_log = dossier_logs / f " { runbook } - { cible } - { time . strftime ( ' % Y % m %d T % H % M % S ' ) } .log "
journal = open ( chemin_log , " w " , encoding = " utf-8 " )
entete = f " $ make { cible } { ' ' . join ( args ) } \n \n "
journal . write ( entete )
self . _ecrire_flux ( entete )
tache_courante = " "
echecs : list [ dict ] = [ ]
proc = subprocess . Popen (
[ " make " , cible , * args ] ,
cwd = str ( RACINE ) , env = env ,
stdin = subprocess . DEVNULL ,
stdout = subprocess . PIPE , stderr = subprocess . STDOUT , text = True , bufsize = 1 ,
)
try :
for ligne in proc . stdout :
journal . write ( ligne )
if ligne . startswith ( " TASK [ " ) :
deb , fin = ligne . find ( " [ " ) , ligne . find ( " ] " )
if - 1 < deb < fin :
tache_courante = ligne [ deb + 1 : fin ]
elif ( " FAILED! " in ligne or " UNREACHABLE! " in ligne ) and \
( " fatal: " in ligne or ligne . lstrip ( ) . startswith ( " failed: " ) ) :
echec = _extraire_echec ( ligne , tache_courante )
if echec :
echecs . append ( echec )
if not self . _ecrire_flux ( ligne ) :
for reste in proc . stdout :
journal . write ( reste )
break
finally :
rc = proc . wait ( )
if rc != 0 and echecs :
self . _ecrire_flux ( " __RESUME__ " + json . dumps ( { " echecs " : echecs [ : 6 ] } ,
ensure_ascii = False ) + " \n " )
journal . write ( f " \n __FIN__ rc= { rc } \n " )
self . _ecrire_flux ( f " \n [journal : { os . path . relpath ( chemin_log , RACINE ) } ] \n __FIN__ rc= { rc } \n " )
finally :
if journal :
try :
journal . close ( )
except OSError :
pass
if fichier_vault :
try :
os . remove ( fichier_vault )
except OSError :
pass
VERROU . release ( )
2026-06-24 20:17:46 -04:00
def do_GET ( self ) - > None :
chemin = urlparse ( self . path ) . path
try :
if chemin == " / " :
page = HTML . replace ( " __JETON__ " , JETON )
self . repondre ( 200 , page . encode ( " utf-8 " ) , " text/html; charset=utf-8 " )
elif chemin == " /favicon.ico " :
self . repondre ( 204 , b " " , " image/x-icon " )
2026-09-16 14:36:59 -04:00
elif chemin == " /api/contexte " :
self . repondre_json ( 200 , contexte ( ) )
console : une vue A traiter rassemble ce que chaque console sait deja
Route en lecture /api/a-traiter et vue sous Veille, par gravite (a prendre, a savoir,
verifie). Locataire : genome face a la forge, face reseau, fiche du site, plan face a
l'inventaire, machines prevues. Site : genome, sa carte et chaque depot de locataire
face a la forge, face, index et fiche de chaque locataire. Les deux : joignabilite et
prerequis. Le niveau de la forge se lit par git ls-remote, sans rien ecrire.
94 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:12:27 -04:00
elif chemin == " /api/a-traiter " :
_c = console ( self . inventaire )
try :
elements = _c . a_traiter ( )
except Exception as exc :
self . repondre_json ( 500 , { " erreur " : f " { type ( exc ) . __name__ } : { exc } " } )
return
if elements is None :
self . repondre_json ( 404 , { " erreur " : " Cette console ne tient aucune veille. " } )
else :
self . repondre_json ( 200 , { " elements " : elements } )
2026-10-11 17:31:13 -04:00
elif chemin in ( " /api/lien-site " , " /api/locataires-heberges " ) :
# EN LECTURE, sans pouvoir requis : ces deux fichiers sont deja dans les
# depots que cette console monte. Une console qui n'a pas ce cote du
# contrat le dit (404), elle ne rend pas un objet vide qui passerait pour
# « rien a signaler ».
_c = console ( self . inventaire )
try :
charge = ( _c . lien_avec_le_site ( ) if chemin == " /api/lien-site "
else _c . locataires_heberges ( ) )
except Exception as exc :
self . repondre_json ( 500 , { " erreur " : f " { type ( exc ) . __name__ } : { exc } " } )
return
if charge is None :
self . repondre_json ( 404 , { " erreur " : " Cette console n ' a pas ce côté du contrat. " } )
else :
self . repondre_json ( 200 , charge )
2026-06-24 20:17:46 -04:00
elif chemin == " /api/inventaire " :
console : un tronc, deux branches — assembler deux fois est ce qui fait deriver
Il y avait deux assembleurs de charge, un par sorte de console. Ils ont derive deux fois le
meme jour : en forme (serveurs valait [] d'un cote et {} de l'autre, et charger() levait),
puis en contenu (cinq registres servis vides alors que le site a SON plan — 9 serveurs,
21 applications, 2 bases, 1 domaine, tous invisibles).
Console assemble, une seule fois, et fixe les clefs et leurs formes. Les branches ne
decident que de ce qui leur appartient : d'ou vient le plan, d'ou vient l'inventaire, quels
pouvoirs elles portent. ConsoleLocataire configure, ConsoleSite materialise et sert
desormais son propre plan, ConsolePoste herite du locataire et sait en plus sur quelle
fabric poser.
Le role et la portee ne se confondent pas : le role est une propriete de la classe, la
portee se calcule depuis les pouvoirs. Un poste prive de la voute du site reste l'atelier du
mainteneur et n'engendre pourtant rien. Le decoupage a montre un trou aussitot : ConsolePoste
heritait du refus d'un locataire — « elle ne sait pas sur quelle fabric poser » — alors qu'il
monte la carte ; ce qui lui manque est la voute, et accuser la mauvaise absence fait chercher
au mauvais endroit.
Le jugement des assistants remonte dans le tronc : il etait ecrit dans la route qui liste ET
dans celle qui execute, et celle qui se trompe est toujours celle qui execute.
Valide : make test a 0 echec, 8 tests de rendu sous node dont deux neufs, console lancee pour
de vrai (13 serveurs, 24 applications, 17 runbooks, 127 etapes, 0 ecart). make verifier a
aussi attrape une faute que j'avais laissee passer sur depots_perimes.yml — risky-shell-pipe,
corrige. P02 et P60 restent, et P60 demande une republication du wiki.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:55:49 -04:00
# LE GESTIONNAIRE NE SAIT PLUS LAQUELLE LUI REPOND, et c'est le but : il
# demandait « est-ce un site ? » pour choisir entre deux assembleurs, et
# les deux ont derive. Il demande maintenant sa charge a sa console.
self . repondre_json ( 200 , console ( self . inventaire ) . charge ( ) )
2026-07-01 17:39:44 -04:00
elif chemin == " /api/sondes " :
console : un tronc, deux branches — assembler deux fois est ce qui fait deriver
Il y avait deux assembleurs de charge, un par sorte de console. Ils ont derive deux fois le
meme jour : en forme (serveurs valait [] d'un cote et {} de l'autre, et charger() levait),
puis en contenu (cinq registres servis vides alors que le site a SON plan — 9 serveurs,
21 applications, 2 bases, 1 domaine, tous invisibles).
Console assemble, une seule fois, et fixe les clefs et leurs formes. Les branches ne
decident que de ce qui leur appartient : d'ou vient le plan, d'ou vient l'inventaire, quels
pouvoirs elles portent. ConsoleLocataire configure, ConsoleSite materialise et sert
desormais son propre plan, ConsolePoste herite du locataire et sait en plus sur quelle
fabric poser.
Le role et la portee ne se confondent pas : le role est une propriete de la classe, la
portee se calcule depuis les pouvoirs. Un poste prive de la voute du site reste l'atelier du
mainteneur et n'engendre pourtant rien. Le decoupage a montre un trou aussitot : ConsolePoste
heritait du refus d'un locataire — « elle ne sait pas sur quelle fabric poser » — alors qu'il
monte la carte ; ce qui lui manque est la voute, et accuser la mauvaise absence fait chercher
au mauvais endroit.
Le jugement des assistants remonte dans le tronc : il etait ecrit dans la route qui liste ET
dans celle qui execute, et celle qui se trompe est toujours celle qui execute.
Valide : make test a 0 echec, 8 tests de rendu sous node dont deux neufs, console lancee pour
de vrai (13 serveurs, 24 applications, 17 runbooks, 127 etapes, 0 ecart). make verifier a
aussi attrape une faute que j'avais laissee passer sur depots_perimes.yml — risky-shell-pipe,
corrige. P02 et P60 restent, et P60 demande une republication du wiki.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:55:49 -04:00
self . repondre_json ( 200 ,
sonder_donnees ( console ( self . inventaire ) . inventaire_brut ( ) ) )
2026-07-07 15:57:47 -04:00
elif chemin == " /api/devis-reseau " :
tenants = devis_reseau . decouvrir ( )
self . repondre_json ( 200 , {
" config " : devis_reseau . generer ( tenants ) if tenants else " " ,
" tenants " : [ { " nom " : nom , " index " : n [ " index " ] } for nom , _ , n in tenants ] ,
} )
2026-08-03 08:52:22 -04:00
elif chemin == " /api/devis-opnsense " :
# Import paresseux : ce module lit l'underlay et les inventaires de tous
# les tenants ; une erreur ici ne doit pas empecher le reste du panneau.
try :
import devis_opnsense as _dop
tenants = _dop . decouvrir ( )
self . repondre_json ( 200 , {
" config " : _dop . rendre ( _dop . construire ( tenants ) ) if tenants else " " ,
} )
except Exception as exc :
self . repondre_json ( 200 , { " config " : " " , " erreur " : str ( exc ) } )
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
elif chemin == " /api/runbooks " :
# Import paresseux, comme les devis : un registre casse ne doit pas
# emporter le reste de la console avec lui.
try :
import runbooks as _rb
ctx = contexte ( )
console : un tronc, deux branches — assembler deux fois est ce qui fait deriver
Il y avait deux assembleurs de charge, un par sorte de console. Ils ont derive deux fois le
meme jour : en forme (serveurs valait [] d'un cote et {} de l'autre, et charger() levait),
puis en contenu (cinq registres servis vides alors que le site a SON plan — 9 serveurs,
21 applications, 2 bases, 1 domaine, tous invisibles).
Console assemble, une seule fois, et fixe les clefs et leurs formes. Les branches ne
decident que de ce qui leur appartient : d'ou vient le plan, d'ou vient l'inventaire, quels
pouvoirs elles portent. ConsoleLocataire configure, ConsoleSite materialise et sert
desormais son propre plan, ConsolePoste herite du locataire et sait en plus sur quelle
fabric poser.
Le role et la portee ne se confondent pas : le role est une propriete de la classe, la
portee se calcule depuis les pouvoirs. Un poste prive de la voute du site reste l'atelier du
mainteneur et n'engendre pourtant rien. Le decoupage a montre un trou aussitot : ConsolePoste
heritait du refus d'un locataire — « elle ne sait pas sur quelle fabric poser » — alors qu'il
monte la carte ; ce qui lui manque est la voute, et accuser la mauvaise absence fait chercher
au mauvais endroit.
Le jugement des assistants remonte dans le tronc : il etait ecrit dans la route qui liste ET
dans celle qui execute, et celle qui se trompe est toujours celle qui execute.
Valide : make test a 0 echec, 8 tests de rendu sous node dont deux neufs, console lancee pour
de vrai (13 serveurs, 24 applications, 17 runbooks, 127 etapes, 0 ecart). make verifier a
aussi attrape une faute que j'avais laissee passer sur depots_perimes.yml — risky-shell-pipe,
corrige. P02 et P60 restent, et P60 demande une republication du wiki.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:55:49 -04:00
_c = console ( self . inventaire )
_juger = _c . peut_conduire
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
liste = [ ]
for rb in _rb . assembler ( RACINE ) :
assistants : la portee se pese a l'etape, pas a la sequence
Mesure sur la console de TechnoLibre, portee tenant : 6 runbooks conduisibles sur 17, et
parmi les onze fermes, locataire-deployer et machine-une — c'est-a-dire le travail
quotidien d'un locataire. La cause : flotte-creer et creer-vm engendrent des VM et
exigent la fabric, et une portee declaree pour toute la sequence faisait basculer avec eux
des etapes voisines qui ne demandent que ce que le locataire possede deja.
Le runbook ne donne plus que le defaut ; l'etape qui exige davantage le declare. Le
locataire conduit sa sequence et bute precisement la ou il faut : sur la machine a
engendrer, pas sur le deploiement qui suit. La page ferme l'etape seule avec sa raison, et
la garde de la route lit la portee de l'etape visee par son index.
Un droit calcule sur l'ensemble se trompe toujours dans le meme sens : il refuse a
quelqu'un ce qu'il a le droit de faire, et le refus parait fonde puisqu'il nomme un vrai
manque. Il a fallu une console de locataire reelle pour le voir — sur le poste, qui porte
les deux liens, les dix-sept sequences s'affichaient conduisibles.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec (4 tests neufs, dont un qui
nomme le cas exact), P83 verte. P02 reste en echec pour la raison anterieure deja consignee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 19:55:02 -04:00
# CHAQUE ETAPE SE PESE SEULE (2026-09-20, mesure sur TechnoLibre).
# Juge au seul runbook, une unique etape qui materialise fermait
# toute la sequence a un locataire — qui perdait le droit de
# deployer SA flotte. Il la conduit donc, et bute precisement la
# ou il faut : sur la machine a engendrer.
etapes = [ ]
for etape in rb [ " etapes " ] :
ok , pourquoi = _juger ( etape [ " portee " ] )
etapes . append ( { * * etape , " conduisible " : ok , " raison " : pourquoi } )
ok_rb , pourquoi_rb = _juger ( rb [ " portee " ] )
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
liste . append ( {
* * rb ,
assistants : la portee se pese a l'etape, pas a la sequence
Mesure sur la console de TechnoLibre, portee tenant : 6 runbooks conduisibles sur 17, et
parmi les onze fermes, locataire-deployer et machine-une — c'est-a-dire le travail
quotidien d'un locataire. La cause : flotte-creer et creer-vm engendrent des VM et
exigent la fabric, et une portee declaree pour toute la sequence faisait basculer avec eux
des etapes voisines qui ne demandent que ce que le locataire possede deja.
Le runbook ne donne plus que le defaut ; l'etape qui exige davantage le declare. Le
locataire conduit sa sequence et bute precisement la ou il faut : sur la machine a
engendrer, pas sur le deploiement qui suit. La page ferme l'etape seule avec sa raison, et
la garde de la route lit la portee de l'etape visee par son index.
Un droit calcule sur l'ensemble se trompe toujours dans le meme sens : il refuse a
quelqu'un ce qu'il a le droit de faire, et le refus parait fonde puisqu'il nomme un vrai
manque. Il a fallu une console de locataire reelle pour le voir — sur le poste, qui porte
les deux liens, les dix-sept sequences s'affichaient conduisibles.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec (4 tests neufs, dont un qui
nomme le cas exact), P83 verte. P02 reste en echec pour la raison anterieure deja consignee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 19:55:02 -04:00
" etapes " : etapes ,
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
# ON NE CACHE PAS CE QU'ON NE PEUT PAS FAIRE. Un runbook hors
# de portee reste VISIBLE et se lit : savoir que le geste
# existe, et chez qui il se fait, fait partie du metier.
assistants : la portee se pese a l'etape, pas a la sequence
Mesure sur la console de TechnoLibre, portee tenant : 6 runbooks conduisibles sur 17, et
parmi les onze fermes, locataire-deployer et machine-une — c'est-a-dire le travail
quotidien d'un locataire. La cause : flotte-creer et creer-vm engendrent des VM et
exigent la fabric, et une portee declaree pour toute la sequence faisait basculer avec eux
des etapes voisines qui ne demandent que ce que le locataire possede deja.
Le runbook ne donne plus que le defaut ; l'etape qui exige davantage le declare. Le
locataire conduit sa sequence et bute precisement la ou il faut : sur la machine a
engendrer, pas sur le deploiement qui suit. La page ferme l'etape seule avec sa raison, et
la garde de la route lit la portee de l'etape visee par son index.
Un droit calcule sur l'ensemble se trompe toujours dans le meme sens : il refuse a
quelqu'un ce qu'il a le droit de faire, et le refus parait fonde puisqu'il nomme un vrai
manque. Il a fallu une console de locataire reelle pour le voir — sur le poste, qui porte
les deux liens, les dix-sept sequences s'affichaient conduisibles.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec (4 tests neufs, dont un qui
nomme le cas exact), P83 verte. P02 reste en echec pour la raison anterieure deja consignee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 19:55:02 -04:00
" conduisible " : any ( e [ " conduisible " ] for e in etapes ) ,
" raison " : pourquoi_rb if not ok_rb else " " ,
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
} )
self . repondre_json ( 200 , { " runbooks " : liste , " contexte " : ctx ,
" ecarts " : _rb . verifier ( RACINE ) } )
except Exception as exc : # noqa: BLE001
self . repondre_json ( 200 , { " runbooks " : [ ] , " erreur " : str ( exc ) } )
Aligne le GUI et la doc sur le multi-instances
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>
2026-07-23 11:57:59 -04:00
elif chemin == " /api/instances " :
import instances as _inst
2026-07-23 14:13:36 -04:00
import instance_creer as _ic
Aligne le GUI et la doc sur le multi-instances
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>
2026-07-23 11:57:59 -04:00
liste = _inst . decouvrir ( )
self . repondre_json ( 200 , {
" instances " : liste ,
" collisions " : { str ( k ) : v for k , v in _inst . collisions ( liste ) . items ( ) } ,
2026-07-23 14:13:36 -04:00
" modeles " : sorted ( _ic . modeles_disponibles ( ) ) ,
" index_pris " : sorted ( _ic . index_federes_utilises ( ) ) ,
Aligne le GUI et la doc sur le multi-instances
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>
2026-07-23 11:57:59 -04:00
} )
2026-06-24 20:17:46 -04:00
else :
self . repondre_json ( 404 , { " erreur " : " Introuvable " } )
except Exception as exc :
self . repondre_json ( 500 , { " erreur " : str ( exc ) } )
def do_POST ( self ) - > None :
chemin = urlparse ( self . path ) . path
if self . headers . get ( " X-Jeton " ) != JETON :
self . repondre_json ( 403 , { " erreur " : " Jeton manquant ou invalide. " } )
return
longueur = int ( self . headers . get ( " Content-Length " , " 0 " ) or 0 )
corps = self . rfile . read ( longueur ) . decode ( " utf-8 " ) if longueur else " "
try :
donnees = json . loads ( corps ) if corps else { }
except json . JSONDecodeError :
self . repondre_json ( 400 , { " erreur " : " Corps JSON invalide. " } )
return
2026-09-16 14:36:59 -04:00
# LA GARDE EST ICI, PAS DANS LE NAVIGATEUR. Un bouton grise est une politesse ;
# ce qui protege, c'est un serveur qui refuse. Et il DIT pourquoi : « interdit »
# sans raison envoie chercher une panne la ou il n'y en a pas.
ctx = contexte ( )
besoin = POUVOIR_REQUIS . get ( chemin )
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
if besoin == " selon-le-runbook " :
# LA PORTEE VIENT DU REGISTRE, PAS DU CORPS DE LA REQUETE. Le navigateur nomme
# un runbook ; c'est le fichier declare qui dit ce que ce runbook exige.
besoin = None
try :
import runbooks as _rb
vise = str ( donnees . get ( " runbook " , " " ) )
assistants : la portee se pese a l'etape, pas a la sequence
Mesure sur la console de TechnoLibre, portee tenant : 6 runbooks conduisibles sur 17, et
parmi les onze fermes, locataire-deployer et machine-une — c'est-a-dire le travail
quotidien d'un locataire. La cause : flotte-creer et creer-vm engendrent des VM et
exigent la fabric, et une portee declaree pour toute la sequence faisait basculer avec eux
des etapes voisines qui ne demandent que ce que le locataire possede deja.
Le runbook ne donne plus que le defaut ; l'etape qui exige davantage le declare. Le
locataire conduit sa sequence et bute precisement la ou il faut : sur la machine a
engendrer, pas sur le deploiement qui suit. La page ferme l'etape seule avec sa raison, et
la garde de la route lit la portee de l'etape visee par son index.
Un droit calcule sur l'ensemble se trompe toujours dans le meme sens : il refuse a
quelqu'un ce qu'il a le droit de faire, et le refus parait fonde puisqu'il nomme un vrai
manque. Il a fallu une console de locataire reelle pour le voir — sur le poste, qui porte
les deux liens, les dix-sept sequences s'affichaient conduisibles.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec (4 tests neufs, dont un qui
nomme le cas exact), P83 verte. P02 reste en echec pour la raison anterieure deja consignee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 19:55:02 -04:00
_i = donnees . get ( " index " )
_etape = _rb . etape_declaree ( vise , str ( donnees . get ( " cible " , " " ) ) , RACINE ,
_i if isinstance ( _i , int ) else None )
# La portee de L'ETAPE. Celle du runbook n'est que son defaut, et juger
# sur elle fermerait a un locataire une sequence qui est la sienne.
portee_etape = _etape [ " portee " ] if _etape else None
connu = any ( r [ " id " ] == vise for r in _rb . assembler ( RACINE ) )
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
except Exception : # noqa: BLE001
assistants : la portee se pese a l'etape, pas a la sequence
Mesure sur la console de TechnoLibre, portee tenant : 6 runbooks conduisibles sur 17, et
parmi les onze fermes, locataire-deployer et machine-une — c'est-a-dire le travail
quotidien d'un locataire. La cause : flotte-creer et creer-vm engendrent des VM et
exigent la fabric, et une portee declaree pour toute la sequence faisait basculer avec eux
des etapes voisines qui ne demandent que ce que le locataire possede deja.
Le runbook ne donne plus que le defaut ; l'etape qui exige davantage le declare. Le
locataire conduit sa sequence et bute precisement la ou il faut : sur la machine a
engendrer, pas sur le deploiement qui suit. La page ferme l'etape seule avec sa raison, et
la garde de la route lit la portee de l'etape visee par son index.
Un droit calcule sur l'ensemble se trompe toujours dans le meme sens : il refuse a
quelqu'un ce qu'il a le droit de faire, et le refus parait fonde puisqu'il nomme un vrai
manque. Il a fallu une console de locataire reelle pour le voir — sur le poste, qui porte
les deux liens, les dix-sept sequences s'affichaient conduisibles.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec (4 tests neufs, dont un qui
nomme le cas exact), P83 verte. P02 reste en echec pour la raison anterieure deja consignee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 19:55:02 -04:00
portee_etape , connu = None , False
if not connu :
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
self . repondre_json ( 409 , { " erreur " : f " Runbook inconnu : « { donnees . get ( ' runbook ' , ' ' ) } ». " } )
return
assistants : la portee se pese a l'etape, pas a la sequence
Mesure sur la console de TechnoLibre, portee tenant : 6 runbooks conduisibles sur 17, et
parmi les onze fermes, locataire-deployer et machine-une — c'est-a-dire le travail
quotidien d'un locataire. La cause : flotte-creer et creer-vm engendrent des VM et
exigent la fabric, et une portee declaree pour toute la sequence faisait basculer avec eux
des etapes voisines qui ne demandent que ce que le locataire possede deja.
Le runbook ne donne plus que le defaut ; l'etape qui exige davantage le declare. Le
locataire conduit sa sequence et bute precisement la ou il faut : sur la machine a
engendrer, pas sur le deploiement qui suit. La page ferme l'etape seule avec sa raison, et
la garde de la route lit la portee de l'etape visee par son index.
Un droit calcule sur l'ensemble se trompe toujours dans le meme sens : il refuse a
quelqu'un ce qu'il a le droit de faire, et le refus parait fonde puisqu'il nomme un vrai
manque. Il a fallu une console de locataire reelle pour le voir — sur le poste, qui porte
les deux liens, les dix-sept sequences s'affichaient conduisibles.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec (4 tests neufs, dont un qui
nomme le cas exact), P83 verte. P02 reste en echec pour la raison anterieure deja consignee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 19:55:02 -04:00
# Etape introuvable a cette place : `executer_runbook` le dira mieux que nous,
# avec le nom et la place. On le laisse refuser, sans rien lancer.
console : un tronc, deux branches — assembler deux fois est ce qui fait deriver
Il y avait deux assembleurs de charge, un par sorte de console. Ils ont derive deux fois le
meme jour : en forme (serveurs valait [] d'un cote et {} de l'autre, et charger() levait),
puis en contenu (cinq registres servis vides alors que le site a SON plan — 9 serveurs,
21 applications, 2 bases, 1 domaine, tous invisibles).
Console assemble, une seule fois, et fixe les clefs et leurs formes. Les branches ne
decident que de ce qui leur appartient : d'ou vient le plan, d'ou vient l'inventaire, quels
pouvoirs elles portent. ConsoleLocataire configure, ConsoleSite materialise et sert
desormais son propre plan, ConsolePoste herite du locataire et sait en plus sur quelle
fabric poser.
Le role et la portee ne se confondent pas : le role est une propriete de la classe, la
portee se calcule depuis les pouvoirs. Un poste prive de la voute du site reste l'atelier du
mainteneur et n'engendre pourtant rien. Le decoupage a montre un trou aussitot : ConsolePoste
heritait du refus d'un locataire — « elle ne sait pas sur quelle fabric poser » — alors qu'il
monte la carte ; ce qui lui manque est la voute, et accuser la mauvaise absence fait chercher
au mauvais endroit.
Le jugement des assistants remonte dans le tronc : il etait ecrit dans la route qui liste ET
dans celle qui execute, et celle qui se trompe est toujours celle qui execute.
Valide : make test a 0 echec, 8 tests de rendu sous node dont deux neufs, console lancee pour
de vrai (13 serveurs, 24 applications, 17 runbooks, 127 etapes, 0 ecart). make verifier a
aussi attrape une faute que j'avais laissee passer sur depots_perimes.yml — risky-shell-pipe,
corrige. P02 et P60 restent, et P60 demande une republication du wiki.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:55:49 -04:00
if portee_etape is not None :
ok_etape , pourquoi = console ( self . inventaire ) . peut_conduire ( portee_etape )
if not ok_etape :
self . repondre_json ( 409 , { " erreur " : pourquoi , " portee " : ctx [ " portee " ] } )
return
2026-09-16 14:36:59 -04:00
if besoin and not ctx [ " peut " ] . get ( besoin ) :
self . repondre_json ( 409 , { " erreur " : ctx [ " raison " ] . get ( besoin , " Geste hors de "
" la portee de cette console. " ) ,
" portee " : ctx [ " portee " ] , " pouvoir " : besoin } )
return
2026-06-24 20:17:46 -04:00
try :
if chemin == " /api/inventaire " :
self . repondre_json ( 409 , { " erreur " : " hosts.yml est genere depuis le plan. "
" Edite instance/plan/serveurs.yml et instance/plan/applications.yml "
" (vues Serveurs / Applications), puis « Appliquer le plan ». " } )
elif chemin == " /api/bases " :
valider_bases ( donnees , charger_applications ( FICHIER_APPLICATIONS ) )
ecrire_bases ( FICHIER_BASES , donnees )
self . repondre_json ( 200 , inventaire_api ( self . inventaire ) )
elif chemin == " /api/applications " :
2026-07-22 21:32:42 -04:00
valider_applications ( donnees , charger_domaines ( FICHIER_DOMAINES ) ,
charger_serveurs ( FICHIER_SERVEURS ) )
2026-06-24 20:17:46 -04:00
ecrire_applications ( FICHIER_APPLICATIONS , donnees )
self . repondre_json ( 200 , inventaire_api ( self . inventaire ) )
elif chemin == " /api/serveurs " :
valider_serveurs ( donnees , charger_nomenclature ( FICHIER_NOMENCLATURE ) )
ecrire_serveurs ( FICHIER_SERVEURS , donnees )
self . repondre_json ( 200 , inventaire_api ( self . inventaire ) )
GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas
LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas
ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a
sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre
sur six. couverture_gui verifier passe : les 28 champs des plans reels
sont editables.
Elle n est pas un registre comme les autres : elle decrit la REGLE dont
VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce
qu elle derive et les VM qui la portent ; l index est montre mais pas
editable, parce qu il est alloue par le site ; valider_nomenclature
refuse de retirer une fonction encore portee, ou de designer une zone
non declaree.
DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS.
Le formulaire des bases, livre la veille, etait casse dans un navigateur.
Il lisait data.schema, or il n existe aucun data global : c est une const
locale de charger(). ReferenceError a l ouverture, et zone morte dans
sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le
banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui
charge le JS entier dans un DOM simule et dessine les douze vues, avec son
controle negatif.
Le schema decrivait reservations comme une table de zones ; le fichier
reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait
rien. Elle compare desormais aussi la FORME.
ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le
bloc entier des qu une valeur change : quinze entrees compactes devenaient
42 lignes, et le commentaire du poste d exploitation se retrouvait en tete
du bloc, ou il affirmait que collab etait le poste d exploitation. Un
commentaire deplace n est pas laid, il est faux. _fusion_table edite les
tables ligne a ligne ; le diff fait trois lignes.
Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran
(reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait
encore par write_text, oubliee au passage des ecritures atomiques.
LIMITE : deux registres sur six sont generes, et je n ai toujours pas
ouvert cette page dans un navigateur.
make prouver : CONFORME, 60 OK, 0 echec, 1 saute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
elif chemin == " /api/nomenclature " :
valider_nomenclature ( donnees , charger_serveurs ( FICHIER_SERVEURS ) )
ecrire_nomenclature ( FICHIER_NOMENCLATURE , donnees )
self . repondre_json ( 200 , inventaire_api ( self . inventaire ) )
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
elif chemin == " /api/domaines " :
valider_domaines ( donnees )
ecrire_domaines ( FICHIER_DOMAINES , donnees )
self . repondre_json ( 200 , inventaire_api ( self . inventaire ) )
2026-06-30 10:07:17 -04:00
elif chemin == " /api/intrants " :
ecrire_intrants ( donnees )
self . repondre_json ( 200 , inventaire_api ( self . inventaire ) )
2026-07-23 12:12:29 -04:00
elif chemin == " /api/instance-utiliser " :
cible = basculer_instance ( str ( donnees . get ( " nom " , " " ) ) . strip ( ) )
self . repondre_json ( 200 , { " ok " : True , " cible " : cible } )
2026-07-23 14:13:36 -04:00
elif chemin == " /api/instance-creer " :
import instance_creer as _ic
brut_idx = str ( donnees . get ( " index " , " " ) ) . strip ( )
_ic . creer ( str ( donnees . get ( " nom " , " " ) ) . strip ( ) ,
str ( donnees . get ( " modele " , " " ) ) . strip ( ) ,
int ( brut_idx ) if brut_idx else None )
self . repondre_json ( 200 , { " ok " : True , " nom " : str ( donnees . get ( " nom " , " " ) ) . strip ( ) } )
2026-06-24 20:17:46 -04:00
elif chemin == " /api/instancier " :
2026-07-01 17:52:39 -04:00
# Cliquer « Appliquer le plan » EST l'intention explicite : on force
# (le diff-refus est un garde-fou CLI ; ici le clic le confirme). git = filet.
2026-06-24 20:17:46 -04:00
resultat = subprocess . run (
2026-07-01 17:52:39 -04:00
[ " python3 " , str ( RACINE / " scripts/instancier.py " ) , " appliquer " , " --force " ] ,
2026-06-24 20:17:46 -04:00
capture_output = True , text = True , cwd = str ( RACINE ) )
self . repondre_json ( 200 , { " ok " : resultat . returncode == 0 ,
" sortie " : ( resultat . stdout + resultat . stderr ) . strip ( ) } )
elif chemin == " /api/verifier " :
self . executer_flux ( str ( donnees . get ( " hote " , " " ) ) , " verifier " , donnees . get ( " vault " ) )
elif chemin == " /api/deployer " :
self . executer_flux ( str ( donnees . get ( " hote " , " " ) ) , " deployer " , donnees . get ( " vault " ) )
2026-07-01 15:58:32 -04:00
elif chemin == " /api/creer " :
self . executer_flux ( str ( donnees . get ( " hote " , " " ) ) , " creer " , donnees . get ( " vault " ) )
assistants : cent trente-deux cibles, et aucune ne disait dans quel ordre
La console offrait des boutons sans sequence. Rien n'y apprenait que site-creer precede
forge-amorcer, que le premier passage de site-deployer-tout s'arrete sur une forge vide
sans que ce soit un echec, ni que rien n'est pret avant valider : cet ordre vivait en
prose dans des documents que la console ne porte pas.
La vue Assistants conduit 17 runbooks et 126 etapes. Les 132 cibles documentees y sont,
chacune portee par un assistant ou exemptee avec son motif — une exemption muette est
refusee. Le registre ne recopie pas le Makefile : il declare l'ordre, la nature, la portee
et le pourquoi, et le libelle de chaque etape est lu dans le Makefile au moment de servir.
P83 est ecrite en meme temps que la liste, pas apres, parce qu'une liste qui suit une
autre prend du retard. Onze tests lui presentent des registres faux, un par forme de
retard, et exigent qu'elle les refuse.
Le navigateur ne nomme pas une commande, il nomme une place : la route lance ce que le
registre declare a cet index-la, avec les seules variables declarees. L'index compte, le
premier jour d'un site jouant site-deployer-tout deux fois. Une etape qui ecrit attend que
la precedente ait reussi ; une mesure reste toujours offerte, parce que mesurer apres un
echec est exactement ce qu'on fait ensuite.
Valide : runbooks.py verifier a 0 ecart, make test a 0 echec, les 83 preuves rejouees, et
la console lancee pour de vrai — 17 runbooks servis, six requetes malformees refusees une
a une, une etape de mesure executee de bout en bout avec son journal.
Limite, anterieure a ce travail : P02 (test_ecriture_plan) echoue sur domaines.yml, a
l'identique sur une copie de HEAD. Ajouter ou retirer un domaine public depuis la vue
Domaines leverait a l'enregistrement. Non corrige ici.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:31:16 -04:00
elif chemin == " /api/runbook-etape " :
_idx = donnees . get ( " index " )
self . executer_runbook ( str ( donnees . get ( " runbook " , " " ) ) ,
str ( donnees . get ( " cible " , " " ) ) ,
donnees . get ( " valeurs " ) or { } ,
donnees . get ( " vault " ) ,
int ( _idx ) if isinstance ( _idx , int ) else None )
2026-07-01 15:58:32 -04:00
elif chemin == " /api/pousser " :
self . executer_flux ( str ( donnees . get ( " hote " , " " ) ) , " pousser " , donnees . get ( " vault " ) )
2026-06-24 20:17:46 -04:00
else :
self . repondre_json ( 404 , { " erreur " : " Introuvable " } )
except Exception as exc :
self . repondre_json ( 400 , { " erreur " : str ( exc ) } )
def main ( ) - > int :
parser = argparse . ArgumentParser ( description = " Interface web locale pour gerer un inventaire Set-OPS. " )
parser . add_argument ( " --inventaire " , type = Path , default = INVENTAIRE_DEFAUT )
parser . add_argument ( " --hote " , default = " 127.0.0.1 " )
parser . add_argument ( " --port " , type = int , default = 8765 )
args = parser . parse_args ( )
serveur = ThreadingHTTPServer ( ( args . hote , args . port ) , Gestionnaire )
print ( f " Interface inventaire: http:// { args . hote } : { args . port } / " )
print ( f " Inventaire: { Gestionnaire . inventaire } " )
serveur . serve_forever ( )
return 0
if __name__ == " __main__ " :
raise SystemExit ( main ( ) )