la page cesse de proposer ce que le serveur refuse
Un seul entonnoir pour les quatre gestes d hote (lancer), POUVOIR_PAR_MODE qui rattache chaque mode a son pouvoir, et des boutons qui demeurent mais inertes, avec leur raison en infobulle. Les panneaux de fabric sont marques lecture seule chez un locataire : ils viennent d une copie locale qui avait deja diverge. P81 refuse en plus une page qui aurait perdu POUVOIR_PAR_MODE. Deux fautes attrapees en route : le banc de rendu a vu un TypeError a l ouverture (ma garde adoptait pour un contexte tout ce qu on lui donnait) la ou node --check restait vert ; et une couleur illisible, vue sur une capture, pas dans le code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
e46fa198fd
commit
8456d298bf
3 changed files with 127 additions and 11 deletions
55
CHANGELOG.md
55
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
|
||||
|
|
|
|||
|
|
@ -1886,18 +1886,42 @@ HTML = r"""<!doctype html>
|
|||
|
||||
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"""<!doctype html>
|
|||
const prod = i.production ? '<span class="chip chi-tls-requis">prod</span>' : '';
|
||||
const action = i.actif
|
||||
? '<span class="chip chi-tls-requis">active</span>'
|
||||
: `<button type="button" class="fantome" onclick="basculerInstance('${echapper(i.nom)}', ${i.production ? 'true' : 'false'})">Activer</button>`;
|
||||
: `<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>`;
|
||||
return `<tr>
|
||||
<td class="mono">${i.actif ? '★ ' : ''}${echapper(i.nom)}</td>
|
||||
<td class="mono">${i.index}</td>
|
||||
|
|
@ -2128,7 +2153,8 @@ HTML = r"""<!doctype html>
|
|||
<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>
|
||||
<button type="button" class="primaire" onclick="creerInstance()">Créer</button>
|
||||
<button type="button" class="primaire"${peut('editer_le_plan') ? ''
|
||||
: ` disabled title="${echapper(pourquoiPas('editer_le_plan'))}" style="opacity:.45"`} onclick="creerInstance()">Créer</button>
|
||||
</div>
|
||||
<div class="hint-dep" style="margin-top:4px">Copie le modèle vers <code>../<nom></code> et fixe l'index. Ne bascule pas : reste à renseigner intrants + voûte, puis « Activer ».${(findexPris||[]).length ? ' Index déjà pris : ' + (findexPris||[]).join(', ') + '.' : ''}</div>` : '';
|
||||
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>
|
||||
|
|
@ -2190,7 +2216,16 @@ HTML = r"""<!doctype html>
|
|||
<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>`;
|
||||
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') ? '' :
|
||||
`<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;
|
||||
} catch (e) {
|
||||
cible.innerHTML = '<div class="vide" style="padding:14px">Erreur : ' + echapper(e.message) + '</div>';
|
||||
}
|
||||
|
|
@ -3254,11 +3289,18 @@ HTML = r"""<!doctype html>
|
|||
hoteOpCourant = s.nom;
|
||||
const hi = h ? hotes.indexOf(h) : -1;
|
||||
let boutons = '';
|
||||
if (h) boutons += `<button type="button" title="Cloner la VM depuis le golden template (Proxmox)" onclick="lancer('creer', ${hi})">🖥 Créer la VM</button>`;
|
||||
// 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 += `<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>`;
|
||||
if (h && h.etat === 'actif' && !hoteBloque(h)) {
|
||||
const clsDep = estProduction ? 'danger' : 'primaire';
|
||||
boutons += `<button type="button" id="btn-verifier" onclick="lancer('verifier', ${hi})">Vérifier</button>`
|
||||
+ `<button type="button" id="btn-deployer" class="${clsDep}" onclick="lancer('deployer', ${hi})">Déployer</button>`;
|
||||
boutons += `<button type="button" id="btn-verifier"${inerte('configurer')} onclick="lancer('verifier', ${hi})">Vérifier</button>`
|
||||
+ `<button type="button" id="btn-deployer" class="${clsDep}"${inerte('configurer')} onclick="lancer('deployer', ${hi})">Déployer</button>`;
|
||||
}
|
||||
let pied;
|
||||
if (boutons) {
|
||||
|
|
@ -3608,6 +3650,13 @@ HTML = r"""<!doctype html>
|
|||
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') {
|
||||
|
|
|
|||
|
|
@ -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 "
|
||||
|
|
|
|||
Loading…
Reference in a new issue