GUI vue Serveurs : Nœud/Stockage en déroulantes, Intégrations en cases à cocher
Les champs libres deviennent des sélections alimentées par les paramètres globaux : Nœud et Stockage puisent dans de nouveaux catalogues proxmox_noeuds / proxmox_stockages (vide = défaut global), Intégrations liste les rôles client_* disponibles (scan roles/client_*). Les catalogues (Nœuds/Stockages/Ponts) sont des intrants de classe « Catalogue » éditables dans le panneau Intrants ; ajout du type `liste` (chaîne virgulée → liste YAML dédoublonnée) et de integrations_disponibles dans l'API. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a21a6b4e02
commit
738fd2b88c
2 changed files with 74 additions and 9 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -19,6 +19,17 @@
|
|||
- Le bouton **Sauvegarder** de l'en-tête devient contextuel (sauve la vue éditable,
|
||||
désactivé en lecture seule) — fin de l'impasse 409 ; suppression du code mort.
|
||||
|
||||
### Ajouté
|
||||
- **Vue Serveurs : champs alimentés par les paramètres globaux.** À `+ Serveur`,
|
||||
**Nœud** et **Stockage** deviennent des listes déroulantes (catalogues
|
||||
`proxmox_noeuds` / `proxmox_stockages`, vide = défaut global) et **Intégrations**
|
||||
une rangée de cases à cocher des rôles `client_*` disponibles (au lieu d'une saisie
|
||||
texte). Les catalogues Nœuds / Stockages / Ponts sont de nouveaux intrants
|
||||
(classe « Catalogue ») éditables dans le panneau « Intrants de base » et stockés
|
||||
dans `group_vars/proxmox.yml` ; l'API expose `integrations_disponibles` (scan de
|
||||
`roles/client_*`). Le type `liste` (chaîne virgulée → liste YAML dédoublonnée) est
|
||||
ajouté au schéma des intrants.
|
||||
|
||||
### Modifié
|
||||
- **Vue Inventaire (cartes) : détail converti en inspection lecture seule.** La vue
|
||||
affichait « généré depuis le plan (lecture seule) » tout en exposant une surface
|
||||
|
|
|
|||
|
|
@ -69,6 +69,9 @@ INTRANTS_SCHEMA = [
|
|||
("proxmox_clone_noeud", "proxmox", "defaut", "Proxmox", "Nœud Proxmox (défaut)", "str"),
|
||||
("proxmox_clone_stockage", "proxmox", "defaut", "Proxmox", "Stockage (défaut)", "str"),
|
||||
("proxmox_clone_pont", "proxmox", "defaut", "Proxmox", "Pont réseau (défaut)", "str"),
|
||||
("proxmox_noeuds", "proxmox", "catalogue", "Proxmox", "Nœuds disponibles (liste)", "liste"),
|
||||
("proxmox_stockages", "proxmox", "catalogue", "Proxmox", "Stockages disponibles (liste)", "liste"),
|
||||
("proxmox_ponts", "proxmox", "catalogue", "Proxmox", "Ponts réseau disponibles (liste)", "liste"),
|
||||
]
|
||||
# Cles secretes : interdites a l'ecriture par le GUI (garde-fou).
|
||||
INTRANTS_CLES_INTERDITES = {
|
||||
|
|
@ -247,6 +250,7 @@ def inventaire_api(path: Path) -> dict:
|
|||
"serveurs": serveurs_reconcilies(data),
|
||||
"hotes": liste_hotes(data),
|
||||
"intrants": intrants_api(),
|
||||
"integrations_disponibles": integrations_disponibles(),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -261,14 +265,24 @@ def _lire_yaml_dict(path: Path) -> dict:
|
|||
def intrants_courants() -> dict:
|
||||
cache: dict = {}
|
||||
valeurs: dict = {}
|
||||
for variable, fcle, *_ in INTRANTS_SCHEMA:
|
||||
for variable, fcle, _classe, _section, _libelle, typ in INTRANTS_SCHEMA:
|
||||
if fcle not in cache:
|
||||
cache[fcle] = _lire_yaml_dict(FICHIERS_INTRANTS[fcle])
|
||||
v = cache[fcle].get(variable, "")
|
||||
valeurs[variable] = "" if v is None else v
|
||||
v = cache[fcle].get(variable)
|
||||
if typ == "liste":
|
||||
items = v if isinstance(v, list) else []
|
||||
valeurs[variable] = [str(x).strip() for x in items if str(x).strip()]
|
||||
else:
|
||||
valeurs[variable] = "" if v is None else v
|
||||
return valeurs
|
||||
|
||||
|
||||
def integrations_disponibles() -> list:
|
||||
"""Roles client_* presents dans roles/ : source des integrations selectionnables."""
|
||||
base = RACINE / "roles"
|
||||
return sorted(p.name for p in base.glob("client_*") if p.is_dir())
|
||||
|
||||
|
||||
def intrants_api() -> dict:
|
||||
schema = [{"variable": v, "fichier": f, "classe": c, "section": s,
|
||||
"libelle": l, "type": t} for (v, f, c, s, l, t) in INTRANTS_SCHEMA]
|
||||
|
|
@ -281,6 +295,15 @@ def intrants_api() -> dict:
|
|||
|
||||
|
||||
def _coercer_intrant(typ: str, brut: object) -> object:
|
||||
if typ == "liste":
|
||||
items = brut if isinstance(brut, list) else str(brut).replace("\n", ",").split(",")
|
||||
# dedoublonne en preservant l'ordre
|
||||
vus, sortie = set(), []
|
||||
for x in items:
|
||||
s = str(x).strip()
|
||||
if s and s not in vus:
|
||||
vus.add(s); sortie.append(s)
|
||||
return sortie
|
||||
s = str(brut).strip()
|
||||
if typ == "bool":
|
||||
return s.lower() in ("true", "1", "oui", "yes", "on")
|
||||
|
|
@ -703,6 +726,10 @@ HTML = r"""<!doctype html>
|
|||
.badge-classe { display: inline-block; font-size: 9.5px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; padding: 1px 6px; border-radius: 999px; margin-left: 6px; vertical-align: middle; }
|
||||
.badge-classe.const { background: color-mix(in srgb, var(--ambre) 20%, transparent); color: var(--ambre); }
|
||||
.badge-classe.def { background: color-mix(in srgb, var(--bleu) 18%, transparent); color: var(--bleu); }
|
||||
.badge-classe.cat { background: color-mix(in srgb, var(--teal) 20%, transparent); color: var(--teal); }
|
||||
.integ-cases { display: flex; flex-wrap: wrap; gap: 6px 12px; padding: 4px 2px; }
|
||||
.integ-case { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 600; color: var(--texte); cursor: pointer; }
|
||||
.integ-case input { width: 14px; height: 14px; flex: none; accent-color: var(--teal); }
|
||||
.intr-ro { color: var(--muted); font-size: 12px; line-height: 1.55; }
|
||||
.intr-ro b { color: var(--texte); font-weight: 700; }
|
||||
.intr-secrets { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
|
||||
|
|
@ -816,6 +843,7 @@ HTML = r"""<!doctype html>
|
|||
let motVault = '';
|
||||
let modaleResolveur = null;
|
||||
let intrants = null;
|
||||
let integrationsDispo = [];
|
||||
let chronoConsole = null;
|
||||
let consoleDebut = 0;
|
||||
|
||||
|
|
@ -879,6 +907,7 @@ HTML = r"""<!doctype html>
|
|||
nomenclature = data.nomenclature || {};
|
||||
chaine = data.chaine || {};
|
||||
intrants = data.intrants || null;
|
||||
integrationsDispo = data.integrations_disponibles || [];
|
||||
chargerBases(data);
|
||||
chargerApplications(data);
|
||||
chargerServeurs(data);
|
||||
|
|
@ -1252,6 +1281,12 @@ HTML = r"""<!doctype html>
|
|||
if (b) { b.classList.add('attention'); b.textContent = 'Sauvegarder les serveurs •'; }
|
||||
}
|
||||
function definirServeur(i, champ, v) { serveurs[i][champ] = v; marquerServeursModifie(); if (champ === 'nom' || champ === 'fonction') dessinerServeurs(); }
|
||||
function basculerIntegration(i, role, actif) {
|
||||
const set = new Set(listeDepuisTexte(serveurs[i].integrations));
|
||||
if (actif) set.add(role); else set.delete(role);
|
||||
serveurs[i].integrations = Array.from(set).join(', ');
|
||||
marquerServeursModifie();
|
||||
}
|
||||
function ajouterServeur() { serveurs.push({nom: '', fonction: (Object.keys(nomenclature.fonctions || {})[0] || ''), etat: 'planifie', noeud: '', stockage: '', disque: '', memoire: '', coeurs: '', integrations: ''}); marquerServeursModifie(); dessinerServeurs(); }
|
||||
function retirerServeur(i) { serveurs.splice(i, 1); marquerServeursModifie(); dessinerServeurs(); }
|
||||
|
||||
|
|
@ -1259,18 +1294,31 @@ HTML = r"""<!doctype html>
|
|||
const cible = document.getElementById('grilles');
|
||||
const optFonction = (sel) => Object.keys(nomenclature.fonctions || {}).map(f => `<option value="${echapper(f)}" ${f === sel ? 'selected' : ''}>${echapper(f)}</option>`).join('');
|
||||
const optEtat = (sel) => ['planifie', 'actif'].map(e => `<option value="${e}" ${e === sel ? 'selected' : ''}>${e}</option>`).join('');
|
||||
const vi = (intrants && intrants.valeurs) || {};
|
||||
const libDef = (d) => '(défaut' + (d ? ' : ' + d : '') + ')';
|
||||
const optDeListe = (liste, sel, libelleDefaut) => {
|
||||
const vals = [...new Set([...(liste || []), ...(sel ? [sel] : [])])];
|
||||
return `<option value="" ${sel ? '' : 'selected'}>${echapper(libelleDefaut)}</option>`
|
||||
+ vals.map(v => `<option value="${echapper(v)}" ${v === sel ? 'selected' : ''}>${echapper(v)}</option>`).join('');
|
||||
};
|
||||
const casesInteg = (i, txt) => {
|
||||
const set = new Set(listeDepuisTexte(txt));
|
||||
const tous = [...new Set([...integrationsDispo, ...set])];
|
||||
if (!tous.length) return '<span class="dsn">aucun rôle client_* disponible</span>';
|
||||
return tous.map(role => `<label class="integ-case" title="${echapper(role)}"><input type="checkbox" ${set.has(role) ? 'checked' : ''} onchange="basculerIntegration(${i}, '${echapper(role)}', this.checked)"><span>${echapper(role.replace(/^client_/, ''))}</span></label>`).join('');
|
||||
};
|
||||
const lignes = serveurs.length ? serveurs.map((s, i) => {
|
||||
const d = deriveServeur(s.nom, s.fonction);
|
||||
return `<div class="base-ligne">
|
||||
<label class="champ"><span>Serveur (fonction-NN)</span><input data-v="nom" value="${echapper(s.nom)}" placeholder="web-frontal-03" oninput="definirServeur(${i}, 'nom', this.value)"></label>
|
||||
<label class="champ"><span>Fonction</span><select onchange="definirServeur(${i}, 'fonction', this.value)">${optFonction(s.fonction)}</select></label>
|
||||
<label class="champ"><span>État</span><select onchange="definirServeur(${i}, 'etat', this.value)">${optEtat(s.etat)}</select></label>
|
||||
<label class="champ"><span>Nœud</span><input value="${echapper(s.noeud)}" placeholder="noeud-1" oninput="definirServeur(${i}, 'noeud', this.value)"></label>
|
||||
<label class="champ"><span>Stockage</span><input value="${echapper(s.stockage)}" placeholder="stockage-1" oninput="definirServeur(${i}, 'stockage', this.value)"></label>
|
||||
<label class="champ"><span>Nœud</span><select onchange="definirServeur(${i}, '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(${i}, '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(${i}, '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(${i}, '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(${i}, 'coeurs', this.value)"></label>
|
||||
<label class="champ"><span>Intégrations</span><input value="${echapper(s.integrations)}" placeholder="client_dns, …" oninput="definirServeur(${i}, 'integrations', this.value)"></label>
|
||||
<label class="champ" style="grid-column:1/-1"><span>Intégrations (rôles client)</span><div class="integ-cases">${casesInteg(i, s.integrations)}</div></label>
|
||||
<button type="button" class="danger" title="Retirer" onclick="retirerServeur(${i})">✕</button>
|
||||
<div class="dsn mono">dérivé → vmid ${echapper(d.vmid)} · ip ${echapper(d.ip)} · vlan ${echapper(d.vlan)}</div>
|
||||
</div>`;
|
||||
|
|
@ -1558,14 +1606,20 @@ HTML = r"""<!doctype html>
|
|||
html += '<div class="intr-section"><div class="intr-tete">' + echapper(sec) + '</div><div class="intr-grille">';
|
||||
parSection[sec].forEach(ch => {
|
||||
const v = valeurs[ch.variable];
|
||||
const badge = ch.classe === 'constante'
|
||||
? '<span class="badge-classe const" title="Valeur unique, non surchargeable">Constante</span>'
|
||||
: '<span class="badge-classe def" title="Valeur par défaut, surchargeable dans les instances">Défaut</span>';
|
||||
let badge;
|
||||
if (ch.classe === 'constante') badge = '<span class="badge-classe const" title="Valeur unique, non surchargeable">Constante</span>';
|
||||
else if (ch.classe === 'catalogue') badge = '<span class="badge-classe cat" title="Liste de valeurs autorisées, proposée dans les menus déroulants">Catalogue</span>';
|
||||
else badge = '<span class="badge-classe def" title="Valeur par défaut, surchargeable dans les instances">Défaut</span>';
|
||||
const lib = echapper(ch.libelle) + badge;
|
||||
if (ch.type === 'bool') {
|
||||
const coche = (v === true || v === 'true' || v === 'True') ? ' checked' : '';
|
||||
html += '<label class="champ"><span>' + lib + '</span>'
|
||||
+ '<input type="checkbox" data-intrant="' + echapper(ch.variable) + '" data-type="bool"' + coche + '></label>';
|
||||
} else if (ch.type === 'liste') {
|
||||
const txt = Array.isArray(v) ? v.join(', ') : (v == null ? '' : v);
|
||||
html += '<label class="champ" style="grid-column:1/-1"><span>' + lib + ' <span class="unite">· séparés par des virgules</span></span>'
|
||||
+ '<input type="text" data-intrant="' + echapper(ch.variable) + '" data-type="liste"'
|
||||
+ ' placeholder="ex. asgard, pve-02, pve-03" value="' + echapper(txt) + '"></label>';
|
||||
} else {
|
||||
const t = ch.type === 'int' ? 'number' : 'text';
|
||||
html += '<label class="champ"><span>' + lib + '</span>'
|
||||
|
|
|
|||
Loading…
Reference in a new issue