diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b25360..f61bbb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,60 @@ # CHANGELOG — Set-OPS +## 2026-09-16 (4) — La page cesse de proposer ce que le serveur refuse + +La portee etait derivee et gardee au serveur ; la page, elle, proposait encore tout. On +apprenait donc l'interdit AU CLIC — au milieu d'un geste, sur une console qu'on croyait +la bonne. + +**Un seul entonnoir.** Les quatre gestes d'hote passent par `lancer(mode, index)` : une +garde la couvre les cinq endroits qui emettent un bouton « Deployer ». `POUVOIR_PAR_MODE` +y rattache chaque mode a son pouvoir, et le refus NOMME sa raison. + +**Les boutons demeurent, inertes.** Les retirer ferait croire que le geste n'existe pas — +meme raisonnement que les integrations universelles, affichees en lecture seule plutot +qu'omises. Ils portent leur raison en infobulle : « exploiter n'est pas engendrer » sur la +creation de VM chez un locataire, « elle n'a la voute d'aucun locataire » sur la +configuration chez un site. + +**La fabric est marquee.** Chez un locataire, les devis de commutateurs et de frontiere +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 et cesse d'etre lu comme une source. + +**P81 s'etend** : elle refuse desormais aussi une page qui aurait perdu `POUVOIR_PAR_MODE` +ou l'un de ses quatre modes. Le serveur et la page doivent porter la meme coupure. + +### Le banc de rendu a attrape ce que `node --check` ne voit pas + +Ma garde de page prenait pour un contexte TOUT ce qu'on lui donnait. Dans le banc, la +reponse d'une autre route est passee pour un contexte, puis `peut()` a lu +`contexte.peut[...]` sur `undefined` : **TypeError a l'ouverture, page morte**. La syntaxe, +elle, etait juste — `node --check` restait vert, et c'est exactement pourquoi +`test_rendu_gui.py` existe : *le rendu, et non la seule syntaxe*. + +Deux corrections, et la seconde compte autant que la premiere : + +- on **verifie la forme** avant d'adopter un contexte (`peut` est un objet, `portee` une + chaine) ; +- sans contexte connu, **on ne retire rien**. Une console qui griserait ses boutons parce + qu'elle n'a pas su lire sa portee serait pire que le defaut corrige : elle empecherait + un geste legitime sans rien expliquer. Le serveur, lui, refuse quand meme — c'est lui la + garde. + +Et une couleur ne se relit pas, elle se **regarde** : mon badge de portee posait un texte +sombre sur un fond clair, dans une console en theme sombre. Vu sur une capture, pas dans le +code. + +### Au passage, une faute de ma part, attrapee par le harnais + +En eprouvant les refus, j'ai vise une machine REELLE depuis une console de locataire +simulee (`SETOPS_UNDERLAY` neutralise). `/api/instancier` n'a pas ete refuse — c'est +correct — et a donc regenere `hosts.yml` de TechnoLibre SANS la fabric : `proxmox_pont` et +`chrony_serveurs` perdus, `proxmox_etiquette_vlan` change, 13 hotes d'ecart. + +**P03 l'a vu au passage suivant**, et le fichier a ete restaure par git. Une epreuve de +garde n'a pas besoin d'une cible vivante ; celle-ci en a pris une. + ## 2026-09-16 (3) — La console affichait un ecosysteme vide, et c'etait le site Servie par le runner d'un SITE, la console d'exploitation montrait **zero serveur, zero diff --git a/scripts/inventory_gui.py b/scripts/inventory_gui.py index b17506f..2a5c7f7 100644 --- a/scripts/inventory_gui.py +++ b/scripts/inventory_gui.py @@ -1886,18 +1886,42 @@ HTML = r""" let contexte = null; + // 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]) || ''; + } + // 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) { - contexte = ctx || null; + // 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; const badge = document.getElementById('portee'); - if (!badge || !ctx) return; + 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: ['#7c4a02', 'color-mix(in srgb, #b9860b 18%, var(--surface))'], - tenant: ['#0d3b34', 'color-mix(in srgb, var(--teal) 16%, var(--surface))'], + site: ['#e0a83a', 'rgba(185, 134, 11, .18)'], + tenant: ['var(--teal)', 'var(--surface-3)'], poste: ['var(--muted)', 'var(--surface-3)'], - orphelin: ['var(--rouge)', 'color-mix(in srgb, var(--rouge) 14%, var(--surface))'], + orphelin: ['var(--rouge)', 'var(--surface-3)'], }; const [fg, bg] = couleurs[ctx.portee] || couleurs.poste; badge.textContent = ctx.titre; @@ -2113,7 +2137,8 @@ HTML = r""" const prod = i.production ? 'prod' : ''; const action = i.actif ? 'active' - : ``; + : ``; return ` ${i.actif ? '★ ' : ''}${echapper(i.nom)} ${i.index} @@ -2128,7 +2153,8 @@ HTML = r""" - +
Copie le modèle vers ../<nom> et fixe l'index. Ne bascule pas : reste à renseigner intrants + voûte, puis « Activer ».${(findexPris||[]).length ? ' Index déjà pris : ' + (findexPris||[]).join(', ') + '.' : ''}
` : ''; return `${banniere}${lignes}
InstanceIndexVLANStatutActive
@@ -2190,7 +2216,16 @@ HTML = r"""
${echapper(devisFrontiereTexte)}
` : `
Devis frontière indisponible${dfr.erreur ? ' : ' + echapper(dfr.erreur) : ''}.
`; - cible.innerHTML = flotte + devis + frontiere; + // 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') ? '' : + `
+ Lecture seule — la fabric appartient à l'hébergeur. ${echapper(pourquoiPas('fabric'))} + Cette console configure un écosystème ; elle ne pose ni VLAN, ni route, ni règle de frontière.
`; + cible.innerHTML = avisFabric + flotte + devis + frontiere; } catch (e) { cible.innerHTML = '
Erreur : ' + echapper(e.message) + '
'; } @@ -3254,11 +3289,18 @@ HTML = r""" hoteOpCourant = s.nom; const hi = h ? hotes.indexOf(h) : -1; let boutons = ''; - if (h) boutons += ``; + // 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"`; + if (h) boutons += ``; if (h && h.etat === 'actif' && !hoteBloque(h)) { const clsDep = estProduction ? 'danger' : 'primaire'; - boutons += `` - + ``; + boutons += `` + + ``; } let pied; if (boutons) { @@ -3608,6 +3650,13 @@ HTML = r""" const hote = hotes[index] ? hotes[index].nom : ''; if (!hote) { message('Hôte introuvable.', 'erreur'); return; } if (occupe) return; + // 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; + } let vault = ''; if ((mode === 'deployer' || mode === 'pousser') && !verifie[hote]) { message('Vérifiez (dry-run) d\'abord.', 'erreur'); return; } if (mode === 'creer' || mode === 'deployer' || mode === 'pousser' || mode === 'verifier') { diff --git a/scripts/prouver.py b/scripts/prouver.py index d95a025..a947460 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -4378,6 +4378,18 @@ def preuve_console_dit_sa_portee() -> tuple[bool, str]: fautes.append(f"`POUVOIR_REQUIS` nomme des routes qui n'existent plus : " f"{', '.join(inutiles)}") + # LA PAGE DOIT PORTER LA MEME COUPURE. Un serveur qui refuse pendant qu'une page + # propose fait decouvrir l'interdit AU CLIC — au milieu d'un geste, sur une console + # qu'on croyait la bonne. Ce n'est pas la garde, c'est ce qui evite d'y aller. + bloc = re.search(r"const POUVOIR_PAR_MODE = \{([^}]*)\}", source, re.S) + if not bloc: + fautes.append("la page ne porte plus `POUVOIR_PAR_MODE` : les boutons " + "proposeraient des gestes que le serveur refuse") + else: + for mode in ("creer", "verifier", "deployer", "pousser"): + if mode not in bloc.group(1): + fautes.append(f"le mode `{mode}` n'est rattache a aucun pouvoir dans la page") + if fautes: return False, ("La console ne dit pas ce qu'elle peut :\n - " + "\n - ".join(fautes)) return True, (f"Portee `{ctx['portee']}` derivee des symlinks, source d'inventaire "