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
This commit is contained in:
parent
c0ea0c8e71
commit
2887b57f0c
9 changed files with 383 additions and 90 deletions
61
CHANGELOG.md
61
CHANGELOG.md
|
|
@ -1,5 +1,66 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-08 (4) — Les SIX registres ont un formulaire genere
|
||||
|
||||
**62 preuves. `make prouver` : CONFORME, 61 OK, 0 echec, 1 saute.**
|
||||
`CHAMPS_ECRITS_A_LA_MAIN` est **vide** : plus un seul champ recopie a la main.
|
||||
|
||||
### L'epreuve qui compte
|
||||
|
||||
Ouvrir chaque vue et enregistrer **sans rien toucher** doit renvoyer au serveur
|
||||
exactement le plan qu'on vient de lire. C'est ce qui separe un formulaire genere d'un
|
||||
formulaire qui a l'air genere : un champ visible a l'ecran et perdu en silence a
|
||||
l'enregistrement serait le pire des deux mondes.
|
||||
|
||||
/api/serveurs 14 entite(s) IDENTIQUE
|
||||
/api/applications 25 entite(s) IDENTIQUE
|
||||
/api/domaines 2 entite(s) IDENTIQUE
|
||||
/api/bases 4 entite(s) IDENTIQUE
|
||||
|
||||
`test_rendu_gui.py` le mesure desormais a chaque `make prouver`.
|
||||
|
||||
### Trois defauts trouves en chemin
|
||||
|
||||
**Le formulaire annoncait des defauts inventes.** « 2048 » pour la memoire, « 2 » pour les
|
||||
coeurs, « 16G » pour le disque. Il n'existe aucun defaut fixe : `deriver_ressources`
|
||||
calcule la taille depuis les ROLES que l'hote porte — 1024 Mo et 1 coeur pour
|
||||
`infra-pki-01`, 5632 et 4 pour `collab-01`. Un repere faux est pire qu'aucun : il fait
|
||||
croire qu'on connait la valeur. Le schema nomme maintenant le champ derive
|
||||
(`x-defaut-derive`), et le formulaire affiche la valeur REELLE de cet hote. De meme,
|
||||
l'option vide d'un `<select>` dit le defaut qu'elle produira : « (défaut : asgard) ».
|
||||
|
||||
**Une SECONDE occurrence du defaut d'hier dormait.** `sourceDeValeurs` lisait encore
|
||||
`data.nomenclature` — le meme `data` qui n'existe pas. Elle n'avait jamais leve parce que
|
||||
la vue Serveurs, seule a emprunter cette source, avait encore un formulaire ecrit a la
|
||||
main. Elle a leve **a la seconde** ou le generateur l'a prise, et c'est le banc de rendu
|
||||
qui l'a dit.
|
||||
|
||||
Le banc ne voit que les chemins VIVANTS. `verifier_gui.py` fait donc desormais une
|
||||
verification STATIQUE : une fonction qui lit `data.` sans le declarer ni le recevoir est
|
||||
refusee. Elle voit aussi ce qui dort. Son premier essai a signale `dessinerReseau()` a
|
||||
tort — `data` y est declare en second declarateur d'un `const` multiple ; corrige, parce
|
||||
qu'un faux positif dans une garde finit toujours par la faire desactiver.
|
||||
|
||||
**La validation client s'accrochait a `data-v`**, pose a la main sur trois champs. Le
|
||||
formulaire genere l'aurait perdu, et `querySelectorAll` aurait rendu une liste vide : la
|
||||
validation serait passee au vert sur ZERO champ. Le generateur marque maintenant chaque
|
||||
controle (`data-champ`), et la sauvegarde REFUSE si elle n'inspecte aucun champ — un
|
||||
controle qui ne trouve rien ne dit pas « tout va bien ».
|
||||
|
||||
### Deux champs gardent leur editeur, et le schema le dit
|
||||
|
||||
`integrations` et `liens` ne sont PAS generes, volontairement. La matrice des integrations
|
||||
montre aussi les universelles (non decochables) et les exemptions `sauf_role` : un champ
|
||||
texte genere ferait lire un plan silencieux comme « cet hote n'est pas supervise »,
|
||||
l'inverse exact de la politique. L'editeur de liens contraint le role a ce que le groupe
|
||||
porteur accepte (`meta/liens.yml`). Le schema porte donc `x-editeur`, et le generateur
|
||||
s'efface — au lieu de remplacer un editeur qui en sait plus que lui.
|
||||
|
||||
### La limite
|
||||
|
||||
Je n'ai toujours pas ouvert ces pages dans un navigateur. Le banc prouve qu'elles rendent
|
||||
sans lever et qu'un aller-retour ne perd rien ; il ne dit rien de leur lisibilite.
|
||||
|
||||
## 2026-09-08 (3) — Quarante lignes de memoire que chaque « Sauvegarder » detruisait
|
||||
|
||||
**62 preuves (P01-P62). `make prouver` : CONFORME, 61 OK, 0 echec, 1 saute.**
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
| 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). |
|
||||
| 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 : 40 groupes classes, aucun cycle, aucune arete en arriere. |
|
||||
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 38 rôles, 99 flux, schéma + matrice OK. |
|
||||
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
|
||||
|
|
|
|||
|
|
@ -31,32 +31,40 @@
|
|||
"description": "Seulement les facultatives. Les universelles viennent du role et sont refusees ici.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"x-editeur": "matrice"
|
||||
},
|
||||
"noeud": {
|
||||
"type": "string",
|
||||
"title": "Nœud Proxmox",
|
||||
"description": "Surcharge le defaut de `make config`."
|
||||
"description": "Surcharge le defaut de `make config`.",
|
||||
"x-defaut-intrant": "proxmox_clone_noeud",
|
||||
"x-source-valeurs": "intrants.proxmox_noeuds"
|
||||
},
|
||||
"stockage": {
|
||||
"type": "string",
|
||||
"title": "Stockage",
|
||||
"description": "Surcharge le defaut."
|
||||
"description": "Surcharge le defaut de `make config`.",
|
||||
"x-defaut-intrant": "proxmox_clone_stockage",
|
||||
"x-source-valeurs": "intrants.proxmox_stockages"
|
||||
},
|
||||
"disque": {
|
||||
"type": "string",
|
||||
"title": "Disque",
|
||||
"description": "Ex. `32G`. Vide = derive des empreintes des roles."
|
||||
"description": "Ex. `32G`. Vide = derive des empreintes des roles.",
|
||||
"x-defaut-derive": "disque_taille"
|
||||
},
|
||||
"memoire": {
|
||||
"type": "integer",
|
||||
"title": "Mémoire (Mo)",
|
||||
"description": "Vide = derive des empreintes des roles."
|
||||
"description": "Vide = derive des empreintes des roles.",
|
||||
"x-defaut-derive": "memoire"
|
||||
},
|
||||
"coeurs": {
|
||||
"type": "integer",
|
||||
"title": "Cœurs",
|
||||
"description": "Vide = derive des empreintes des roles."
|
||||
"description": "Vide = derive des empreintes des roles.",
|
||||
"x-defaut-derive": "coeurs"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
|
@ -132,7 +140,8 @@
|
|||
"role",
|
||||
"vers"
|
||||
]
|
||||
}
|
||||
},
|
||||
"x-editeur": "liens"
|
||||
},
|
||||
"websocket": {
|
||||
"type": "boolean",
|
||||
|
|
|
|||
|
|
@ -119,6 +119,12 @@ c'est le feu vert pour appliquer.
|
|||
Édition → **Sauvegarder** (écrit le registre) → **Appliquer le plan** (régénère
|
||||
`hosts.yml`). L'écriture directe de l'inventaire est refusée (409).
|
||||
|
||||
**Les formulaires des six registres sont générés** depuis `docs/audit/schema-plan.json`
|
||||
(`make schema`), dérivé des constantes du moteur. La sauvegarde en dérive aussi : un
|
||||
champ ajouté au plan apparaît à l'écran *et* arrive au fichier. Deux exceptions
|
||||
déclarées au schéma (`x-editeur`) : la matrice des intégrations et l'éditeur de liens
|
||||
gardent leur éditeur propre, plus riche que ce que le schéma sait dire.
|
||||
|
||||
### Via le CLI / `make`
|
||||
Chaque registre a son script miroir et ses cibles `make` :
|
||||
|
||||
|
|
|
|||
|
|
@ -558,25 +558,22 @@ def intrants_courants() -> dict:
|
|||
# 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.
|
||||
#
|
||||
# `bases_donnees` en est SORTIE la premiere : son formulaire est desormais GENERE depuis
|
||||
# `docs/audit/schema-plan.json` (`make schema`), donc la liste s'en deduit au lieu d'etre
|
||||
# recopiee. Les registres restants gardent leur declaration manuelle jusqu'a ce que leur
|
||||
# formulaire passe au generateur — on ne bascule pas six vues d'un coup.
|
||||
# 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`.
|
||||
#
|
||||
# P19 lit `champs_ecrits_par_gui()` et non plus cette constante : le jour ou la table sera
|
||||
# vide, elle gardera un mecanisme au lieu d'une liste.
|
||||
CHAMPS_ECRITS_A_LA_MAIN = {
|
||||
"serveurs": {"fonction", "etat", "noeud", "stockage", "disque", "memoire",
|
||||
"coeurs", "integrations"},
|
||||
"applications": {"groupe", "hote", "port", "requiert", "expose", "liens", "websocket"},
|
||||
# nomenclature : plus AUCUN champ a la main non plus — elle est passee au generateur
|
||||
# le 2026-09-08, et c'etait le dernier registre que le GUI ne savait pas ecrire du
|
||||
# tout. Ajouter une fonction n'exige plus d'editer le YAML.
|
||||
"nomenclature": set(),
|
||||
}
|
||||
# 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]] = {}
|
||||
|
||||
# Les registres dont le formulaire est GENERE : leurs champs viennent du schema.
|
||||
REGISTRES_GENERES = {"bases_donnees", "nomenclature", "serveurs_bd", "domaines_publics"}
|
||||
REGISTRES_GENERES = {"bases_donnees", "nomenclature", "serveurs_bd",
|
||||
"domaines_publics", "serveurs", "applications"}
|
||||
|
||||
|
||||
def champs_ecrits_par_gui() -> dict:
|
||||
|
|
@ -2440,10 +2437,10 @@ HTML = r"""<!doctype html>
|
|||
|
||||
function chargerApplications(data) {
|
||||
const a = (data.applications || {}).applications || {};
|
||||
applications = Object.entries(a).map(([id, x]) => ({
|
||||
id, groupe: x.groupe || '', hote: x.hote || '', port: x.port || '',
|
||||
requiert: (x.requiert || []).join(', '), expose: (x.expose || []).join(', '),
|
||||
websocket: !!x.websocket,
|
||||
// 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, {
|
||||
liens: (x.liens || []).map(l => ({vers: l.vers || '', role: l.role || ''}))}));
|
||||
applicationsModifie = false;
|
||||
}
|
||||
|
|
@ -2465,7 +2462,10 @@ HTML = r"""<!doctype html>
|
|||
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);
|
||||
if (nom === 'nomenclature.fonctions') return Object.keys((data.nomenclature || {}).fonctions || {});
|
||||
// `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) || {});
|
||||
// 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.
|
||||
|
|
@ -2473,6 +2473,11 @@ HTML = r"""<!doctype html>
|
|||
// 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'));
|
||||
// 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 : [];
|
||||
}
|
||||
if (nom === 'nomenclature.categories') {
|
||||
const cats = (nomEdit.categories || {});
|
||||
return Object.keys(cats).map(k => ({v: k, t: k + ' — ' + ((cats[k] || {}).libelle || '?')}));
|
||||
|
|
@ -2481,6 +2486,11 @@ HTML = r"""<!doctype html>
|
|||
}
|
||||
|
||||
function champDepuisSchema(cle, spec, valeur, entite, surChangement) {
|
||||
// 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 '';
|
||||
const titre = echapper(spec.title || cle);
|
||||
const aide = spec.description ? ` title="${echapper(spec.description)}"` : '';
|
||||
// Marqueur discret : le schema SAIT ce qui est requis, le formulaire ecrit a la
|
||||
|
|
@ -2488,6 +2498,11 @@ HTML = r"""<!doctype html>
|
|||
// collait au libelle et se lisait « Base requis ».
|
||||
const req = (spec['x-requis'] ? '<abbr class="dsn" title="Champ requis" style="text-decoration:none"> *</abbr>' : '');
|
||||
const appel = (expr) => `${surChangement}('${cle}', ${expr})`;
|
||||
// `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)}"`;
|
||||
|
||||
// Liste fermee : soit fixe (enum), soit alimentee a l'execution, soit dependante
|
||||
// d'un autre champ de la meme entite.
|
||||
|
|
@ -2501,13 +2516,18 @@ HTML = r"""<!doctype html>
|
|||
}
|
||||
|
||||
if (options) {
|
||||
// Une option est soit une valeur nue, soit {v: valeur ecrite, t: texte affiche}.
|
||||
const opts = ['<option value="">—</option>'].concat(options.map(o => {
|
||||
// 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 => {
|
||||
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('');
|
||||
return `<label class="champ"${aide}><span>${titre}${req}</span><select onchange="${appel('this.value')}">${opts}</select></label>`;
|
||||
return `<label class="champ"${aide}><span>${titre}${req}</span><select${marque} onchange="${appel('this.value')}">${opts}</select></label>`;
|
||||
}
|
||||
if (spec.type === 'object' && spec.properties) {
|
||||
// BLOC FIXE (`sous_champs` au schema) : ses cases une fois, sans « ajouter ».
|
||||
|
|
@ -2522,10 +2542,20 @@ HTML = r"""<!doctype html>
|
|||
+ `<div class="grille">${cases}</div></div>`;
|
||||
}
|
||||
if (spec.type === 'boolean') {
|
||||
return `<label class="champ"${aide}><span>${titre}${req}</span><input type="checkbox"${valeur ? ' checked' : ''} onchange="${appel('this.checked')}"></label>`;
|
||||
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])}"`;
|
||||
}
|
||||
if (spec.type === 'integer') {
|
||||
return `<label class="champ"${aide}><span>${titre}${req}</span><input type="number" value="${echapper(valeur == null ? '' : valeur)}" oninput="${appel('this.value')}"></label>`;
|
||||
return `<label class="champ"${aide}><span>${titre}${req}</span><input type="number"${marque}${repere} value="${echapper(valeur == null ? '' : valeur)}" oninput="${appel('this.value')}"></label>`;
|
||||
}
|
||||
if (spec.type === 'array' && (spec.items || {}).properties) {
|
||||
return listeDepuisSchema(cle, spec, valeur, surChangement);
|
||||
|
|
@ -2534,12 +2564,12 @@ HTML = r"""<!doctype html>
|
|||
// 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(', ') : '';
|
||||
return `<label class="champ"${aide}><span>${titre}${req}</span><input value="${echapper(v)}" placeholder="a, b" oninput="${appel('this.value.split(\',\').map(x=>x.trim()).filter(Boolean)')}"></label>`;
|
||||
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>`;
|
||||
}
|
||||
return `<label class="champ"${aide}><span>${titre}${req}</span><input value="${echapper(valeur == null ? '' : valeur)}" oninput="${appel('this.value')}"></label>`;
|
||||
return `<label class="champ"${aide}><span>${titre}${req}</span><input${marque}${repere} value="${echapper(valeur == null ? '' : valeur)}" oninput="${appel('this.value')}"></label>`;
|
||||
}
|
||||
|
||||
function formulaireDepuisSchema(nomRegistre, entite, surChangement, cleCourante, surChangementCle) {
|
||||
function formulaireDepuisSchema(nomRegistre, entite, surChangement, cleCourante, surChangementCle, nomClef) {
|
||||
const reg = schemaPlan[nomRegistre] || null;
|
||||
if (!reg) return '<div class="vide">Aucun schéma pour ce registre — lancer <code>make schema</code>.</div>';
|
||||
const props = (reg.entite || {}).properties || {};
|
||||
|
|
@ -2548,7 +2578,7 @@ HTML = r"""<!doctype html>
|
|||
if (reg['x-clef'] && surChangementCle) {
|
||||
const k = reg['x-clef'];
|
||||
morceaux.push(`<label class="champ" title="${echapper(k.description || '')}"><span>${echapper(k.title)}</span>` +
|
||||
`<input value="${echapper(cleCourante == null ? '' : cleCourante)}" oninput="(${surChangementCle})(this.value)"></label>`);
|
||||
`<input data-champ="${echapper(nomClef || 'cle')}" value="${echapper(cleCourante == null ? '' : cleCourante)}" oninput="(${surChangementCle})(this.value)"></label>`);
|
||||
}
|
||||
Object.keys(props).forEach(cle => {
|
||||
const spec = Object.assign({}, props[cle], {'x-requis': requis.has(cle)});
|
||||
|
|
@ -2748,7 +2778,7 @@ HTML = r"""<!doctype html>
|
|||
function choisirApp(i) { selApp = i; dessiner(); }
|
||||
function definirApplication(champ, v) {
|
||||
const a = applications[selApp]; if (!a) return;
|
||||
a[champ] = v; marquerApplicationsModifie();
|
||||
poserChemin(a, champ, v); marquerApplicationsModifie();
|
||||
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'); } }
|
||||
else if (champ === 'groupe') {
|
||||
const ok = Object.keys(liensAcceptes[a.groupe] || {});
|
||||
|
|
@ -2841,8 +2871,6 @@ HTML = r"""<!doctype html>
|
|||
return;
|
||||
}
|
||||
const a = applications[selApp];
|
||||
const optGroupe = groupes.map(g => `<option value="${echapper(g)}" ${g === a.groupe ? 'selected' : ''}>${echapper(g)}</option>`).join('');
|
||||
const optHote = '<option value="">—</option>' + hotes.map(h => h.nom).filter(Boolean).map(n => `<option value="${echapper(n)}" ${n === a.hote ? 'selected' : ''}>${echapper(n)}</option>`).join('');
|
||||
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>`
|
||||
|
|
@ -2851,15 +2879,7 @@ HTML = r"""<!doctype html>
|
|||
<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>
|
||||
<div class="grille">
|
||||
<label class="champ"><span>Identifiant</span><input value="${echapper(a.id)}" placeholder="boutique" oninput="definirApplication('id', this.value)"></label>
|
||||
<label class="champ"><span>Groupe (rôle)</span><select onchange="definirApplication('groupe', this.value)">${optGroupe}</select></label>
|
||||
<label class="champ"><span>Hôte (VM)</span><select onchange="definirApplication('hote', this.value)">${optHote}</select></label>
|
||||
<label class="champ"><span>Port</span><input type="number" value="${echapper(a.port)}" placeholder="3000" oninput="definirApplication('port', this.value)"></label>
|
||||
<label class="champ"><span>WebSocket</span><span style="display:flex;align-items:center;gap:8px;height:2.1rem"><input type="checkbox" ${a.websocket ? 'checked' : ''} onchange="definirApplication('websocket', this.checked)"><span class="hint-dep">vhost compatible WebSocket (Collabora…)</span></span></label>
|
||||
<label class="champ" style="grid-column:1/-1"><span>Requiert (applis)</span><input value="${echapper(a.requiert)}" placeholder="pg-principal, …" oninput="definirApplication('requiert', this.value)"></label>
|
||||
<label class="champ" style="grid-column:1/-1"><span>Expose (FQDN)</span><input value="${echapper(a.expose)}" placeholder="forge.exemple.ca" oninput="definirApplication('expose', this.value)"></label>
|
||||
</div>
|
||||
${formulaireDepuisSchema('applications', a, 'definirApplication', a.id, "definirApplication.bind(null,'id')", 'id')}
|
||||
<div class="section-tete" style="margin:12px 2px 4px">Liens (bindings)
|
||||
<span class="compte">${(a.liens || []).length}</span></div>
|
||||
${blocLiens(a)}
|
||||
|
|
@ -2876,21 +2896,41 @@ HTML = r"""<!doctype html>
|
|||
</article>`;
|
||||
}
|
||||
|
||||
function listeDepuisTexte(v) { return String(v || '').split(',').map(s => s.trim()).filter(Boolean); }
|
||||
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);
|
||||
}
|
||||
|
||||
async function sauvegarderApplications() {
|
||||
const apps = {};
|
||||
const propsA = ((schemaPlan.applications || {}).entite || {}).properties || {};
|
||||
applications.forEach(a => {
|
||||
if (!String(a.id).trim()) return;
|
||||
const o = {groupe: a.groupe, hote: a.hote};
|
||||
if (String(a.port).trim()) o.port = parseInt(a.port, 10) || a.port;
|
||||
const req = listeDepuisTexte(a.requiert); if (req.length) o.requiert = req;
|
||||
const exp = listeDepuisTexte(a.expose); if (exp.length) o.expose = exp;
|
||||
if (a.websocket) o.websocket = true;
|
||||
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;
|
||||
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 || '';
|
||||
apps[a.id] = o;
|
||||
});
|
||||
const rep = await fetch('/api/applications', {
|
||||
|
|
@ -2904,7 +2944,7 @@ HTML = r"""<!doctype html>
|
|||
}
|
||||
|
||||
function chargerServeurs(data) {
|
||||
serveurs = (data.serveurs || []).map(s => ({...s, integrations: (s.integrations || []).join(', ')}));
|
||||
serveurs = (data.serveurs || []).map(s => ({...s, integrations: s.integrations || []}));
|
||||
serveursModifie = false;
|
||||
}
|
||||
|
||||
|
|
@ -2913,7 +2953,7 @@ HTML = r"""<!doctype html>
|
|||
function choisirServeur(i) { selServeur = i; dessiner(); }
|
||||
function definirServeur(champ, v) {
|
||||
const s = serveurs[selServeur]; if (!s) return;
|
||||
s[champ] = v; marquerServeursModifie();
|
||||
poserChemin(s, champ, v); marquerServeursModifie();
|
||||
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();
|
||||
}
|
||||
|
|
@ -2921,7 +2961,7 @@ HTML = r"""<!doctype html>
|
|||
const s = serveurs[selServeur]; 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();
|
||||
s.integrations = Array.from(set); marquerServeursModifie();
|
||||
}
|
||||
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: ''});
|
||||
|
|
@ -3032,15 +3072,8 @@ HTML = r"""<!doctype html>
|
|||
<div class="stats">${tuile(s.vmid, 'VMID')}${tuile(s.adresse_ip, 'IP')}${tuile(s.vlan, 'VLAN')}</div>
|
||||
<div class="dsn" style="padding:5px 2px"><b>Plan</b> : ${echapper(s.etat)} · <b>Réel</b> : ${sondes[s.nom] === true ? '<span class="pt actif"></span> vivante' + (s.adresse_ip ? ' (' + echapper(s.adresse_ip) + ')' : '') : sondes[s.nom] === false ? '<span class="pt bloq"></span> injoignable' : '<span class="pt plan"></span> non sondé'}</div>
|
||||
<div class="section-tete" style="margin:4px 2px 2px">Identité — saisie du plan (surcharge les défauts)</div>
|
||||
${formulaireDepuisSchema('serveurs', s, 'definirServeur', s.nom, "definirServeur.bind(null,'nom')", 'nom')}
|
||||
<div class="grille">
|
||||
<label class="champ"><span>Serveur (fonction-NN)</span><input data-v="nom" value="${echapper(s.nom)}" placeholder="web-frontal-03" oninput="definirServeur('nom', this.value)"></label>
|
||||
<label class="champ"><span>Fonction</span><select onchange="definirServeur('fonction', this.value)">${optFonction}</select></label>
|
||||
<label class="champ"><span>État</span><select onchange="definirServeur('etat', this.value)">${optEtat}</select></label>
|
||||
<label class="champ"><span>Nœud</span><select onchange="definirServeur('noeud', this.value)">${optDeListe(vi.proxmox_noeuds, s.noeud, libDef(vi.proxmox_clone_noeud))}</select></label>
|
||||
<label class="champ"><span>Stockage</span><select onchange="definirServeur('stockage', this.value)">${optDeListe(vi.proxmox_stockages, s.stockage, libDef(vi.proxmox_clone_stockage))}</select></label>
|
||||
<label class="champ"><span>Disque</span><input data-v="disque" value="${echapper(s.disque)}" placeholder="16G" oninput="definirServeur('disque', this.value)"></label>
|
||||
<label class="champ"><span>Mémoire</span><input data-v="memoire" type="number" value="${echapper(s.memoire)}" placeholder="2048" oninput="definirServeur('memoire', this.value)"></label>
|
||||
<label class="champ"><span>Cœurs</span><input data-v="coeurs" type="number" value="${echapper(s.coeurs)}" placeholder="2" oninput="definirServeur('coeurs', this.value)"></label>
|
||||
<label class="champ" style="grid-column:1/-1"><span>Intégrations (rôles client)</span><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></label>
|
||||
</div>
|
||||
<div class="section-tete" style="margin:12px 2px 2px">Applications ici</div>${appsHtml}
|
||||
|
|
@ -3061,25 +3094,40 @@ HTML = r"""<!doctype html>
|
|||
return true;
|
||||
}
|
||||
function validerChampServeur(el) {
|
||||
const ok = serveurValeurValide(el.getAttribute('data-v'), el.value);
|
||||
const ok = serveurValeurValide(el.getAttribute('data-champ'), el.value);
|
||||
el.classList.toggle('invalide', !ok);
|
||||
return ok;
|
||||
}
|
||||
|
||||
async function sauvegarderServeurs() {
|
||||
const invalides = Array.from(document.querySelectorAll('#detail [data-v]')).filter(el => !validerChampServeur(el));
|
||||
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; }
|
||||
if (invalides.length) {
|
||||
message('Corrige les ' + invalides.length + ' champ(s) en rouge avant de sauvegarder.', 'erreur');
|
||||
invalides[0].focus();
|
||||
return;
|
||||
}
|
||||
const srv = {};
|
||||
const propsS = ((schemaPlan.serveurs || {}).entite || {}).properties || {};
|
||||
serveurs.forEach(s => {
|
||||
if (!String(s.nom).trim()) return;
|
||||
const o = {fonction: s.fonction, etat: s.etat || 'planifie'};
|
||||
['noeud', 'stockage', 'disque'].forEach(k => { if (String(s[k] || '').trim()) o[k] = s[k]; });
|
||||
['memoire', 'coeurs'].forEach(k => { if (String(s[k] || '').trim()) o[k] = parseInt(s[k], 10) || s[k]; });
|
||||
const integ = listeDepuisTexte(s.integrations); if (integ.length) o.integrations = integ;
|
||||
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';
|
||||
srv[s.nom] = o;
|
||||
});
|
||||
const rep = await fetch('/api/serveurs', {
|
||||
|
|
|
|||
|
|
@ -69,17 +69,36 @@ REGISTRES: dict = {
|
|||
"etat": {"type": "string", "enum": sorted(ETATS_SERVEUR),
|
||||
"libelle": "État",
|
||||
"aide": "`planifie` = decrit mais pas deploye ; `actif` = joignable par Ansible."},
|
||||
# UN EDITEUR PROPRE, ET C'EST VOULU. La matrice de cases a cocher montre
|
||||
# aussi les integrations UNIVERSELLES (non decochables) et les exemptions
|
||||
# `sauf_role`. Un champ texte genere serait une regression : un plan
|
||||
# silencieux se lirait « cet hote n'est pas supervise », l'inverse exact de
|
||||
# la politique. Le schema le DIT, au lieu de laisser le generateur l'ecraser.
|
||||
"integrations": {"type": "array", "items": {"type": "string"},
|
||||
"libelle": "Intégrations facultatives",
|
||||
"libelle": "Intégrations facultatives", "editeur": "matrice",
|
||||
"aide": "Seulement les facultatives. Les universelles viennent du role et sont refusees ici."},
|
||||
"noeud": {"type": "string", "libelle": "Nœud Proxmox",
|
||||
"source_valeurs": "intrants.proxmox_noeuds",
|
||||
"defaut_intrant": "proxmox_clone_noeud",
|
||||
"aide": "Surcharge le defaut de `make config`."},
|
||||
"stockage": {"type": "string", "libelle": "Stockage", "aide": "Surcharge le defaut."},
|
||||
"stockage": {"type": "string", "libelle": "Stockage",
|
||||
"source_valeurs": "intrants.proxmox_stockages",
|
||||
"defaut_intrant": "proxmox_clone_stockage",
|
||||
"aide": "Surcharge le defaut de `make config`."},
|
||||
# `defaut_derive` NOMME LE CHAMP DE L'INVENTAIRE qui porte la valeur reellement
|
||||
# derivee pour CET hote. Le formulaire ecrit a la main annoncait « 2048 » et
|
||||
# « 2 » en repere de saisie : des constantes inventees. Il n'existe aucun
|
||||
# defaut fixe — `deriver_ressources` calcule la taille depuis les ROLES que
|
||||
# l'hote porte (5632 Mo et 4 coeurs pour `collab-01`). Un repere faux est pire
|
||||
# qu'aucun : il fait croire qu'on connait la valeur.
|
||||
"disque": {"type": "string", "libelle": "Disque",
|
||||
"defaut_derive": "disque_taille",
|
||||
"aide": "Ex. `32G`. Vide = derive des empreintes des roles."},
|
||||
"memoire": {"type": "integer", "libelle": "Mémoire (Mo)",
|
||||
"defaut_derive": "memoire",
|
||||
"aide": "Vide = derive des empreintes des roles."},
|
||||
"coeurs": {"type": "integer", "libelle": "Cœurs",
|
||||
"defaut_derive": "coeurs",
|
||||
"aide": "Vide = derive des empreintes des roles."},
|
||||
},
|
||||
},
|
||||
|
|
@ -105,7 +124,11 @@ REGISTRES: dict = {
|
|||
# `items: {type: object}` ne disait rien de la FORME : un formulaire genere
|
||||
# aurait offert « ajouter un lien » sans savoir quelles cases y mettre.
|
||||
# `valider_applications` exige `vers` et `role` — on les nomme.
|
||||
"liens": {"type": "array", "libelle": "Liens (bindings)",
|
||||
# EDITEUR PROPRE lui aussi : il contraint le ROLE du lien a ceux que le
|
||||
# groupe porteur accepte (`roles/<groupe>/meta/liens.yml`) et montre les
|
||||
# variables injectees. Le generateur, lui, offrirait un champ libre — il
|
||||
# saurait moins que l'editeur qu'il remplacerait.
|
||||
"liens": {"type": "array", "libelle": "Liens (bindings)", "editeur": "liens",
|
||||
"aide": "Roles acceptes par le role porteur (meta/liens.yml).",
|
||||
"entrees": {
|
||||
"vers": {"type": "string", "requis": True, "libelle": "Vers",
|
||||
|
|
@ -315,6 +338,11 @@ def construire() -> dict:
|
|||
# Meme intention que `x-source-valeurs`, mais la liste change selon la
|
||||
# valeur d'un autre champ de la meme entite.
|
||||
p["x-source-selon"] = d["source_selon"]
|
||||
for cle_src, cle_dst in (("editeur", "x-editeur"),
|
||||
("defaut_intrant", "x-defaut-intrant"),
|
||||
("defaut_derive", "x-defaut-derive")):
|
||||
if cle_src in d:
|
||||
p[cle_dst] = d[cle_src]
|
||||
if "source_valeurs" in d:
|
||||
# Extension hors JSON Schema : dit au formulaire d'offrir une LISTE
|
||||
# fermee, alimentee a l'execution. C'est ce qui rend l'hote fantome
|
||||
|
|
|
|||
|
|
@ -51,7 +51,23 @@ Elem.prototype.setAttribute = function () {};
|
|||
Elem.prototype.getAttribute = function () { return ''; };
|
||||
Elem.prototype.addEventListener = function () {};
|
||||
Elem.prototype.querySelector = function () { return null; };
|
||||
Elem.prototype.querySelectorAll = function () { return []; };
|
||||
// `querySelectorAll('[data-champ]')` rend vraiment les controles du dernier rendu : le
|
||||
// GUI s'en sert pour valider avant d'enregistrer, et un stub qui rend toujours [] ferait
|
||||
// passer cette validation sur ZERO champ — le banc mesurerait alors moins que rien.
|
||||
Elem.prototype.querySelectorAll = function (sel) {
|
||||
if (!/data-champ/.test(String(sel))) { return []; }
|
||||
var html = this.innerHTML || '';
|
||||
var out = [], re = /<(input|select)([^>]*?)data-champ="([^"]*)"([^>]*)>/g, m;
|
||||
while ((m = re.exec(html)) !== null) {
|
||||
var attrs = m[2] + m[4];
|
||||
var v = /\svalue="([^"]*)"/.exec(attrs);
|
||||
out.push({__champ: m[3], value: v ? v[1] : '',
|
||||
getAttribute: function (n) { return n === 'data-champ' ? this.__champ : null; },
|
||||
classList: {add: function () {}, remove: function () {}, toggle: function () {}},
|
||||
focus: function () {}});
|
||||
}
|
||||
return out;
|
||||
};
|
||||
Elem.prototype.focus = function () {};
|
||||
Elem.prototype.remove = function () {};
|
||||
Elem.prototype.closest = function () { return null; };
|
||||
|
|
@ -62,7 +78,14 @@ var document = {
|
|||
getElementById: function (id) { return __elems[id] || (__elems[id] = new Elem(id)); },
|
||||
createElement: function () { return new Elem('cree'); },
|
||||
querySelector: function () { return null; },
|
||||
querySelectorAll: function () { return []; },
|
||||
// `#detail [data-champ]` : le GUI interroge le DOCUMENT, pas l'element. Un stub qui
|
||||
// rendait [] ici faisait croire que le formulaire n'etait pas marque — et la
|
||||
// sauvegarde des serveurs refusait, silencieusement, dans le banc seul.
|
||||
querySelectorAll: function (sel) {
|
||||
var m = /^#([\w-]+)\s+(.+)$/.exec(String(sel));
|
||||
if (m && __elems[m[1]]) { return __elems[m[1]].querySelectorAll(m[2]); }
|
||||
return [];
|
||||
},
|
||||
addEventListener: function () {},
|
||||
body: new Elem('body'),
|
||||
documentElement: new Elem('html'),
|
||||
|
|
@ -77,10 +100,15 @@ function EventSource() { this.addEventListener = function () {}; this.close = fu
|
|||
function alert() {} function confirm() { return false; } function prompt() { return null; }
|
||||
var localStorage = {getItem: function () { return null; }, setItem: function () {}, removeItem: function () {}};
|
||||
var __PAYLOAD__ = JSON.parse(process.env.PAYLOAD);
|
||||
var __postsCaptures = {};
|
||||
// `fetch` rend la VRAIE reponse de /api/inventaire ; les autres routes rendent un objet
|
||||
// vide plutot que de jeter, pour que les sondes de fond ne masquent pas l'erreur cherchee.
|
||||
function fetch(url) {
|
||||
var corps = String(url).indexOf('/api/inventaire') === 0 ? __PAYLOAD__ : {};
|
||||
function fetch(url, opts) {
|
||||
if (opts && opts.method === 'POST') { __postsCaptures[String(url)] = JSON.parse(opts.body); }
|
||||
// Un POST rend la MEME charge que l'inventaire : le GUI recharge apres sauvegarde, et
|
||||
// lui rendre {} le ferait repartir d'un plan vide — un faux vert, pas un test.
|
||||
var corps = (String(url).indexOf('/api/inventaire') === 0 || (opts && opts.method === 'POST'))
|
||||
? __PAYLOAD__ : {};
|
||||
return Promise.resolve({ok: true, status: 200, json: function () { return Promise.resolve(corps); },
|
||||
text: function () { return Promise.resolve(''); }});
|
||||
}
|
||||
|
|
@ -166,6 +194,47 @@ class TestRenduGui(unittest.TestCase):
|
|||
self.assertTrue(any("donneesQuiNexistentPas" in e for e in erreurs),
|
||||
f"le banc n'a pas vu la reference absente : {erreurs}")
|
||||
|
||||
def test_charger_puis_sauvegarder_ne_change_pas_le_plan(self):
|
||||
"""L'EPREUVE DECISIVE : ouvrir une vue et enregistrer sans rien toucher.
|
||||
|
||||
Les six formulaires sont GENERES, et leur sauvegarde l'est aussi : les champs
|
||||
ecrits sont ceux que le schema declare. C'est ce qui evite le pire des deux
|
||||
mondes — un champ visible a l'ecran et perdu en silence a l'enregistrement.
|
||||
|
||||
Encore faut-il le mesurer. On charge la vraie reponse de l'API, on ouvre chaque
|
||||
vue, on appelle sa sauvegarde, on intercepte le POST, et on compare la charge au
|
||||
plan REEL lu par le moteur. Aucune entite perdue, aucune en trop, aucun champ
|
||||
different : sinon un simple aller-retour appauvrirait le plan.
|
||||
"""
|
||||
js = self.js.replace(APRES, r"""
|
||||
charger()
|
||||
.then(function () { setVue('serveurs'); choisirServeur(0); return sauvegarderServeurs(); })
|
||||
.then(function () { setVue('applications'); if (applications.length) ouvrirApp(applications[0].id); return sauvegarderApplications(); })
|
||||
.then(function () { setVue('domaines'); if (domaines.length) choisirDomaine(0); return sauvegarderDomaines(); })
|
||||
.then(function () { setVue('bases'); return sauvegarderBases(); })
|
||||
.then(function () { console.log('__RESULTAT__' + JSON.stringify(__postsCaptures)); })
|
||||
.catch(function (e) { console.log('__RESULTAT__' + JSON.stringify({erreur: String(e && e.stack || e)})); });
|
||||
""")
|
||||
payload = g.inventaire_api(g.INVENTAIRE_DEFAUT)
|
||||
posts = self._jouer(js, payload)
|
||||
self.assertNotIn("erreur", posts, posts.get("erreur", ""))
|
||||
|
||||
attendu = {
|
||||
"/api/serveurs": (g.charger_serveurs(g.FICHIER_SERVEURS), "serveurs"),
|
||||
"/api/applications": (g.charger_applications(g.FICHIER_APPLICATIONS), "applications"),
|
||||
"/api/domaines": (g.charger_domaines(g.FICHIER_DOMAINES), "domaines_publics"),
|
||||
"/api/bases": (g.charger_bases_donnees(g.FICHIER_BASES), "bases_donnees"),
|
||||
}
|
||||
for url, (plan, racine) in attendu.items():
|
||||
with self.subTest(route=url):
|
||||
envoye = (posts.get(url) or {}).get(racine)
|
||||
self.assertIsNotNone(envoye, f"{url} n'a pas ete appele — la sauvegarde a-t-elle refuse ?")
|
||||
reel = plan.get(racine) or {}
|
||||
self.assertEqual(sorted(envoye), sorted(reel), "entites perdues ou ajoutees")
|
||||
for cle in sorted(reel):
|
||||
self.assertEqual(envoye[cle], reel[cle],
|
||||
f"{racine}.{cle} : l'aller-retour a change l'entite")
|
||||
|
||||
def test_le_schema_est_bien_servi(self):
|
||||
"""Le formulaire ne peut rien generer si l'API n'envoie pas le schema."""
|
||||
payload = g.inventaire_api(g.INVENTAIRE_DEFAUT)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,60 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|||
import inventory_gui as g # noqa: E402
|
||||
|
||||
|
||||
# Les noms qui RESSEMBLENT a un global sans en etre un. `data` est le cas fondateur :
|
||||
# chaque `charger*`/`sauvegarder*` en declare un LOCAL, si bien que le nom se lit partout
|
||||
# comme s'il etait disponible — et ne l'est pas.
|
||||
NON_GLOBALES = ("data",)
|
||||
|
||||
# Une fonction de premier niveau du <script> : ` function nom(args) {` ou sa variante
|
||||
# `async`. Le GUI les ecrit toutes a quatre espaces d'indentation.
|
||||
_FONCTION = re.compile(r"^ {4}(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)", re.M)
|
||||
|
||||
|
||||
def variables_orphelines(js: str) -> list[str]:
|
||||
"""Fonctions qui LISENT un nom surveille sans le declarer ni le recevoir.
|
||||
|
||||
POURQUOI CETTE GARDE (2026-09-08). `node --check` ne voit que la syntaxe. Le
|
||||
formulaire genere des bases lisait `data.schema` — or `data` n'est pas un global de
|
||||
cette page : c'est une const locale de `charger()`. Syntaxe parfaite,
|
||||
`ReferenceError` a l'ouverture, et personne pour le dire.
|
||||
|
||||
`test_rendu_gui.py` l'attrape maintenant en EXECUTANT les vues — mais seulement les
|
||||
chemins vivants. La preuve en est qu'une SECONDE occurrence dormait dans
|
||||
`sourceDeValeurs` : la vue Serveurs, seule a emprunter cette source, avait encore un
|
||||
formulaire ecrit a la main. Elle a leve a la seconde ou le generateur l'a prise.
|
||||
|
||||
Cette verification-ci est statique : elle voit aussi ce qui dort.
|
||||
|
||||
Sa limite, dite franchement : elle raisonne par fonction de premier niveau et par
|
||||
motif, pas par portee lexicale. Une fonction imbriquee qui hériterait du `data` de
|
||||
sa parente serait signalee a tort — on la nommerait alors autrement, ce qui est de
|
||||
toute facon plus lisible.
|
||||
"""
|
||||
orphelins = []
|
||||
bornes = [(m.start(), m.group(1), m.group(2)) for m in _FONCTION.finditer(js)]
|
||||
for i, (debut, nom, args) in enumerate(bornes):
|
||||
fin = bornes[i + 1][0] if i + 1 < len(bornes) else len(js)
|
||||
corps = js[debut:fin]
|
||||
parametres = {a.strip().split("=")[0].strip() for a in args.split(",") if a.strip()}
|
||||
for surveille in NON_GLOBALES:
|
||||
if surveille in parametres:
|
||||
continue
|
||||
# La declaration peut etre un SECOND declarateur : `const di = …, data = …`.
|
||||
# Sans ce cas, `dessinerReseau()` etait signalee a tort — et un faux positif
|
||||
# dans une garde finit toujours par la faire desactiver.
|
||||
if re.search(rf"\b(?:const|let|var)\b[^;\n]*?(?<![\w.$]){surveille}\s*=", corps):
|
||||
continue
|
||||
for ligne in corps.split("\n"):
|
||||
nue = ligne.strip()
|
||||
if nue.startswith("//") or nue.startswith("*"):
|
||||
continue
|
||||
if re.search(rf"(?<![\w.$]){surveille}\s*[.\[]", nue):
|
||||
orphelins.append(f"{nom}() lit `{surveille}` : {nue[:88]}")
|
||||
break
|
||||
return orphelins
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not shutil.which("node"):
|
||||
print("node absent : verification de la syntaxe JS du GUI sautee.")
|
||||
|
|
@ -36,7 +90,16 @@ def main() -> int:
|
|||
if resultat.returncode != 0:
|
||||
print(resultat.stderr, file=sys.stderr)
|
||||
return 1
|
||||
print("JS du GUI : syntaxe valide (node --check).")
|
||||
|
||||
orphelins = variables_orphelines("\n".join(blocs))
|
||||
if orphelins:
|
||||
print("erreur: des fonctions lisent une variable qu'elles n'ont pas :", file=sys.stderr)
|
||||
for ligne in orphelins:
|
||||
print(f" - {ligne}", file=sys.stderr)
|
||||
print(" (declarer un global, ou passer la valeur en parametre)", file=sys.stderr)
|
||||
return 1
|
||||
print(f"JS du GUI : syntaxe valide (node --check), {len(NON_GLOBALES)} nom(s) surveille(s) "
|
||||
f"sans reference orpheline.")
|
||||
return 0
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -40,11 +40,20 @@ Le flux d'exploitation, de bout en bout :
|
|||
propose que des **hôtes réels** : impossible de pointer vers un hôte fantôme. La console couvre le
|
||||
schéma du plan — et la preuve **P19** le vérifie.
|
||||
|
||||
**Les formulaires sont GÉNÉRÉS.** Depuis le 2026-09-08, quatre registres sur six — **bases de
|
||||
données**, **serveurs de BD**, **domaines** et **nomenclature** — ne portent plus de formulaire
|
||||
écrit à la main : ils le construisent depuis `docs/audit/schema-plan.json`, lui-même dérivé des
|
||||
constantes du moteur (`make schema`). Un champ ajouté au plan apparaît donc à l'écran **sans qu'on
|
||||
touche à l'interface**. Deux preuves le tiennent : **P61** refuse qu'un champ des plans réels
|
||||
**Les formulaires sont GÉNÉRÉS.** Depuis le 2026-09-08, **les six registres** — serveurs,
|
||||
applications, bases de données, serveurs de BD, domaines et nomenclature — ne portent plus de
|
||||
formulaire écrit à la main : ils le construisent depuis `docs/audit/schema-plan.json`, lui-même
|
||||
dérivé des constantes du moteur (`make schema`). Un champ ajouté au plan apparaît donc à l'écran
|
||||
**sans qu'on touche à l'interface** — et il arrive au fichier, parce que la sauvegarde dérive du
|
||||
même schéma. Ouvrir une vue et enregistrer sans rien toucher renvoie **exactement** le plan qu'on
|
||||
vient de lire ; c'est mesuré à chaque `make prouver`.
|
||||
|
||||
Deux champs gardent volontairement leur propre éditeur, et le schéma le déclare : la **matrice des
|
||||
intégrations** (elle montre aussi les universelles, non décochables, et les exemptions) et
|
||||
l'**éditeur de liens** (il contraint le rôle à ce que `meta/liens.yml` accepte). Le générateur
|
||||
s'efface plutôt que de remplacer un éditeur qui en sait plus que lui.
|
||||
|
||||
Deux preuves tiennent le schéma : **P61** refuse qu'un champ des plans réels
|
||||
manque au schéma, ou que le schéma décrive une *forme* que les plans n'ont pas ; **P62** refuse
|
||||
qu'un champ accepté par les *validateurs du moteur* manque au schéma — sans quoi le formulaire ne
|
||||
saurait pas offrir une fonctionnalité que Set-OPS possède déjà.
|
||||
|
|
|
|||
Loading…
Reference in a new issue