diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dfe0d5..02130b7 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/scripts/inventory_gui.py b/scripts/inventory_gui.py index fafb439..36c264c 100644 --- a/scripts/inventory_gui.py +++ b/scripts/inventory_gui.py @@ -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""" .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""" let motVault = ''; let modaleResolveur = null; let intrants = null; + let integrationsDispo = []; let chronoConsole = null; let consoleDebut = 0; @@ -879,6 +907,7 @@ HTML = r""" 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""" 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""" const cible = document.getElementById('grilles'); const optFonction = (sel) => Object.keys(nomenclature.fonctions || {}).map(f => ``).join(''); const optEtat = (sel) => ['planifie', 'actif'].map(e => ``).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 `` + + vals.map(v => ``).join(''); + }; + const casesInteg = (i, txt) => { + const set = new Set(listeDepuisTexte(txt)); + const tous = [...new Set([...integrationsDispo, ...set])]; + if (!tous.length) return 'aucun rôle client_* disponible'; + return tous.map(role => ``).join(''); + }; const lignes = serveurs.length ? serveurs.map((s, i) => { const d = deriveServeur(s.nom, s.fonction); return `
- - + + - +
dérivé → vmid ${echapper(d.vmid)} · ip ${echapper(d.ip)} · vlan ${echapper(d.vlan)}
`; @@ -1558,14 +1606,20 @@ HTML = r""" html += '
' + echapper(sec) + '
'; parSection[sec].forEach(ch => { const v = valeurs[ch.variable]; - const badge = ch.classe === 'constante' - ? 'Constante' - : 'Défaut'; + let badge; + if (ch.classe === 'constante') badge = 'Constante'; + else if (ch.classe === 'catalogue') badge = 'Catalogue'; + else badge = 'Défaut'; const lib = echapper(ch.libelle) + badge; if (ch.type === 'bool') { const coche = (v === true || v === 'true' || v === 'True') ? ' checked' : ''; html += ''; + } else if (ch.type === 'liste') { + const txt = Array.isArray(v) ? v.join(', ') : (v == null ? '' : v); + html += ''; } else { const t = ch.type === 'int' ? 'number' : 'text'; html += '