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>
This commit is contained in:
parent
7ef7ca196a
commit
859ac07a54
3 changed files with 258 additions and 1 deletions
58
CHANGELOG.md
58
CHANGELOG.md
|
|
@ -1,5 +1,63 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-20 (10) — Un vide doit avoir la forme de ce qu'il remplace
|
||||
|
||||
**83 preuves, `make test` à 0 échec, 7 tests de rendu.** La console de
|
||||
`console.genese.internal` — celle du runner de SITE-Chezlepro — ne montrait rien. Elle a
|
||||
douze machines.
|
||||
|
||||
### `charger()` levait, et la page mourait avant de dessiner
|
||||
|
||||
`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()`
|
||||
lève. Toute la console d'un site s'arrêtait là, avant la première vue, et le message
|
||||
accusait une méthode manquante plutôt qu'une forme qui ment.
|
||||
|
||||
Un vide doit avoir la **forme** de ce qu'il remplace. Sinon ce n'est pas un vide, c'est un
|
||||
autre objet qui se fait passer pour lui — et le malentendu ne se voit qu'au moment où
|
||||
quelqu'un appelle une méthode dessus.
|
||||
|
||||
Un test compare désormais les deux charges **type par type** pour les clés qu'elles
|
||||
partagent, avec deux écarts admis et motivés. Il ne demande pas les mêmes valeurs — un
|
||||
site n'a pas de plan de locataire — seulement les mêmes formes.
|
||||
|
||||
### Et la vue regardait au mauvais endroit
|
||||
|
||||
Le registre vide était **voulu** : « on ne fabrique pas un faux plan de tenant pour
|
||||
remplir l'écran ». C'est juste — un site déclare ses machines dans son propre plan, avec
|
||||
`ip`, `vmid` et `noeud` **écrits**, là où un locataire les dérive de son seed ; il est le
|
||||
terrain, il n'a rien dont les faire descendre.
|
||||
|
||||
Mais la vue d'accueil tirait de ce registre vide le message d'un locataire sans serveurs —
|
||||
« + Serveur », `make serveurs-bootstrap` — et les douze machines, servies dans `hotes`,
|
||||
n'étaient dessinées nulle part. Une console de site montre maintenant **ses** machines :
|
||||
mêmes tuiles que les serveurs d'un locataire, en lecture seule, avec leur adresse, leur
|
||||
VMID, leurs rôles et leur point de vie ; le panneau droit dit ce que cette console peut —
|
||||
matérialiser le terrain, n'entrer chez aucun locataire — et renvoie aux Assistants.
|
||||
|
||||
**C'est le défaut du 2026-09-16 par une autre porte.** La première fois, l'inventaire était
|
||||
vide et la page dessinait ce vide comme un plan vide. Cette fois l'inventaire est **plein**
|
||||
et c'est la vue qui regarde ailleurs. Le premier correctif a donné à la console de quoi
|
||||
répondre ; il ne lui a pas appris où regarder.
|
||||
|
||||
### Ce qui a trouvé le défaut
|
||||
|
||||
Pas une lecture : **le banc**. `test_rendu_gui.py` dessine les vues sous `node` dans un
|
||||
DOM simulé. En lui donnant pour la première fois la charge d'une console de SITE, il a
|
||||
levé `(data.serveurs || []).map is not a function` — c'est-à-dire la cause réelle, sous
|
||||
le symptôme que je m'apprêtais à corriger. Sans lui, j'aurais livré une vue juste par
|
||||
dessus un `charger()` qui lève, et la console serait restée vide.
|
||||
|
||||
### Ce qui a été vérifié
|
||||
|
||||
`node --check` sur le bloc `<script>` (147 707 caractères), les 7 tests de rendu,
|
||||
`make test` à 0 échec, `runbooks.py verifier` à 0 écart, P81 et P83 vertes. Le test neuf
|
||||
nomme chaque machine du site attendue à l'écran, refuse le message d'amorçage d'un
|
||||
locataire, et vérifie que choisir une machine montre ses rôles.
|
||||
|
||||
**P02 reste en échec**, pour la raison antérieure consignée à l'entrée `(6)`.
|
||||
|
||||
## 2026-09-20 (9) — Ce qui n'existe qu'ici ne se détruit pas depuis ici
|
||||
|
||||
**83 preuves, `make test` à 0 échec, 133 cibles documentées.** Le runner de
|
||||
|
|
|
|||
|
|
@ -275,7 +275,17 @@ def inventaire_api_du_site(ctx: dict) -> dict:
|
|||
"bases": {},
|
||||
"applications": {},
|
||||
"domaines": {},
|
||||
"serveurs": {},
|
||||
# UNE LISTE VIDE, PAS UNE TABLE VIDE (2026-09-20).
|
||||
#
|
||||
# `inventaire_api` sert `serveurs` en LISTE ; ici c'etait une table. Les deux sont
|
||||
# « vides », et la page ne les lit pas pareil : `(data.serveurs || []).map(...)`
|
||||
# trouve `{}` — truthy, sans `.map` — et `charger()` LEVE. Toute la console d'un
|
||||
# site mourait donc avant de dessiner quoi que ce soit, et le message d'erreur
|
||||
# accusait une methode manquante plutot qu'une forme qui ment.
|
||||
#
|
||||
# Un vide doit avoir la FORME de ce qu'il remplace. Sinon ce n'est pas un vide,
|
||||
# c'est un autre objet qui se fait passer pour lui.
|
||||
"serveurs": [],
|
||||
"hotes": liste_hotes(data),
|
||||
"intrants": None,
|
||||
"integrations_disponibles": integrations_disponibles(),
|
||||
|
|
@ -1935,6 +1945,9 @@ HTML = r"""<!doctype html>
|
|||
let domainesModifie = false;
|
||||
const AUTORITES_DNS = ['primaire-cache', 'auto-heberge', 'delegue'];
|
||||
let serveurs = [];
|
||||
// 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;
|
||||
let serveursModifie = false;
|
||||
let ongletActif = 'reseau';
|
||||
let occupe = false;
|
||||
|
|
@ -3334,7 +3347,83 @@ HTML = r"""<!doctype html>
|
|||
${recon ? `<div class="carte-grps">${recon}</div>` : ''}
|
||||
</button>`;
|
||||
}
|
||||
// LA CONSOLE D'UN SITE DESSINE SES MACHINES (2026-09-20).
|
||||
//
|
||||
// `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.
|
||||
//
|
||||
// Un site a douze machines et sa console n'en montrait aucune : c'est le defaut du
|
||||
// 2026-09-16 qui revient par une autre porte. La premiere fois, l'inventaire etait
|
||||
// vide ; cette fois il est plein et c'est la VUE qui regarde au mauvais endroit.
|
||||
function dessinerMachinesDuSite() {
|
||||
const cible = document.getElementById('grilles');
|
||||
const nom = (contexte && contexte.fabric) || 'ce site';
|
||||
const entete = `<div class="section-grille"><div class="section-tete">Machines de ${echapper(nom)}
|
||||
<span class="compte">${hotes.length}</span></div>
|
||||
<div class="hint-dep">Ces machines sont 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 :
|
||||
un site est le terrain, il n'a rien dont les faire descendre. Cette console ne les édite
|
||||
donc pas ; elle les <b>matérialise</b> et leur applique des rôles — voir la vue
|
||||
<b>Assistants</b>.</div></div>`;
|
||||
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;
|
||||
}
|
||||
const vus = hotes.map((h, i) => [h, i]).filter(([h]) =>
|
||||
!filtre || (h.nom || '').toLowerCase().includes(filtre));
|
||||
const sections = [['Actives', vus.filter(([h]) => h.etat === 'actif')],
|
||||
['Planifiées', vus.filter(([h]) => h.etat !== 'actif')]];
|
||||
cible.innerHTML = entete + sections.filter(([, l]) => l.length).map(([t, l]) =>
|
||||
`<div class="section-grille"><div class="section-tete">${t} <span class="compte">${l.length}</span></div>
|
||||
<div class="grille-cartes">${l.map(([h, i]) => carteMachineDuSite(h, i)).join('')}</div></div>`).join('');
|
||||
}
|
||||
|
||||
// LE MEME GABARIT QUE LES SERVEURS, et pas un second qui lui ressemblerait : deux
|
||||
// cartes qui se ressemblent finissent par ne plus se ressembler.
|
||||
function carteMachineDuSite(h, i) {
|
||||
const actif = h.etat === 'actif';
|
||||
const roles = (h.groupes || []).length;
|
||||
return `<button type="button" class="carte ${actif ? 'actif' : 'attente'} ${i === selMachine ? 'selectionnee' : ''}" id="tuile-mach-${i}" onclick="choisirMachine(${i})">
|
||||
<div class="carte-haut"><span class="badge-statut ${actif ? 'ok' : 'attente'}">${actif ? 'actif' : 'planifié'}</span>${h.vmid ? `<span class="carte-vmid">#${echapper(h.vmid)}</span>` : ''}</div>
|
||||
<div class="carte-nom">${echapper(h.nom)}</div>
|
||||
<div class="carte-ip">${pointVie(h.nom)}${echapper(h.adresse_ip || '—')}</div>
|
||||
${roles ? `<div class="carte-grps"><span class="tag">${roles} rôle${roles > 1 ? 's' : ''}</span></div>` : ''}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
function choisirMachine(i) { selMachine = i; dessinerMachinesDuSite(); dessinerDetail(); }
|
||||
|
||||
function inspecteurMachineDuSite() {
|
||||
const cible = document.getElementById('detail');
|
||||
const h = hotes[selMachine];
|
||||
if (!h) {
|
||||
cible.innerHTML = `<div class="detail"><div class="section-tete">Console du SITE</div>
|
||||
<div class="hint-dep">Choisis une machine à gauche. Cette console <b>matérialise</b> le
|
||||
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>
|
||||
<div class="hint-dep" style="margin-top:10px">Les séquences qui lui reviennent sont
|
||||
dans la vue <b>Assistants</b>.</div></div>`;
|
||||
return;
|
||||
}
|
||||
const ligne = (c, v) => v ? `<div><span class="aide-ro">${c}</span> <span class="mono">${echapper(String(v))}</span></div>` : '';
|
||||
cible.innerHTML = `<div class="detail"><div class="section-tete">${echapper(h.nom)}</div>
|
||||
${ligne('état', h.etat)}${ligne('adresse', h.adresse_ip)}${ligne('VMID', h.vmid)}
|
||||
${ligne('VLAN', h.vlan)}${ligne('nœud', h.noeud)}
|
||||
<div class="section-tete" style="margin-top:12px">Rôles</div>
|
||||
<div>${(h.groupes || []).map(g => `<span class="chip">${echapper(g)}</span>`).join(' ') || '<span class="aide-ro">aucun</span>'}</div>
|
||||
<div class="hint-dep" style="margin-top:12px">Lecture seule. Le plan de ce site vit dans
|
||||
son dépôt ; cette console ne l'édite pas. Pour lui appliquer un rôle :
|
||||
<code>make site-appliquer GROUPE=…</code>, ou la vue <b>Assistants</b>.</div></div>`;
|
||||
}
|
||||
|
||||
function dessinerServeurs() {
|
||||
// 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();
|
||||
const cible = document.getElementById('grilles');
|
||||
const entete = BANNIERE_MODELE + `<div class="section-grille"><div class="section-tete">Serveurs <span class="compte">${serveurs.length}</span>
|
||||
<button type="button" class="fantome" style="margin-left:auto" onclick="ajouterServeur()">+ Serveur</button>
|
||||
|
|
@ -3346,6 +3435,7 @@ HTML = r"""<!doctype html>
|
|||
`<div class="section-grille"><div class="section-tete">${t} <span class="compte">${l.length}</span></div><div class="grille-cartes">${l.map(([s, i]) => carteServeur(s, i)).join('')}</div></div>`).join('');
|
||||
}
|
||||
function inspecteurServeur() {
|
||||
if (!peut('editer_le_plan')) return inspecteurMachineDuSite();
|
||||
const cible = document.getElementById('detail');
|
||||
if (selServeur == null || !serveurs[selServeur]) {
|
||||
hoteOpCourant = '';
|
||||
|
|
|
|||
|
|
@ -146,6 +146,75 @@ charger().then(function () {
|
|||
"""
|
||||
|
||||
|
||||
# LE BANC D'UNE CONSOLE DE SITE. On ne rejoue pas les neuf vues : on regarde CE QUE
|
||||
# la vue d'accueil dessine, parce que c'est la qu'un site ne montrait rien.
|
||||
APRES_SITE = r"""
|
||||
charger().then(function () {
|
||||
var sortie = {erreurs: [], grille: '', detail: '', detailChoisi: '', hotes: 0};
|
||||
try {
|
||||
sortie.hotes = (typeof hotes !== 'undefined' && hotes) ? hotes.length : 0;
|
||||
setVue('serveurs');
|
||||
sortie.grille = document.getElementById('grilles').innerHTML;
|
||||
sortie.detail = document.getElementById('detail').innerHTML;
|
||||
if (sortie.hotes) { choisirMachine(0); sortie.detailChoisi = document.getElementById('detail').innerHTML; }
|
||||
} catch (e) { sortie.erreurs.push(String((e && e.stack) || e)); }
|
||||
console.log('__RESULTAT__' + JSON.stringify(sortie));
|
||||
}).catch(function (e) {
|
||||
console.log('__RESULTAT__' + JSON.stringify({erreurs: ['charger() : ' + ((e && e.stack) || e)]}));
|
||||
});
|
||||
"""
|
||||
|
||||
|
||||
class TestFormeDesCharges(unittest.TestCase):
|
||||
"""Les deux charges que la page consomme ont-elles la MEME FORME ?
|
||||
|
||||
Une console de locataire recoit `inventaire_api`, une console de site
|
||||
`inventaire_api_du_site`. La page est la MEME : ce qu'elle sait lire d'un cote, elle
|
||||
doit pouvoir le lire de l'autre.
|
||||
|
||||
Le 2026-09-20, `serveurs` valait `[]` d'un cote et `{}` de l'autre. Les deux sont
|
||||
« vides », et la page ne les lit pas pareil : `(data.serveurs || []).map(...)` trouve
|
||||
une table — truthy, sans `.map` — et `charger()` LEVE. La console d'un site mourait
|
||||
avant de dessiner, et l'erreur accusait une methode manquante plutot qu'une forme.
|
||||
|
||||
Un vide doit avoir la FORME de ce qu'il remplace. Ce test ne demande pas que les deux
|
||||
charges portent les memes VALEURS — un site n'a pas de plan de locataire — seulement
|
||||
qu'elles portent les memes TYPES. Ce test n'a pas besoin de node.
|
||||
"""
|
||||
|
||||
# Ce que les deux charges ne partagent legitimement pas, et pourquoi.
|
||||
ECARTS_ADMIS = {
|
||||
"contexte": "ajoute par la route GET pour un locataire, deja present pour un site",
|
||||
"intrants": "un site n'a pas de plan de locataire dont tirer des intrants",
|
||||
}
|
||||
|
||||
def test_les_deux_charges_portent_les_memes_types(self):
|
||||
if not g.FICHIER_UNDERLAY.exists():
|
||||
self.skipTest("aucune fabric montee : pas de charge de site a comparer")
|
||||
ctx = dict(g.contexte())
|
||||
ctx.update(portee="site", ecosysteme=None)
|
||||
site = g.inventaire_api_du_site(ctx)
|
||||
tenant = g.inventaire_api(g.INVENTAIRE_DEFAUT)
|
||||
|
||||
ecarts = []
|
||||
for clef in sorted(set(tenant) & set(site)):
|
||||
if clef in self.ECARTS_ADMIS:
|
||||
continue
|
||||
a, b = type(tenant[clef]).__name__, type(site[clef]).__name__
|
||||
if a != b:
|
||||
ecarts.append(f"{clef} : locataire={a}, site={b}")
|
||||
self.assertEqual(ecarts, [],
|
||||
"des clefs changent de type selon la console :\n - "
|
||||
+ "\n - ".join(ecarts))
|
||||
|
||||
def test_un_ecart_admis_doit_encore_exister(self):
|
||||
"""Une exemption qui survit a son objet est un mensonge tranquille."""
|
||||
tenant = g.inventaire_api(g.INVENTAIRE_DEFAUT)
|
||||
for clef in self.ECARTS_ADMIS:
|
||||
self.assertTrue(clef in tenant or clef == "contexte",
|
||||
f"{clef} n'existe plus : son exemption n'a plus d'objet")
|
||||
|
||||
|
||||
class TestRenduGui(unittest.TestCase):
|
||||
"""Chaque vue du GUI se dessine sans lever."""
|
||||
|
||||
|
|
@ -156,6 +225,7 @@ class TestRenduGui(unittest.TestCase):
|
|||
html = g.HTML.replace("__JETON__", "TEST")
|
||||
blocs = re.findall(r"<script>(.*?)</script>", html, re.S)
|
||||
assert blocs, "aucun bloc <script>"
|
||||
cls.blocs = blocs
|
||||
cls.js = BANC + "\n".join(blocs) + APRES
|
||||
|
||||
def _jouer(self, js: str, payload: dict) -> list:
|
||||
|
|
@ -179,6 +249,45 @@ class TestRenduGui(unittest.TestCase):
|
|||
erreurs = self._jouer(self.js, payload)
|
||||
self.assertEqual(erreurs, [], "des vues ont leve :\n - " + "\n - ".join(erreurs))
|
||||
|
||||
def test_une_console_de_site_montre_ses_machines(self):
|
||||
"""UN SITE A DOUZE MACHINES ET SA CONSOLE N'EN MONTRAIT AUCUNE (2026-09-20).
|
||||
|
||||
`inventaire_api_du_site` sert volontairement `serveurs: {}` — un site declare ses
|
||||
machines dans SON plan, avec `ip` et `vmid` ecrits, la ou un locataire les derive.
|
||||
Mais la vue d'accueil tirait de ce registre vide le message d'un locataire sans
|
||||
serveurs (« + Serveur », `make serveurs-bootstrap`), et les machines servies dans
|
||||
`hotes` n'etaient dessinees nulle part.
|
||||
|
||||
C'est le defaut du 2026-09-16 par une autre porte : la premiere fois l'inventaire
|
||||
etait vide ; cette fois il est PLEIN et c'est la vue qui regarde au mauvais endroit.
|
||||
"""
|
||||
if not g.FICHIER_UNDERLAY.exists():
|
||||
self.skipTest("aucune fabric montee : pas de console de site a simuler")
|
||||
ctx = dict(g.contexte())
|
||||
ctx.update(portee="site", ecosysteme=None,
|
||||
peut={"editer_le_plan": False, "configurer": False,
|
||||
"materialiser": True, "fabric": True})
|
||||
payload = g.inventaire_api_du_site(ctx)
|
||||
if not payload["hotes"]:
|
||||
self.skipTest("l'inventaire du site ne rend aucune machine ici")
|
||||
|
||||
js = BANC + "\n".join(self.blocs) + APRES_SITE
|
||||
r = self._jouer(js, payload)
|
||||
self.assertEqual(r["erreurs"], [], "la vue a leve :\n - " + "\n - ".join(r["erreurs"]))
|
||||
self.assertEqual(r["hotes"], len(payload["hotes"]))
|
||||
|
||||
noms = [h["nom"] for h in payload["hotes"]]
|
||||
manquants = [n for n in noms if n not in r["grille"]]
|
||||
self.assertEqual(manquants, [],
|
||||
f"des machines du site ne sont pas dessinees : {manquants}")
|
||||
# Et surtout : plus le message d'un locataire vide, qui envoyait l'exploitant
|
||||
# vers un amorcage de plan que ce site n'a pas.
|
||||
self.assertNotIn("serveurs-bootstrap", r["grille"])
|
||||
self.assertIn("Machines de", r["grille"])
|
||||
# Le panneau droit dit ce que cette console PEUT, au lieu de rester muet.
|
||||
self.assertIn("matérialise", r["detail"])
|
||||
self.assertIn(noms[0], r["detailChoisi"])
|
||||
|
||||
def test_controle_negatif_reference_absente(self):
|
||||
"""LE CONTROLE. Une lecture d'identifiant inexistant DOIT faire echouer le banc.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue