diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9662152..5ffa0cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,40 @@
# CHANGELOG — Set-OPS
+## 2026-10-11 (127) — Deux consoles servaient la même page ; chacune dit maintenant qui elle est
+
+**94 preuves.** `make verifier` passe, le banc de rendu (`test_rendu_gui.py`) aussi, et les
+deux consoles ont été rendues sur leurs vraies charges puis regardées en capture. Seul
+l'affichage change : aucune route, aucun pouvoir, aucun geste n'est ajouté ni retiré.
+
+### Un site se voyait offrir le plan d'un locataire
+
+La console du site et celle du locataire partageaient dix boutons de vue. Le site y
+trouvait la nomenclature, les intrants et un bouton « Sauvegarder » grisé, pour un plan dont
+il ne peut rien écrire ; ses pouvoirs et leurs raisons n'existaient qu'en infobulle. La page
+dessine désormais sa navigation selon la portée — Exploiter / Le plan / Lire pour un
+locataire, Le terrain / Gestes / Plan en lecture pour un site — et un encadré « Cette console
+peut » dit les quatre pouvoirs en clair. L'en-tête porte une couleur d'identité (sarcelle,
+ambre, gris, rouge) qui ne sert qu'à dire qui parle, jamais un état.
+
+### La mosaïque cachait les zones, la fiche cachait les gestes
+
+Le parc est un tableau groupé par catégorie et VLAN ; plan, réel, inventaire et prérequis y
+restent quatre colonnes distinctes, et `j`/`k` suit l'ordre affiché. Au site, l'inventaire
+servait les hyperviseurs comme des machines « planifiées » : ils sont montrés comme les nœuds
+qu'ils sont, avec les machines qu'ils portent. Dans la fiche, Vérifier puis Déployer passent
+en tête, numérotés dans l'ordre que `majBoutonsDeploiement` imposait déjà. Les assistants
+qu'une console ne peut pas conduire sont repliés, toujours lisibles ; les boutons suivent la
+nature de l'étape. Le travail non sauvegardé se dit dans un bandeau en tête de vue, avec
+l'étape suivante, au lieu d'un « • » sur un bouton parmi douze.
+
+### Les rapports de preuves sont versionnés
+
+`preuve-2026-10-11.md` rejoint `docs/audit/`, comme ses prédécesseurs, et la carte compte
+53 pièces d'audit (P48).
+
+**Limite.** Les étapes d'un assistant ne sont pas encore groupées en phases : il faudrait les
+déclarer dans le registre, les deviner aux noms des cibles serait fragile.
+
## 2026-10-11 (126) — Le site savait créer une machine de locataire ; son assistant ne l'offrait qu'en bloc
**94 preuves.** `make prouver` est conforme, `make test` passe et `runbooks.py verifier`
diff --git a/docs/audit/preuve-2026-10-11.md b/docs/audit/preuve-2026-10-11.md
new file mode 100644
index 0000000..72782c4
--- /dev/null
+++ b/docs/audit/preuve-2026-10-11.md
@@ -0,0 +1,130 @@
+# Preuve de conformite — Set-OPS — 2026-10-11
+
+> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
+> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
+> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
+> [`docs/audit/README.md`](README.md), et le registre trace :
+> [`docs/audit/affirmations.md`](affirmations.md).
+
+- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
+- **Verdict** : ✅ CONFORME (94 OK · 0 echec · 0 saute)
+
+## Preuves
+
+| # | Preuve | Affirmations | Statut | Detail |
+|---|---|---|---|---|
+| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | [0m[0m |
+| P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK |
+| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 3 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre : plan et inventaire applique coincident. |
+| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
+| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
+| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
+| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check), 1 nom(s) surveille(s) sans reference orpheline. |
+| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 42 groupes classes, aucun cycle, aucune arete en arriere ; playbooks/site.yml a jour. |
+| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 40 rôles, 122 flux, schéma + matrice OK. |
+| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
+| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_resolveur_site |
+| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
+| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
+| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
+| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
+| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 13 hotes, 33 groupes (inventaire dechiffre et parse). |
+| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
+| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 35 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
+| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 30 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
+| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) et 7 zone(s) de site : adressage 100% derive du seed index. |
+| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 4 instance(s) federee(s), aucun index en collision. |
+| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
+| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 14 reseau(x), aucune collision avec la plage tenant. |
+| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
+| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 40 groupe(s), 108 regle(s). |
+| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 13 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
+| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
+| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. |
+| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 34 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 23, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
+| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. |
+| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 78 scripts expliques et atteignables, 151 cibles make documentees, 69 roles avec README. |
+| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (148 cle(s) declaree(s) par l'instance). |
+| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 44 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). |
+| P34 | Chaque document declare son lecteur | — | ✅ OK | 50 document(s) declarent leur lecteur (48 genere(s) exempte(s)). |
+| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
+| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) de l'ecosysteme et 3 du site detiennent de l'etat, tous porteurs de `client_backup` (11 groupe(s) au catalogue). |
+| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
+| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 42 role(s) serveur/client tous nommes, 42 groupe(s) cite(s) en table existent tous. |
+| P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 97 page(s) de wiki toutes atteignables. |
+| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
+| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 74 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
+| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 3 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal). |
+| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 9 machine(s) du plan retrouvees, 174 regle(s) du site. |
+| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |
+| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
+| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
+| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
+| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 102 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
+| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (140 lignes). |
+| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 277 regles `pass`), tous non consignes et tous motives. |
+| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
+| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
+| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere : WAN muet, interfa |
+| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
+| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 13 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 13. |
+| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
+| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (94 preuves, 69 roles, 42 groupes). |
+| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
+| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
+| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `e9215a2` (publie le 2026-10-07). |
+| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 56 champ(s) sur 7 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp |
+| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:9, serveurs:3 champ(s) lus par validateur). |
+| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
+| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 47 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, client_s |
+| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
+| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (6 exposition(s)). |
+| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 13 service(s) expose(s) portent le nom du plan, sur 2 inventaire(s) : instance, SITE. |
+| P68 | Cle de depot telechargee : mesuree avant d'etre utilisee | — | ✅ OK | 5 role(s) telechargent une cle de depot, tous la mesurent avant de s'en servir. |
+| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. |
+| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 27 artefact(s) direct(s) tenus par le depot du site. |
+| P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. |
+| P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. |
+| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 10 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). |
+| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal ». |
+| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |
+| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 189 gabarits de role : tous se rendent. |
+| P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. |
+| P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). |
+| P79 | Replis silencieux : une derivation vide ne passe pas pour un succes | — | ✅ OK | 4 ecosysteme(s) (OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, SITE-Chezlepro) : pattes, edges, certificats, rechargements, jumeaux d'amorcage, sorties du |
+| P80 | Remise au client : inscrite, nommee, et son second temps a l'heure | — | ✅ OK | Aucun ecosysteme remis a un client : rien a tenir. |
+| P81 | La console dit sa portee, et ne sert pas un inventaire vide en silence | — | ✅ OK | Portee `poste` derivee des voutes portees, source d'inventaire `instance/inventories/principal/hosts.yml`, et 15 route(s) POST exigent toutes un pouvoir. |
+| P82 | DNS public : les zones publiees sont servies, signees avant d'etre exposees | — | ✅ OK | 2 zone(s) publique(s) declaree(s), toutes servies par le site ; 2 replication(s) declaree(s) des deux cotes ; transfert ouvert a la cle seule ; aucune expositio |
+| P83 | Assistants : le registre des runbooks ne prend pas de retard sur le Makefile | — | ✅ OK | 17 runbooks, 145 etapes, 150 cibles documentees : chacune portee par un assistant ou exemptee avec son motif. |
+| P84 | La fiche du site dit exactement ce que chaque locataire porte | — | ✅ OK | 2 fiche(s) de site : chacune dit exactement ce que son locataire porte (copies, racine, inventaire genere). |
+| P85 | La face reseau du locataire dit exactement ce que le site en tire | — | ✅ OK | 2 face(s) reseau : chacune dit exactement ce que le site en tire (decouverte, devis Proxmox, frontiere, DNS public, sauvegarde). |
+| P86 | Machine par machine, le locataire et Proxmox admettent les memes entrees | — | ✅ OK | 2 locataire(s) : machine par machine et port par port, nftables et Proxmox admettent les memes sources. |
+| P87 | La frontiere designe chaque locataire par les identites qu'il publie | — | ✅ OK | 2 locataire(s) : la frontiere les designe par les identites qu'ils publient (supernet, administration, tunnel, groupes, routes, sortie). |
+| P88 | Ce qui entre depuis l'Internet chez un locataire, la frontiere le tient de lui | — | ✅ OK | 2 locataire(s) : regles WAN, redirections et tunnel disent exactement ce que leur face reseau ouvre a tous. |
+| P89 | Ce que l'administration atteint chez un locataire, la frontiere le tient de lui | — | ✅ OK | 2 locataire(s) : par la gestion, le VPN et le tunnel, l'administration atteint exactement ce que leur face reseau lui ouvre. |
+| P90 | Ce qui sort d'un locataire vers l'Internet, la frontiere le tient de lui | — | ✅ OK | 2 locataire(s) : la frontiere laisse sortir exactement ce que leur face reseau publie. |
+| P91 | La fiche du site est deposee a jour, et l'inventaire se genere sans le site | — | ✅ OK | 2 locataire(s) : fiche du site deposee a jour, et inventaire genere sans le site identique a l'octet pres. |
+| P92 | Chaque locataire a publie sa face reseau a jour | — | ✅ OK | 2 locataire(s) : face reseau publiee, a jour. |
+| P93 | La face publie, machine par machine, les parametres de clonage de l'inventaire | — | ✅ OK | 26 machine(s) : la face publie exactement les parametres de clonage de l'inventaire. |
+| P94 | Creer, raser et placer un locataire par sa face visent les memes machines | — | ✅ OK | 2 locataire(s) : creer, raser et placer visent par la face les memes machines que par l'instance montee. |
+
+## Couverture des affirmations ✅ du registre
+
+Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
+Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
+`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
+d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
+elles restent hors du harnais recurrent (rien d'executable a rejouer).
+
+## Declarations d'intention (⚪ invérifiables localement — assumees)
+
+Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
+comme declarations d'intention, non comme preuves :
+
+- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
+ contre une flotte vivante.
+- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
+- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
+- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
+
+_Rapport genere le 2026-10-11._
diff --git a/docs/carte-set-ops.md b/docs/carte-set-ops.md
index 4eac7e6..348ed05 100644
--- a/docs/carte-set-ops.md
+++ b/docs/carte-set-ops.md
@@ -26,7 +26,7 @@ README de rôles). Cette page comble ces deux trous.
| rôles | 69 | `roles/*/` |
| README de rôles | 69 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
| documents | 46 | `docs/*.md` |
-| pièces d'audit | 52 | `docs/audit/*` |
+| pièces d'audit | 53 | `docs/audit/*` |
| unités de wiki | 27 | `wiki/*.md` |
| décisions en vigueur | 85 | lignes `\| **D-nn** \|` de `decisions-architecture.md` |
| décisions renversées | 3 | lignes `\| **D-nn** —` du même document |
diff --git a/scripts/inventory_gui.py b/scripts/inventory_gui.py
index e10f6a5..79dce17 100644
--- a/scripts/inventory_gui.py
+++ b/scripts/inventory_gui.py
@@ -2072,46 +2072,156 @@ HTML = r"""
.rb-nature.destructif{background:#3a1c1c;color:#f08a8a}
.rb-meta{font-size:11.5px;opacity:.6;margin-top:4px}
.rb-but{font-size:12.5px;line-height:1.5;opacity:.88}
+
+/* --- 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: 3px solid var(--identite); gap: 14px; }
+body[data-portee="site"] header { background: color-mix(in srgb, #1a170f 86%, transparent); }
+.sep { width: 1px; height: 26px; background: var(--ligne-forte); flex: none; }
+.identite { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
+.identite-haut { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
+.portee-etiq { font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; padding: 3px 8px; border-radius: 5px; white-space: nowrap;
+ color: var(--identite); background: color-mix(in srgb, var(--identite) 16%, transparent); }
+.ident-nom { font-weight: 800; font-size: 15px; white-space: nowrap; }
+.cadre { display: flex; align-items: stretch; min-height: calc(100vh - 64px); }
+.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: 16px 10px 20px; display: flex; flex-direction: column; gap: 18px; position: sticky; top: 64px; max-height: calc(100vh - 64px); overflow: auto; }
+.nav-groupe { display: flex; flex-direction: column; gap: 2px; }
+.nav-titre { font-size: 10.5px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; color: var(--faint); padding: 0 10px; margin: 0 0 3px; }
+.nav-sous { font-size: 11px; color: var(--faint); padding: 0 10px; margin: -1px 0 5px; line-height: 1.35; }
+.nav-lien { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 36px; padding: 0 10px; border-radius: 7px; border: 0; background: transparent;
+ color: var(--muted); font: inherit; font-size: 13.5px; 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 2px 0 0 var(--identite); }
+.nav-lien .n { margin-left: auto; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; color: var(--faint); font-weight: 500; }
+.pouvoirs { border: 1px solid var(--ligne); border-radius: var(--rayon-sm); background: var(--surface); padding: 11px 12px; display: flex; flex-direction: column; gap: 7px; font-size: 12px; }
+.pouvoir { display: flex; gap: 7px; align-items: flex-start; color: var(--texte); line-height: 1.35; }
+.pouvoir.non { color: var(--faint); cursor: help; }
+.pouvoir .i { flex: none; width: 14px; text-align: center; font-weight: 800; }
+.pouvoir .i.oui { color: var(--vert); }
+.raccourcis { font-size: 11px; color: var(--faint); padding: 0 10px; line-height: 1.5; }
+.bandeau-plan { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 0 0 14px; padding: 9px 12px; border-radius: 9px;
+ background: color-mix(in srgb, var(--ambre) 9%, var(--surface)); border: 1px 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: 12px; flex-wrap: wrap; margin: 0 0 14px; }
+.vue-titre { margin: 0; font-size: 21px; font-weight: 750; letter-spacing: -.01em; }
+.vue-sous { font-size: 13px; color: var(--muted); }
+.etiq { display: inline-flex; align-items: center; gap: 6px; height: 22px; padding: 0 8px; border-radius: 5px; font-size: 11.5px; 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 1px 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: 7px; font-size: 12.5px; color: var(--muted); }
+.pt.ns { background: var(--faint); }
+.tableau { background: var(--surface); border: 1px solid var(--ligne); border-radius: var(--rayon); overflow-x: auto; margin-bottom: 18px; }
+.tableau-corps { min-width: 720px; }
+.rang { display: grid; grid-template-columns: 1.35fr 1fr .75fr .95fr .9fr 1.7fr; align-items: center; gap: 10px; width: 100%; min-height: 42px; padding: 0 14px;
+ border: 0; border-bottom: 1px solid var(--ligne); border-radius: 0; background: transparent; color: var(--texte); font: inherit; font-size: 13px; 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: 34px; font-size: 11px; font-weight: 700; letter-spacing: .04em; 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 3px 0 0 var(--identite); }
+.rang .carte-nom { font-size: 13.5px; }
+.rang .ip { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; color: var(--muted); }
+.rang .rendus { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.rang.site { grid-template-columns: 1.3fr 1fr .6fr .8fr .9fr 2fr; }
+.groupe-tete { display: flex; align-items: center; gap: 10px; min-height: 32px; padding: 0 14px; background: var(--surface-2); border-bottom: 1px solid var(--ligne);
+ color: var(--muted); font-size: 11.5px; font-weight: 700; letter-spacing: .03em; }
+.groupe-tete .mono { color: var(--faint); font-weight: 500; }
+.noeuds { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 10px; margin-bottom: 18px; }
+.noeud { background: var(--surface); border: 1px solid var(--ligne); border-radius: var(--rayon); padding: 11px 13px; display: flex; flex-direction: column; gap: 8px; }
+.noeud-tete { display: flex; align-items: center; gap: 8px; }
+.puces { display: flex; flex-wrap: wrap; gap: 5px; }
+.puce { display: inline-flex; align-items: center; height: 24px; padding: 0 8px; border-radius: 5px; background: var(--surface-3); color: var(--muted); font-size: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
+.puce.socle { background: transparent; box-shadow: inset 0 0 0 1px var(--ligne-forte); color: var(--faint); }
+.gestes { margin: 14px 16px 4px; border: 1px solid var(--ligne-forte); border-radius: var(--rayon-sm); background: var(--surface-2); padding: 12px 13px; display: flex; flex-direction: column; gap: 11px; }
+.gestes-tete { display: flex; align-items: center; gap: 8px; }
+.geste { display: flex; align-items: center; gap: 12px; }
+.geste .num { width: 24px; height: 24px; border-radius: 50%; flex: none; display: flex; align-items: center; justify-content: center; font-size: 12px; 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.5px; }
+.geste .quoi span { font-size: 12px; color: var(--faint); }
+.gestes-note { font-size: 12px; color: var(--faint); border-top: 1px solid var(--ligne); padding-top: 9px; }
+button.prod { background: #7f1d1d; border-color: #b23b3b; color: #fff; }
+button.prod:hover:not(:disabled) { background: #8f2424; border-color: #c94a4a; }
+.section-fiche { padding: 12px 16px 2px; }
+.section-fiche > .section-tete { margin-bottom: 7px; }
+.rb-autres { margin: -6px 0 14px; font-size: 12.5px; color: var(--muted); }
+.rb-autres > summary { cursor: pointer; padding: 4px 2px; }
+.rb-autres .rb-liste { margin-top: 8px; }
+.rb-entete { display: flex; flex-direction: column; gap: 6px; margin: 0 0 12px; }
+.rb-progres { display: flex; align-items: center; gap: 12px; font-size: 12.5px; color: var(--muted); }
+.rb-barre { flex: 0 1 300px; height: 6px; border-radius: 3px; 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: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center; opacity: 1; padding: 0; font-size: 12px; 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 1px var(--identite); }
+.rb-etape button.destructif { background: transparent; border-color: #b23b3b; color: var(--rouge); }
+@media (max-width: 1000px) {
+ .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 200px; }
+ .pouvoirs { flex: 1 1 100%; }
+}
Set-OPS
Votre artisan numérique
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
- Dépendances causales
-
-
-
-
-
+
+
+
+
+
+
+
+
+ Plan modifié.
+
+
+
+
+
+ Prérequis entre groupes
+
+
+
+
+
+
@@ -2275,20 +2385,16 @@ let selMachine = null;
contexte = ctx;
const badge = document.getElementById('portee');
if (!badge) return;
- // LES COULEURS DU THEME, PAS LES MIENNES (corrige le 2026-09-16). Ma premiere
- // version posait un texte sombre sur un fond clair — illisible sur une console qui
- // est en thème sombre. Vu sur une capture, pas dans le code : une couleur ne se
- // relit pas, elle se regarde.
- const couleurs = {
- site: ['#e0a83a', 'rgba(185, 134, 11, .18)'],
- tenant: ['var(--teal)', 'var(--surface-3)'],
- poste: ['var(--muted)', 'var(--surface-3)'],
- orphelin: ['var(--rouge)', 'var(--surface-3)'],
- };
- const [fg, bg] = couleurs[ctx.portee] || couleurs.poste;
- badge.textContent = ctx.titre;
- badge.style.color = fg;
- badge.style.background = bg;
+ // 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 || '');
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.
@@ -2300,10 +2406,93 @@ let selMachine = null;
};
verrou(document.getElementById('btn-sauver'), ctx.peut.editer_le_plan,
ctx.raison.editer_le_plan);
+ dessinerNavigation();
document.querySelectorAll('button[onclick^="ouvrirIntrants"]').forEach(
b => verrou(b, ctx.peut.editer_le_plan, ctx.raison.editer_le_plan));
}
+ // 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;
+ if (peut('editer_le_plan')) {
+ return [
+ {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)]);
+ return [
+ {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 `';
+ }).join('');
+ return `
${echapper(g.titre)}
`
+ + (g.sous ? `
${echapper(g.sous)}
` : '') + liens + '
';
+ }).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 `
`
+ + `${oui ? '✓' : '—'}${echapper(libelle)}
`;
+ }).join('') : '';
+ nav.innerHTML = '
' + groupes
+ + (pouvoirs ? `
Cette console peut
${pouvoirs}`
+ + '
Survoler une ligne grisée pour savoir pourquoi.
' : '')
+ + '
Raccourcis : 1-3 vues · j/k serveur suivant · v vérifier · d déployer · / filtrer · Ctrl+S sauvegarder
';
+ }
+
async function charger() {
try {
const ctxRep = await fetch('/api/contexte');
@@ -2363,21 +2552,29 @@ let selMachine = null;
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; }
- function dessiner() { dessinerResume(); dessinerGrilles(); dessinerDependances(); dessinerDetail(); majIndicateurSauvegarde(); }
+ function dessiner() { dessinerResume(); dessinerNavigation(); dessinerGrilles(); dessinerDependances(); dessinerDetail(); majIndicateurSauvegarde(); }
+ // 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'};
function majIndicateurSauvegarde() {
const b = document.getElementById('btn-sauver');
if (!b) return;
- const editable = vuePrincipale === 'serveurs' || vuePrincipale === 'integrations' || vuePrincipale === 'applications' || vuePrincipale === 'bases' || vuePrincipale === 'domaines' || vuePrincipale === 'nomenclature';
+ const editable = vuePrincipale in NOMS_VUES_EDITABLES;
const sale = ((vuePrincipale === 'serveurs' || vuePrincipale === 'integrations') && serveursModifie)
|| (vuePrincipale === 'applications' && applicationsModifie)
|| (vuePrincipale === 'bases' && basesModifie)
|| (vuePrincipale === 'domaines' && domainesModifie)
|| (vuePrincipale === 'nomenclature' && nomModifie);
- b.classList.toggle('attention', sale);
- b.textContent = sale ? 'Sauvegarder •' : 'Sauvegarder';
- b.disabled = !editable;
+ b.disabled = !editable || !peut('editer_le_plan');
b.title = editable ? 'Sauvegarder cette vue (Ctrl+S)' : 'Lecture seule — édite dans Serveurs / Applications / Bases';
+ 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 = 'Plan modifié — ' + echapper(NOMS_VUES_EDITABLES[vuePrincipale] || '')
+ + ' pas encore sauvegardé. Ensuite : appliquer le plan pour régénérer l\'inventaire.';
}
function dessinerResume() {
@@ -2385,8 +2582,13 @@ let selMachine = null;
const bloques = hotes.filter(h => hoteBloque(h)).length;
const vivantes = hotes.filter(h => sondes[h.nom] === true).length;
const sonde = Object.keys(sondes).length > 0;
+ // 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
+ ? 'PROD'
+ : 'bac à sable';
document.getElementById('resume').innerHTML = `
- ${estProduction ? 'PROD' : 'bac à sable'}
${hotes.length} hôtes${actifs} actifs${hotes.length - actifs} planifiés
@@ -2404,15 +2606,7 @@ let selMachine = null;
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);
- document.getElementById('btn-vue-serveurs').classList.toggle('on', v === 'serveurs');
- document.getElementById('btn-vue-applications').classList.toggle('on', v === 'applications');
- document.getElementById('btn-vue-bases').classList.toggle('on', v === 'bases');
- document.getElementById('btn-vue-domaines').classList.toggle('on', v === 'domaines');
- document.getElementById('btn-vue-integrations').classList.toggle('on', v === 'integrations');
- document.getElementById('btn-vue-flux').classList.toggle('on', v === 'flux');
- document.getElementById('btn-vue-couches').classList.toggle('on', v === 'couches');
- document.getElementById('btn-vue-reseau').classList.toggle('on', v === 'reseau');
- document.getElementById('btn-vue-assistants').classList.toggle('on', v === 'assistants');
+ dessinerNavigation();
dessinerGrilles();
dessinerDetail(); // le panneau droit reflète la sélection de la vue courante
majIndicateurSauvegarde();
@@ -2428,7 +2622,7 @@ let selMachine = null;
Application — un service qui tourne sur un hôte, via un groupe, et expose un FQDN.
Base — une donnée hébergée sur un hôte et consommée par une application.
-
Saisis à gauche (tuiles), édite à droite ; les dérivés (VMID/IP/VLAN/groupes) se calculent à « Appliquer le plan ».
+
Choisis à gauche (une ligne par serveur), édite à droite ; les dérivés (VMID/IP/VLAN/groupes) se calculent à « Appliquer le plan ».