Aucun hôte ne correspond au filtre.
'; return; }
- const sections = [['Actifs', visibles.filter(([h]) => h.etat === 'actif')], ['Planifiés', visibles.filter(([h]) => h.etat !== 'actif')]];
- const banniere = 'Aucun groupe opérationnel ni application.
';
- const grpHtml = grps.map(g => {
- const c = chaine[g] || {roles: [], stub: true};
- const corps = (c.roles && c.roles.length)
- ? `Aucun serveur de BD.
';
- const sourceConso = (portee) => portee === 'hote' ? hotes.map(h => h.nom).filter(Boolean)
- : portee === 'application' ? applications.map(a => a.id)
- : groupes;
- const optConso = (portee, sel) => 'Aucune base applicative.
';
- cible.innerHTML = `
- Aucun serveur de BD.
';
+ const bd = bdApplis.length ? `Aucune base.
';
+ cible.innerHTML = `Aucune application. Une VM peut en porter plusieurs.
';
- cible.innerHTML = `
-
-
Applications ${applications.length}
-
${lignes}
+ const tuiles = applications.length
+ ? `
${applications.map((a, i) => carteApp(a, i)).join('')}
`
+ : '
Aucune application. Une VM peut en porter plusieurs.
';
+ cible.innerHTML = `
+
Applications ${applications.length}
+ ${tuiles}
`;
+ }
+ function inspecteurApplication() {
+ const cible = document.getElementById('detail');
+ if (selApp == null || !applications[selApp]) {
+ cible.innerHTML = '
Aucune application sélectionnéeChoisis une tuile à gauche, ou ajoute une application.
';
+ return;
+ }
+ const a = applications[selApp];
+ const optGroupe = groupes.map(g => `
`).join('');
+ const optHote = '
' + hotes.map(h => h.nom).filter(Boolean).map(n => `
`).join('');
+ const dsns = basesDeApp(a, a.hote);
+ const liens = dsns.length
+ ? `
${dsns.map(b => `
${echapper(b.portee || 'groupe')} ${echapper(dsnBase(b))}
`).join('')}
`
+ : '
aucune base liée
';
+ cible.innerHTML = `
+ ${echapper(a.id || '(sans id)')}
+
+
+
+
+
+
+
+
+
- `;
+ Bases consommées
+ ${liens}
+ Sauvegarde (Ctrl+S) puis « Appliquer le plan » pour régénérer.
+ `;
}
function listeDepuisTexte(v) { return String(v || '').split(',').map(s => s.trim()).filter(Boolean); }
@@ -1249,64 +1227,128 @@ HTML = r"""
return {vmid: '9' + info.categorie + info.service + String(seq).padStart(2, '0'), ip: base3 + '.' + (info.service * 10 + seq), vlan: String(info.vlan)};
}
- function marquerServeursModifie() {
- serveursModifie = true;
- const b = document.getElementById('btn-sauver-serveurs');
- if (b) { b.classList.add('attention'); b.textContent = 'Sauvegarder les serveurs •'; }
+ function marquerServeursModifie() { serveursModifie = true; majIndicateurSauvegarde(); }
+ function choisirServeur(i) { selServeur = i; dessiner(); }
+ function definirServeur(champ, v) {
+ const s = serveurs[selServeur]; if (!s) return;
+ 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();
}
- 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));
+ function basculerIntegration(role, actif) {
+ const s = serveurs[selServeur]; if (!s) return;
+ const set = new Set(listeDepuisTexte(s.integrations));
if (actif) set.add(role); else set.delete(role);
- serveurs[i].integrations = Array.from(set).join(', ');
- marquerServeursModifie();
+ s.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(); }
+ 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: ''});
+ selServeur = 0; marquerServeursModifie(); dessiner();
+ const c = document.querySelector('#detail .grille input'); if (c) c.focus();
+ }
+ function retirerServeur() {
+ const s = serveurs[selServeur]; if (!s) return;
+ if (s.nom && !confirm('Retirer le serveur « ' + s.nom + ' » du plan ?')) return;
+ serveurs.splice(selServeur, 1);
+ selServeur = serveurs.length ? Math.min(selServeur, serveurs.length - 1) : null;
+ marquerServeursModifie(); dessiner();
+ }
+ function ouvrirApp(id) { const i = applications.findIndex(a => a.id === id); selApp = i >= 0 ? i : null; setVue('applications'); }
+ function ouvrirBaseServeur(nom) { const i = bdServeurs.findIndex(x => x.nom === nom); selBase = i >= 0 ? 'srv:' + i : null; setVue('bases'); }
+ function carteServeur(s, i) {
+ const actif = s.etat === 'actif';
+ const recon = s.statut === 'divergence' ? '
⚠ divergent'
+ : s.statut === 'absent' ? '
○ non instancié' : '';
+ return `
`;
+ }
function dessinerServeurs() {
const cible = document.getElementById('grilles');
- const optFonction = (sel) => Object.keys(nomenclature.fonctions || {}).map(f => `
`).join('');
- const optEtat = (sel) => ['planifie', 'actif'].map(e => `
`).join('');
+ const entete = BANNIERE_MODELE + `
Serveurs ${serveurs.length}
+
+
`;
+ if (!serveurs.length) { cible.innerHTML = entete + '
Aucun serveur. « + Serveur » ou make serveurs-bootstrap.
'; return; }
+ const idx = serveurs.map((s, i) => [s, i]).filter(([s]) => correspondServeur(s));
+ const sections = [['Actifs', idx.filter(([s]) => s.etat === 'actif')], ['Planifiés', idx.filter(([s]) => s.etat !== 'actif')]];
+ cible.innerHTML = entete + sections.filter(([, l]) => l.length).map(([t, l]) =>
+ `
${t} ${l.length}
${l.map(([s, i]) => carteServeur(s, i)).join('')}
`).join('');
+ }
+ function inspecteurServeur() {
+ const cible = document.getElementById('detail');
+ if (selServeur == null || !serveurs[selServeur]) {
+ hoteOpCourant = '';
+ cible.innerHTML = '
Aucun serveur sélectionnéChoisis une tuile à gauche, ou ajoute un serveur.
';
+ return;
+ }
+ const s = serveurs[selServeur];
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)}
-
`;
- }).join('') : '
Aucun serveur. Utilise « + Serveur » ou make serveurs-bootstrap.
';
- cible.innerHTML = `
-
-
Serveurs du plan ${serveurs.length}
-
${lignes}
-
-
-
-
-
VMID / IP / VLAN sont dérivés de la fonction. « Appliquer le plan » régénère l'inventaire (refuse si conflit ; git est le filet).
-
`;
+ const optFonction = Object.keys(nomenclature.fonctions || {}).map(f => `
`).join('');
+ const optEtat = ['planifie', 'actif'].map(e => `
`).join('');
+ const optDeListe = (liste, sel, lib) => `
`
+ + [...new Set([...(liste || []), ...(sel ? [sel] : [])])].map(v => `
`).join('');
+ const set = new Set(listeDepuisTexte(s.integrations));
+ const tousRoles = [...new Set([...integrationsDispo, ...set])];
+ const cases = tousRoles.length
+ ? tousRoles.map(role => `
`).join('')
+ : '
aucun rôle client_* disponible';
+ const info = infoFonction(s.fonction);
+ const zoneHint = info ? `
${echapper(info.libelle)} · VLAN ${info.vlan}` : '';
+ const h = hotes.find(x => x.nom === s.nom);
+ const grpHtml = h && (h.groupes || []).length
+ ? `
${h.groupes.map(g => renduGroupe(h, g)).join('')}
`
+ : `
${s.statut === 'absent' ? 'Pas encore instancié — « Appliquer le plan ».' : 'Aucun groupe.'}
`;
+ const apps = applications.filter(a => a.hote === s.nom);
+ const appsHtml = apps.length
+ ? `
${apps.map(a => `
app ${a.expose ? ` → ${echapper(listeDepuisTexte(a.expose).join(', '))}` : ''}
`).join('')}
`
+ : '
aucune application sur cet hôte
';
+ const bds = bdServeurs.filter(x => x.hote === s.nom);
+ const bdHtml = bds.length
+ ? `
${bds.map(x => `
${echapper(x.type)} :${echapper(x.port)}
`).join('')}
`
+ : '
aucun serveur de base ici
';
+ hoteOpCourant = s.nom;
+ let pied;
+ if (h && h.etat === 'actif' && !hoteBloque(h) && estProduction) {
+ const hi = hotes.indexOf(h);
+ pied = `
+
+
`;
+ } else {
+ const txt = !h ? 'Applique le plan pour instancier cet hôte' : (h.etat !== 'actif' ? 'Hôte actif requis pour déployer' : (!estProduction ? 'Déploiement : inventaire production requis' : 'Prérequis manquant'));
+ pied = `
${txt}
`;
+ }
+ const statBadge = s.statut === 'divergence' ? '
divergent'
+ : s.statut === 'absent' ? '
non instancié'
+ : '
réconcilié';
+ cible.innerHTML = `
+ ${echapper(s.nom || '(sans nom)')}${zoneHint}
+
${statBadge}
+
+ ${tuile(s.vmid, 'VMID')}${tuile(s.adresse_ip, 'IP')}${tuile(s.vlan, 'VLAN')}
+ Identité — saisie du plan (surcharge les défauts)
+
+ Groupes (dérivés)
${grpHtml}
+ Applications ici
${appsHtml}
+ Bases ici
${bdHtml}
+ VMID / IP / VLAN sont dérivés. Sauvegarde (Ctrl+S) puis « Appliquer le plan » pour régénérer l'inventaire.
+ ${pied}
+ `;
+ majBoutonsDeploiement();
}
function serveurValeurValide(cle, v) {
@@ -1325,7 +1367,7 @@ HTML = r"""
}
async function sauvegarderServeurs() {
- const invalides = Array.from(document.querySelectorAll('#grilles [data-v]')).filter(el => !validerChampServeur(el));
+ const invalides = Array.from(document.querySelectorAll('#detail [data-v]')).filter(el => !validerChampServeur(el));
if (invalides.length) {
message('Corrige les ' + invalides.length + ' champ(s) en rouge avant de sauvegarder.', 'erreur');
invalides[0].focus();
@@ -1385,82 +1427,22 @@ HTML = r"""
}).join('');
}
- function champLecture(h, cle, libelle, opts={}) {
- const brut = h[cle];
- const v = (brut === '' || brut == null) ? '—' : String(brut);
- const unite = opts.unite ? `
· ${echapper(opts.unite)}` : '';
- return `
`;
- }
-
function tuile(valeur, libelle) {
const v = (valeur === '' || valeur == null) ? '—' : String(valeur);
return `
${echapper(v)}
${echapper(libelle)}
`;
}
function dessinerDetail() {
- const cible = document.getElementById('detail');
- if (selection < 0 || !hotes[selection]) {
- cible.innerHTML = '
Aucun hôte sélectionnéChoisissez une carte pour l\'inspecter.
';
- return;
- }
- const index = selection, h = hotes[index];
- const actif = h.etat === 'actif';
- const info = infoFonction(fonctionDe(h.nom).fonction);
- const zoneHint = info ? `
${echapper(info.libelle)} · VLAN ${info.vlan}` : '';
- const st = statutHote(h);
- const libelle = actif ? (hoteBloque(h) ? 'prérequis manquant' : 'déployable') : 'planifié';
- let pied;
- if (actif && !hoteBloque(h) && estProduction) {
- pied = `
-
-
`;
- } else {
- const txt = !actif ? 'Hôte actif requis pour déployer' : (!estProduction ? 'Déploiement : inventaire production requis' : 'Prérequis manquant');
- pied = `
${txt}${libelle}
`;
- }
- const onglet = ONGLETS[ongletActif];
- let corps;
- if (onglet) {
- corps = `
${onglet.map(([c, l, o]) => champLecture(h, c, l, o)).join('')}
`;
- } else if (ongletActif === 'chaine') {
- corps = renduChaine(h);
- } else {
- const gs = h.groupes || [];
- corps = gs.length
- ? `
${gs.map(g => renduGroupe(h, g)).join('')}
`
- : '
Aucun groupe rattaché à cet hôte.
';
- }
- cible.innerHTML = `
-
-
-
${echapper(h.nom || '(sans nom)')}${zoneHint}
-
-
${libelleStatut(st)}
-
-
- ${tuile(h.vmid, 'VMID')}
- ${tuile(h.memoire ? h.memoire + ' Mo' : '', 'Mémoire')}
- ${tuile(h.coeurs, 'Cœurs')}
- ${tuile(h.disque_taille, 'Disque')}
- ${tuile((h.groupes || []).length, 'Groupes')}
-
-
-
-
-
-
-
- ${corps}
- ${pied}
- `;
- majBoutonsDeploiement();
+ // Le panneau de droite reflète la sélection de la VUE COURANTE.
+ if (vuePrincipale === 'applications') return inspecteurApplication();
+ if (vuePrincipale === 'bases') return inspecteurBase();
+ return inspecteurServeur();
}
function majBoutonsDeploiement() {
const bv = document.getElementById('btn-verifier');
const bd = document.getElementById('btn-deployer');
- const hote = selection >= 0 && hotes[selection] ? (hotes[selection].nom || '') : '';
+ const hote = hoteOpCourant || '';
if (bv) { bv.disabled = occupe; bv.title = occupe ? 'Exécution en cours' : 'Dry-run (--check --diff), aucune modification'; }
if (bd) { bd.disabled = occupe || !verifie[hote]; bd.title = !verifie[hote] ? 'Vérifiez (dry-run) d\'abord' : 'Applique les playbooks sur la VM réelle'; }
}
@@ -1718,18 +1700,18 @@ HTML = r"""
else message('Vue en lecture seule — édite dans Serveurs / Applications / Bases.');
}
- function ordreHotesVisibles() {
- const vis = hotes.map((h, i) => [h, i]).filter(([h]) => correspond(h));
- return [...vis.filter(([h]) => h.etat === 'actif'), ...vis.filter(([h]) => h.etat !== 'actif')].map(([, i]) => i);
+ function ordreServeursVisibles() {
+ const vis = serveurs.map((s, i) => [s, i]).filter(([s]) => correspondServeur(s));
+ return [...vis.filter(([s]) => s.etat === 'actif'), ...vis.filter(([s]) => s.etat !== 'actif')].map(([, i]) => i);
}
- function naviguerHotes(delta) {
- const ordre = ordreHotesVisibles();
+ function naviguerServeurs(delta) {
+ const ordre = ordreServeursVisibles();
if (!ordre.length) return;
- let pos = ordre.indexOf(selection);
+ let pos = ordre.indexOf(selServeur);
pos = pos < 0 ? (delta > 0 ? 0 : ordre.length - 1)
: Math.min(ordre.length - 1, Math.max(0, pos + delta));
- selectionner(ordre[pos]);
- const c = document.getElementById('carte-' + ordre[pos]);
+ choisirServeur(ordre[pos]);
+ const c = document.getElementById('tuile-srv-' + ordre[pos]);
if (c) c.scrollIntoView({block: 'nearest', behavior: 'smooth'});
}
@@ -1738,14 +1720,14 @@ HTML = r"""
if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) { e.preventDefault(); sauvegarderVue(); return; }
const dansSaisie = /^(INPUT|SELECT|TEXTAREA)$/.test((document.activeElement || {}).tagName || '');
if (dansSaisie || e.ctrlKey || e.metaKey || e.altKey) return;
- const vues = {'1': 'cartes', '2': 'serveurs', '3': 'applications', '4': 'bases'};
+ const vues = {'1': 'serveurs', '2': 'applications', '3': 'bases'};
if (vues[e.key]) { setVue(vues[e.key]); return; }
if (e.key === '/') { e.preventDefault(); const f = document.getElementById('filtre'); if (f) f.focus(); return; }
- if (vuePrincipale !== 'cartes') return;
- if (e.key === 'j') { naviguerHotes(1); return; }
- if (e.key === 'k') { naviguerHotes(-1); return; }
- if (e.key === 'v') { const b = document.getElementById('btn-verifier'); if (b && !b.disabled) lancer('verifier', selection); return; }
- if (e.key === 'd') { const b = document.getElementById('btn-deployer'); if (b && !b.disabled) lancer('deployer', selection); return; }
+ if (vuePrincipale !== 'serveurs') return;
+ if (e.key === 'j') { naviguerServeurs(1); return; }
+ if (e.key === 'k') { naviguerServeurs(-1); return; }
+ if (e.key === 'v') { const b = document.getElementById('btn-verifier'); if (b && !b.disabled) b.click(); return; }
+ if (e.key === 'd') { const b = document.getElementById('btn-deployer'); if (b && !b.disabled) b.click(); return; }
});
// Validation inline des champs du plan (vue Serveurs) — liseré rouge avant l'envoi.