GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à gauche, détail + saisie à droite, panneau droit contextuel (fin du panneau figé au changement de vue). Applications et Bases passent en maître-détail ; liens cliquables entre objets. Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles sont fusionnées en une vue Serveurs unique — tuile avec statut de réconciliation, détail réunissant identité éditable, dérivés, groupes, applications/bases hébergées et Vérifier/Déployer. Navigation clavier et filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ». Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection d'hôte, chips de filtre). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2f9db3f516
commit
5176d714e9
2 changed files with 341 additions and 342 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
|
@ -2,6 +2,23 @@
|
|||
|
||||
## 2026-06-30
|
||||
|
||||
### Modifié
|
||||
- **GUI refondu en atelier maître-détail unifié.** Toutes les vues suivent le même
|
||||
motif : tuiles à gauche, **détail + saisie à droite**, le panneau droit reflétant
|
||||
la sélection de la vue courante (fin du panneau « figé » au changement de vue).
|
||||
Les vues **Applications** et **Bases** passent de tableaux pleine largeur à ce
|
||||
motif ; la saisie se fait dans le panneau droit, avec **liens cliquables** entre
|
||||
objets (serveur → application → base). Le panneau droit est élargi (~38 %).
|
||||
- **Fusion Serveur/Hôte.** Les vues « Inventaire » (hôtes, lecture seule) et
|
||||
« Serveurs » (plan) faisaient doublon : elles sont fusionnées en **une seule vue
|
||||
Serveurs**. Sa tuile porte le statut de réconciliation (réconcilié / divergent /
|
||||
non instancié) ; son détail réunit l'**identité éditable** (plan), les **dérivés**
|
||||
(VMID/IP/VLAN), les **groupes**, les **applications et bases hébergées**, et
|
||||
**Vérifier/Déployer**. La navigation clavier (`1-3`, `j/k`, `v/d`, `/`) et le
|
||||
filtre opèrent désormais sur les serveurs. Un bandeau « Comment lire ce parc »
|
||||
explicite le modèle serveur·hôte·groupe·application·base. Code mort retiré
|
||||
(`carte`, `renduChaine`, `ONGLETS`, `champLecture`, sélection d'hôte, etc.).
|
||||
|
||||
### Ajouté
|
||||
- **Fluidité d'exploitation du GUI** (sans dépendance, stdlib pure) :
|
||||
- **Notifications empilées (toasts)** auto-effaçables au lieu d'une bannière unique
|
||||
|
|
|
|||
|
|
@ -576,7 +576,7 @@ HTML = r"""<!doctype html>
|
|||
input::placeholder { color: var(--faint); }
|
||||
input:focus, select:focus { outline: none; border-color: var(--teal); box-shadow: 0 0 0 3px color-mix(in srgb, var(--teal) 22%, transparent); }
|
||||
|
||||
main { padding: 16px 22px 44px; display: grid; grid-template-columns: minmax(0, 1fr) minmax(330px, 380px); gap: 20px; align-items: start; max-width: 1640px; margin: 0 auto; }
|
||||
main { padding: 16px 22px 44px; display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(420px, 1fr); gap: 20px; align-items: start; max-width: 1760px; margin: 0 auto; }
|
||||
.message { min-height: 18px; font-size: 13px; font-weight: 650; color: var(--bleu); margin: 0 2px 12px; }
|
||||
.message.erreur { color: var(--rouge); } .message.ok { color: var(--vert); }
|
||||
.toasts { position: fixed; top: 14px; right: 14px; z-index: 60; display: flex; flex-direction: column; gap: 8px; max-width: min(380px, 92vw); pointer-events: none; }
|
||||
|
|
@ -648,6 +648,14 @@ HTML = r"""<!doctype html>
|
|||
.champ > span { color: var(--muted); font-size: 11px; font-weight: 700; }
|
||||
.champ .unite { color: var(--faint); font-weight: 600; }
|
||||
.champ > span.a-aide { cursor: help; border-bottom: 1px dotted var(--ligne-forte); width: fit-content; }
|
||||
.aide-modele { margin: 0 2px 14px; background: var(--surface); border: 1px solid var(--ligne); border-left: 3px solid var(--bleu); border-radius: var(--rayon-sm); }
|
||||
.aide-modele > summary { cursor: pointer; padding: 10px 13px; font-size: 12.5px; font-weight: 750; color: var(--bleu); list-style: none; }
|
||||
.aide-modele > summary::-webkit-details-marker { display: none; }
|
||||
.aide-modele > summary::before { content: "ⓘ "; }
|
||||
.aide-corps { padding: 2px 15px 12px; color: var(--muted); font-size: 12.5px; line-height: 1.55; }
|
||||
.aide-corps p { margin: 6px 0; } .aide-corps ul { margin: 6px 0; padding-left: 18px; } .aide-corps li { margin: 3px 0; }
|
||||
.aide-corps b { color: var(--texte); }
|
||||
.aide-ro { border-top: 1px solid var(--ligne); padding-top: 8px; }
|
||||
.champ .champ-val { font-size: 13px; font-weight: 600; color: var(--texte); padding: 5px 2px; min-height: 18px; overflow-wrap: anywhere; }
|
||||
.champ .champ-val.vide { color: var(--faint); }
|
||||
.detail-nom-txt { font-weight: 800; font-size: 17px; padding: 4px 7px; display: inline-block; overflow-wrap: anywhere; }
|
||||
|
|
@ -723,6 +731,8 @@ HTML = r"""<!doctype html>
|
|||
.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); }
|
||||
.lien-objet { background: none; border: 0; padding: 0; color: var(--bleu); font: inherit; font-weight: 700; cursor: pointer; text-decoration: underline dotted; }
|
||||
.lien-objet:hover { 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; }
|
||||
|
|
@ -749,10 +759,9 @@ HTML = r"""<!doctype html>
|
|||
<span class="chemin" id="chemin"></span>
|
||||
<div class="grandit"></div>
|
||||
<div class="resume" id="resume"></div>
|
||||
<div class="barre" title="Raccourcis clavier — 1-4 : changer de vue · j/k : naviguer les hôtes · v : vérifier · d : déployer · / : filtrer · Ctrl+S : sauvegarder la vue">
|
||||
<div class="barre" title="Raccourcis clavier — 1-3 : changer de vue · j/k : naviguer les serveurs · v : vérifier · d : déployer · / : filtrer · Ctrl+S : sauvegarder la vue">
|
||||
<div class="vue-bascule">
|
||||
<button type="button" id="btn-vue-cartes" class="on" onclick="setVue('cartes')">Inventaire</button>
|
||||
<button type="button" id="btn-vue-serveurs" onclick="setVue('serveurs')">Serveurs</button>
|
||||
<button type="button" id="btn-vue-serveurs" class="on" onclick="setVue('serveurs')">Serveurs</button>
|
||||
<button type="button" id="btn-vue-applications" onclick="setVue('applications')">Applications</button>
|
||||
<button type="button" id="btn-vue-bases" onclick="setVue('bases')">Bases</button>
|
||||
</div>
|
||||
|
|
@ -818,17 +827,18 @@ HTML = r"""<!doctype html>
|
|||
let hotes = [];
|
||||
let estProduction = false;
|
||||
let filtre = '';
|
||||
let filtreEtat = 'tous';
|
||||
let vuePrincipale = 'cartes';
|
||||
let vuePrincipale = 'serveurs';
|
||||
let bdServeurs = [];
|
||||
let bdApplis = [];
|
||||
let basesModifie = false;
|
||||
let selApp = null; // index de l'application sélectionnée (vue Applications)
|
||||
let selServeur = null; // index du serveur sélectionné (vue Serveurs, maître-détail fusionné)
|
||||
let selBase = null; // clé de l'item sélectionné (vue Bases) : "srv:<i>" ou "bd:<i>"
|
||||
let hoteOpCourant = ''; // nom de l'hôte dont les boutons Vérifier/Déployer sont affichés
|
||||
let applications = [];
|
||||
let applicationsModifie = false;
|
||||
let serveurs = [];
|
||||
let serveursModifie = false;
|
||||
let selection = -1;
|
||||
let selectionNom = null;
|
||||
let ongletActif = 'reseau';
|
||||
let occupe = false;
|
||||
let verifie = {};
|
||||
|
|
@ -839,26 +849,6 @@ HTML = r"""<!doctype html>
|
|||
let chronoConsole = null;
|
||||
let consoleDebut = 0;
|
||||
|
||||
const ONGLETS = {
|
||||
reseau: [
|
||||
['adresse_ip', 'Adresse IP', {placeholder:'192.168.12.101'}],
|
||||
['cidr', 'CIDR', {type:'number', placeholder:'24', min:0, max:32}],
|
||||
['passerelle', 'Passerelle', {placeholder:'192.168.12.1'}],
|
||||
['vlan', 'VLAN', {type:'number', placeholder:'12', min:1, max:4094}],
|
||||
['pont', 'Pont', {placeholder:'vmbr0'}],
|
||||
['dns', 'DNS', {placeholder:'192.168.12.1'}],
|
||||
['utilisateur_ansible', 'Utilisateur Ansible', {placeholder:'ansible'}],
|
||||
],
|
||||
proxmox: [
|
||||
['vmid', 'VMID', {type:'number', placeholder:'95301'}],
|
||||
['noeud', 'Nœud', {placeholder:'pve-01'}],
|
||||
['stockage', 'Stockage', {placeholder:'local-zfs'}],
|
||||
['disque_taille', 'Disque', {placeholder:'32G', unite:'taille'}],
|
||||
['memoire', 'Mémoire', {type:'number', placeholder:'2048', unite:'Mo'}],
|
||||
['coeurs', 'Cœurs', {type:'number', placeholder:'2', min:1}],
|
||||
],
|
||||
};
|
||||
|
||||
let toastSeq = 0;
|
||||
function message(texte, type='', opts={}) {
|
||||
const conteneur = document.getElementById('toasts');
|
||||
|
|
@ -907,29 +897,12 @@ HTML = r"""<!doctype html>
|
|||
estProduction = !!data.production;
|
||||
verifie = {};
|
||||
document.getElementById('chemin').textContent = data.inventaire;
|
||||
restaurerSelection();
|
||||
if (selServeur == null || selServeur >= serveurs.length) selServeur = serveurs.length ? 0 : null;
|
||||
dessiner();
|
||||
message('Inventaire chargé.', 'ok');
|
||||
}
|
||||
|
||||
function restaurerSelection() {
|
||||
if (selectionNom != null) {
|
||||
const i = hotes.findIndex(h => h.nom === selectionNom);
|
||||
selection = i >= 0 ? i : (hotes.length ? 0 : -1);
|
||||
} else {
|
||||
selection = hotes.length ? 0 : -1;
|
||||
}
|
||||
selectionNom = selection >= 0 ? hotes[selection].nom : null;
|
||||
}
|
||||
|
||||
function selectionner(index) {
|
||||
selection = index;
|
||||
selectionNom = hotes[index] ? hotes[index].nom : null;
|
||||
dessiner();
|
||||
}
|
||||
|
||||
function setOnglet(o) { ongletActif = o; dessinerDetail(); }
|
||||
function setFiltreEtat(e) { filtreEtat = e; dessinerChips(); dessinerGrilles(); }
|
||||
function filtrer(valeur) { filtre = (valeur || '').trim().toLowerCase(); dessinerGrilles(); }
|
||||
|
||||
function groupesActifs() { const a = new Set(); hotes.forEach(h => { if (h.etat === 'actif') (h.groupes || []).forEach(g => a.add(g)); }); return a; }
|
||||
|
|
@ -945,11 +918,9 @@ HTML = r"""<!doctype html>
|
|||
}
|
||||
function manquantsPourGroupe(groupe) { const a = groupesActifs(); return ((dependances[groupe] || {}).requiert_groupes_actifs || []).filter(i => !a.has(i)); }
|
||||
function hoteBloque(h) { if (!h || h.etat !== 'actif') return false; return (h.groupes || []).some(g => manquantsPourGroupe(g).length); }
|
||||
function statutHote(h) { return h.etat === 'actif' ? (hoteBloque(h) ? 'bloque' : 'ok') : 'attente'; }
|
||||
function libelleStatut(s) { return {ok:'actif', attente:'planifié', bloque:'bloqué', neutre:'libre'}[s] || s; }
|
||||
function abrege(g) { return g.replace(/^serveurs_/, '').replace(/^clients_/, 'cli·'); }
|
||||
|
||||
function dessiner() { dessinerResume(); dessinerChips(); dessinerGrilles(); dessinerDependances(); dessinerDetail(); majIndicateurSauvegarde(); }
|
||||
function dessiner() { dessinerResume(); dessinerGrilles(); dessinerDependances(); dessinerDetail(); majIndicateurSauvegarde(); }
|
||||
|
||||
function majIndicateurSauvegarde() {
|
||||
const b = document.getElementById('btn-sauver');
|
||||
|
|
@ -974,67 +945,41 @@ HTML = r"""<!doctype html>
|
|||
${bloques ? `<span class="chip"><span class="pt bloq"></span><b>${bloques}</b> bloqués</span>` : ''}`;
|
||||
}
|
||||
|
||||
function dessinerChips() {
|
||||
const n = {tous: hotes.length, actif: 0, planifie: 0, bloque: 0};
|
||||
hotes.forEach(h => { if (h.etat === 'actif') n.actif++; else n.planifie++; if (hoteBloque(h)) n.bloque++; });
|
||||
const defs = [['tous', 'Tous'], ['actif', 'Actifs'], ['planifie', 'Planifiés'], ['bloque', 'Bloqués']];
|
||||
document.getElementById('chips').innerHTML = defs.map(([cle, lib]) =>
|
||||
`<button type="button" class="chip-f ${cle} ${filtreEtat === cle ? 'on' : ''}" onclick="setFiltreEtat('${cle}')">${lib}<span class="n">${n[cle]}</span></button>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function correspond(h) {
|
||||
if (filtreEtat === 'actif' && h.etat !== 'actif') return false;
|
||||
if (filtreEtat === 'planifie' && h.etat === 'actif') return false;
|
||||
if (filtreEtat === 'bloque' && !hoteBloque(h)) return false;
|
||||
if (filtre && ![h.nom, h.adresse_ip, h.vmid, ...(h.groupes || [])].join(' ').toLowerCase().includes(filtre)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function carte(h, index) {
|
||||
const st = statutHote(h);
|
||||
const cls = st === 'bloque' ? 'bloque' : (h.etat === 'actif' ? 'actif' : 'attente');
|
||||
const grps = h.groupes || [];
|
||||
const tags = grps.slice(0, 3).map(g => `<span class="tag">${echapper(abrege(g))}</span>`).join('')
|
||||
+ (grps.length > 3 ? `<span class="tag plus">+${grps.length - 3}</span>` : '');
|
||||
return `
|
||||
<button type="button" class="carte ${cls} ${index === selection ? 'selectionnee' : ''}" id="carte-${index}" onclick="selectionner(${index})">
|
||||
<div class="carte-haut">
|
||||
<span class="badge-statut ${st}">${libelleStatut(st)}</span>
|
||||
${h.vmid ? `<span class="carte-vmid">#${echapper(h.vmid)}</span>` : ''}
|
||||
</div>
|
||||
<div class="carte-nom ${h.nom ? '' : 'sans'}">${echapper(h.nom || '(sans nom)')}</div>
|
||||
<div class="carte-ip">${echapper(h.adresse_ip || '—')}</div>
|
||||
${tags ? `<div class="carte-grps">${tags}</div>` : ''}
|
||||
</button>`;
|
||||
function correspondServeur(s) {
|
||||
if (!filtre) return true;
|
||||
return [s.nom, s.fonction, s.adresse_ip, s.vmid, s.etat].join(' ').toLowerCase().includes(filtre);
|
||||
}
|
||||
|
||||
function setVue(v) {
|
||||
vuePrincipale = v;
|
||||
document.getElementById('btn-vue-cartes').classList.toggle('on', v === 'cartes');
|
||||
if (v === 'serveurs' && selServeur == null && serveurs.length) selServeur = 0;
|
||||
if (v === 'applications' && selApp == null && applications.length) selApp = 0;
|
||||
if (v === 'bases' && selBase == null) selBase = bdServeurs.length ? 'srv:0' : (bdApplis.length ? 'bd:0' : null);
|
||||
document.getElementById('btn-vue-serveurs').classList.toggle('on', v === 'serveurs');
|
||||
document.getElementById('btn-vue-applications').classList.toggle('on', v === 'applications');
|
||||
document.getElementById('btn-vue-bases').classList.toggle('on', v === 'bases');
|
||||
dessinerGrilles();
|
||||
dessinerDetail(); // le panneau droit reflète la sélection de la vue courante
|
||||
majIndicateurSauvegarde();
|
||||
}
|
||||
|
||||
const BANNIERE_MODELE = `<details class="aide-modele">
|
||||
<summary>Comment lire ce parc — serveur · hôte · groupe · application · base</summary>
|
||||
<div class="aide-corps">
|
||||
<p><b>Serveur</b> (ce que tu écris ici) → <b>« Appliquer le plan »</b> → <b>Hôte</b> (la VM générée). C'est le même objet, à deux étapes.</p>
|
||||
<ul>
|
||||
<li><b>Serveur / Hôte</b> — une VM ; elle <b>appartient à des groupes</b>.</li>
|
||||
<li><b>Groupe</b> — un rôle appliqué à l'hôte (ex. <span class="mono">serveur_nginx</span>, <span class="mono">client_dns</span>).</li>
|
||||
<li><b>Application</b> — un service qui <b>tourne sur</b> un hôte, <b>via</b> un groupe, et <b>expose</b> un FQDN.</li>
|
||||
<li><b>Base</b> — une donnée <b>hébergée sur</b> un hôte et <b>consommée par</b> une application.</li>
|
||||
</ul>
|
||||
<p class="aide-ro">Saisis à <b>gauche</b> (tuiles), édite à <b>droite</b> ; les dérivés (VMID/IP/VLAN/groupes) se calculent à « Appliquer le plan ».</p>
|
||||
</div></details>`;
|
||||
|
||||
function dessinerGrilles() {
|
||||
document.getElementById('chips').style.display = vuePrincipale === 'cartes' ? '' : 'none';
|
||||
if (vuePrincipale === 'serveurs') { dessinerServeurs(); return; }
|
||||
if (vuePrincipale === 'applications') { dessinerApplications(); return; }
|
||||
if (vuePrincipale === 'bases') { dessinerBases(); return; }
|
||||
const cible = document.getElementById('grilles');
|
||||
if (!hotes.length) { cible.innerHTML = '<div class="vide"><b>Inventaire vide</b>Ajoutez un hôte pour commencer.</div>'; return; }
|
||||
const visibles = hotes.map((h, i) => [h, i]).filter(([h]) => correspond(h));
|
||||
if (!visibles.length) { cible.innerHTML = '<div class="vide">Aucun hôte ne correspond au filtre.</div>'; return; }
|
||||
const sections = [['Actifs', visibles.filter(([h]) => h.etat === 'actif')], ['Planifiés', visibles.filter(([h]) => h.etat !== 'actif')]];
|
||||
const banniere = '<div class="dsn" style="margin:0 2px 12px;padding:10px 12px;background:#1e293b;border-left:3px solid #38bdf8;border-radius:6px;color:#cbd5e1">🔒 Inventaire <b>généré depuis le plan</b> (lecture seule). Pour modifier : vues <b>Serveurs</b> et <b>Applications</b>, puis <b>« Appliquer le plan »</b>.</div>';
|
||||
cible.innerHTML = banniere + sections.filter(([, l]) => l.length).map(([titre, liste]) => `
|
||||
<div class="section-grille">
|
||||
<div class="section-tete">${titre} <span class="compte">${liste.length}</span></div>
|
||||
<div class="grille-cartes">${liste.map(([h, i]) => carte(h, i)).join('')}</div>
|
||||
</div>`).join('');
|
||||
dessinerServeurs();
|
||||
}
|
||||
|
||||
function basesDeApp(app, hoteNom) {
|
||||
|
|
@ -1046,45 +991,6 @@ HTML = r"""<!doctype html>
|
|||
});
|
||||
}
|
||||
|
||||
function lignesDsn(liste, etiquette) {
|
||||
return liste.length ? `<div class="ch-bases">${liste.map(a => `
|
||||
<div class="ch-base"><span class="badge-bd">${echapper(etiquette(a))}</span> <span class="mono">${echapper(dsnBase(a))}</span></div>`).join('')}</div>` : '';
|
||||
}
|
||||
|
||||
function renduChaine(hote) {
|
||||
const grps = hote.groupes || [];
|
||||
const apps = applications.filter(a => a.hote === hote.nom);
|
||||
if (!grps.length && !apps.length) return '<div class="vide" style="padding:18px">Aucun groupe opérationnel ni application.</div>';
|
||||
const grpHtml = grps.map(g => {
|
||||
const c = chaine[g] || {roles: [], stub: true};
|
||||
const corps = (c.roles && c.roles.length)
|
||||
? `<div class="ch-roles">${c.roles.map(r => `<span class="tag">${echapper(r)}</span>`).join('')}</div>`
|
||||
: '<div class="ch-stub">stub — aucun rôle rattaché</div>';
|
||||
const bg = bdApplis.filter(a => (a.portee || 'groupe') === 'groupe' && a.consommateur === g);
|
||||
return `<div class="ch-grp">
|
||||
<div class="ch-grp-tete"><span>${echapper(g)}</span><span class="ch-fleche">→</span><span class="mono">${echapper(g)}.yml</span></div>
|
||||
${corps}
|
||||
${lignesDsn(bg, a => 'BD partagée ' + (a.usage || ''))}
|
||||
</div>`;
|
||||
}).join('');
|
||||
const ligneLien = (etiquette, valeur) => `<div class="ch-base"><span class="badge-bd">${etiquette}</span> <span class="mono">${echapper(valeur)}</span></div>`;
|
||||
const appHtml = apps.map(app => {
|
||||
const dsns = basesDeApp(app, hote.nom);
|
||||
const liens = [];
|
||||
if (String(app.expose || '').trim()) liens.push(ligneLien('expose', app.expose));
|
||||
if (String(app.requiert || '').trim()) liens.push(ligneLien('requiert', app.requiert));
|
||||
dsns.forEach(a => liens.push(`<div class="ch-base"><span class="badge-bd">BD ${echapper((a.portee || 'groupe') + ' · ' + (a.usage || ''))}</span> <span class="mono">${echapper(dsnBase(a))}</span></div>`));
|
||||
const corps = liens.length ? `<div class="ch-bases">${liens.join('')}</div>` : '<div class="ch-stub">aucun lien</div>';
|
||||
const port = String(app.port || '').trim() ? ` <span class="ch-meta">:${echapper(app.port)}</span>` : '';
|
||||
return `<div class="ch-grp">
|
||||
<div class="ch-grp-tete"><span class="badge-bd">app</span><span>${echapper(app.id)}</span>${port}<span class="ch-fleche">→</span><span class="mono">${echapper(app.groupe)}</span></div>
|
||||
${corps}
|
||||
</div>`;
|
||||
}).join('');
|
||||
const appSection = apps.length ? `<div class="section-tete" style="margin:6px 2px 2px">Applications sur cet hôte <span class="compte">${apps.length}</span></div>${appHtml}` : '';
|
||||
return grpHtml + appSection;
|
||||
}
|
||||
|
||||
function chargerBases(data) {
|
||||
const b = data.bases || {serveurs_bd: {}, bases_donnees: {}};
|
||||
bdServeurs = Object.entries(b.serveurs_bd || {}).map(([nom, s]) => ({
|
||||
|
|
@ -1109,59 +1015,96 @@ HTML = r"""<!doctype html>
|
|||
return `${s.type}://${a.proprietaire || '?'}:****@${s.hote || '?'}:${s.port || '?'}/${a.base || '?'}`;
|
||||
}
|
||||
|
||||
function marquerBasesModifie() {
|
||||
basesModifie = true;
|
||||
const b = document.getElementById('btn-sauver-bases');
|
||||
if (b) { b.classList.toggle('attention', true); b.textContent = 'Sauvegarder les bases •'; }
|
||||
function marquerBasesModifie() { basesModifie = true; majIndicateurSauvegarde(); }
|
||||
function selBaseInfo() { if (!selBase) return null; const p = selBase.split(':'); return {kind: p[0], i: +p[1]}; }
|
||||
function choisirBase(key) { selBase = key; dessiner(); }
|
||||
function definirServeurBd(champ, v) {
|
||||
const info = selBaseInfo(); if (!info || info.kind !== 'srv') return;
|
||||
const s = bdServeurs[info.i]; if (!s) return;
|
||||
s[champ] = v; marquerBasesModifie();
|
||||
if (champ === 'nom') { const t = document.querySelector('#tuile-base-srv-' + info.i + ' .carte-nom'); if (t) t.textContent = v || '(sans nom)'; }
|
||||
}
|
||||
function definirServeurBd(i, champ, v) { bdServeurs[i][champ] = v; marquerBasesModifie(); }
|
||||
function definirBaseBd(i, champ, v) { bdApplis[i][champ] = v; marquerBasesModifie(); if (champ === 'serveur' || champ === 'portee') dessinerBases(); }
|
||||
function ajouterServeurBd() { bdServeurs.push({nom: '', type: 'postgres', hote: '', port: 5432, groupe: ''}); marquerBasesModifie(); dessinerBases(); }
|
||||
function retirerServeurBd(i) { bdServeurs.splice(i, 1); marquerBasesModifie(); dessinerBases(); }
|
||||
function ajouterBaseBd() { bdApplis.push({cle: '', serveur: (bdServeurs[0] || {}).nom || '', base: '', proprietaire: '', secret: '', consommateur: '', portee: 'groupe', usage: 'principale'}); marquerBasesModifie(); dessinerBases(); }
|
||||
function retirerBaseBd(i) { bdApplis.splice(i, 1); marquerBasesModifie(); dessinerBases(); }
|
||||
function definirBaseBd(champ, v) {
|
||||
const info = selBaseInfo(); if (!info || info.kind !== 'bd') return;
|
||||
const a = bdApplis[info.i]; if (!a) return;
|
||||
a[champ] = v; marquerBasesModifie();
|
||||
if (champ === 'cle') { const t = document.querySelector('#tuile-base-bd-' + info.i + ' .carte-nom'); if (t) t.textContent = v || '(sans id)'; }
|
||||
else if (champ === 'serveur' || champ === 'portee') dessinerDetail();
|
||||
}
|
||||
function ajouterServeurBd() { bdServeurs.unshift({nom: '', type: 'postgres', hote: '', port: 5432, groupe: ''}); selBase = 'srv:0'; marquerBasesModifie(); dessiner(); }
|
||||
function retirerServeurBd() { const info = selBaseInfo(); if (!info || info.kind !== 'srv') return; bdServeurs.splice(info.i, 1); selBase = null; marquerBasesModifie(); dessiner(); }
|
||||
function ajouterBaseBd() { bdApplis.unshift({cle: '', serveur: (bdServeurs[0] || {}).nom || '', base: '', proprietaire: '', secret: '', consommateur: '', portee: 'groupe', usage: 'principale'}); selBase = 'bd:0'; marquerBasesModifie(); dessiner(); }
|
||||
function retirerBaseBd() { const info = selBaseInfo(); if (!info || info.kind !== 'bd') return; bdApplis.splice(info.i, 1); selBase = null; marquerBasesModifie(); dessiner(); }
|
||||
|
||||
function carteBaseServeur(s, i) {
|
||||
const key = 'srv:' + i;
|
||||
return `<button type="button" class="carte ${selBase === key ? 'selectionnee' : ''}" id="tuile-base-srv-${i}" onclick="choisirBase('${key}')">
|
||||
<div class="carte-haut"><span class="badge-bd">${echapper(s.type || 'bd')}</span>${s.port ? `<span class="carte-vmid">:${echapper(s.port)}</span>` : ''}</div>
|
||||
<div class="carte-nom ${s.nom ? '' : 'sans'}">${echapper(s.nom || '(sans nom)')}</div>
|
||||
<div class="carte-ip">${echapper(s.hote || '— hôte ?')}</div>
|
||||
</button>`;
|
||||
}
|
||||
function carteBaseDonnee(a, i) {
|
||||
const key = 'bd:' + i;
|
||||
return `<button type="button" class="carte ${selBase === key ? 'selectionnee' : ''}" id="tuile-base-bd-${i}" onclick="choisirBase('${key}')">
|
||||
<div class="carte-haut"><span class="badge-bd">${echapper(a.portee || 'groupe')}</span></div>
|
||||
<div class="carte-nom ${a.cle ? '' : 'sans'}">${echapper(a.cle || '(sans id)')}</div>
|
||||
<div class="carte-ip">${echapper(a.base || '—')} @ ${echapper(a.serveur || '?')}</div>
|
||||
</button>`;
|
||||
}
|
||||
function dessinerBases() {
|
||||
const cible = document.getElementById('grilles');
|
||||
const serveurs = bdServeurs.length ? bdServeurs.map((s, i) => `
|
||||
<div class="base-ligne">
|
||||
<label class="champ"><span>Nom</span><input value="${echapper(s.nom)}" oninput="definirServeurBd(${i}, 'nom', this.value)"></label>
|
||||
<label class="champ"><span>Type</span><input value="${echapper(s.type)}" oninput="definirServeurBd(${i}, 'type', this.value)"></label>
|
||||
<label class="champ"><span>Hôte</span><input value="${echapper(s.hote)}" oninput="definirServeurBd(${i}, 'hote', this.value)"></label>
|
||||
<label class="champ"><span>Port</span><input type="number" value="${echapper(s.port)}" oninput="definirServeurBd(${i}, 'port', this.value)"></label>
|
||||
<label class="champ"><span>Groupe Ansible</span><input value="${echapper(s.groupe)}" placeholder="serveur_postgresql" oninput="definirServeurBd(${i}, 'groupe', this.value)"></label>
|
||||
<button type="button" class="danger" title="Retirer" onclick="retirerServeurBd(${i})">✕</button>
|
||||
</div>`).join('') : '<div class="vide" style="padding:14px">Aucun serveur de BD.</div>';
|
||||
const sourceConso = (portee) => portee === 'hote' ? hotes.map(h => h.nom).filter(Boolean)
|
||||
: portee === 'application' ? applications.map(a => a.id)
|
||||
: groupes;
|
||||
const optConso = (portee, sel) => '<option value="">—</option>' + sourceConso(portee).map(v => `<option value="${echapper(v)}" ${v === sel ? 'selected' : ''}>${echapper(v)}</option>`).join('');
|
||||
const optPortee = (sel) => ['groupe', 'application', 'hote'].map(p => `<option value="${p}" ${p === sel ? 'selected' : ''}>${p}</option>`).join('');
|
||||
const applis = bdApplis.length ? bdApplis.map((a, i) => `
|
||||
<div class="base-ligne">
|
||||
<label class="champ"><span>Identifiant</span><input value="${echapper(a.cle)}" oninput="definirBaseBd(${i}, 'cle', this.value)"></label>
|
||||
<label class="champ"><span>Portée</span><select onchange="definirBaseBd(${i}, 'portee', this.value)">${optPortee(a.portee || 'groupe')}</select></label>
|
||||
<label class="champ"><span>Consommateur</span><select onchange="definirBaseBd(${i}, 'consommateur', this.value)">${optConso(a.portee || 'groupe', a.consommateur)}</select></label>
|
||||
<label class="champ"><span>Usage</span><input value="${echapper(a.usage)}" placeholder="principale" oninput="definirBaseBd(${i}, 'usage', this.value)"></label>
|
||||
<label class="champ"><span>Serveur</span><select onchange="definirBaseBd(${i}, 'serveur', this.value)">
|
||||
${bdServeurs.map(s => `<option value="${echapper(s.nom)}" ${s.nom === a.serveur ? 'selected' : ''}>${echapper(s.nom)}</option>`).join('')}
|
||||
</select></label>
|
||||
<label class="champ"><span>Base</span><input value="${echapper(a.base)}" oninput="definirBaseBd(${i}, 'base', this.value)"></label>
|
||||
<label class="champ"><span>Propriétaire</span><input value="${echapper(a.proprietaire)}" oninput="definirBaseBd(${i}, 'proprietaire', this.value)"></label>
|
||||
<label class="champ"><span>Secret (Vault)</span><input value="${echapper(a.secret)}" placeholder="vault_bd_app" oninput="definirBaseBd(${i}, 'secret', this.value)"></label>
|
||||
<button type="button" class="danger" title="Retirer" onclick="retirerBaseBd(${i})">✕</button>
|
||||
<div class="dsn mono">${echapper(a.portee || 'groupe')} · ${echapper(a.consommateur || '—')} · ${echapper(dsnBase(a))}</div>
|
||||
</div>`).join('') : '<div class="vide" style="padding:14px">Aucune base applicative.</div>';
|
||||
cible.innerHTML = `
|
||||
<div class="section-grille">
|
||||
<div class="section-tete">Serveurs de bases de données <button type="button" class="fantome" style="margin-left:auto" onclick="ajouterServeurBd()">+ Serveur</button></div>
|
||||
<div class="bases-liste">${serveurs}</div>
|
||||
</div>
|
||||
<div class="section-grille">
|
||||
<div class="section-tete">Bases applicatives <button type="button" class="fantome" style="margin-left:auto" onclick="ajouterBaseBd()">+ Base</button></div>
|
||||
<div class="bases-liste">${applis}</div>
|
||||
</div>
|
||||
<div style="margin:4px 2px 20px"><button type="button" class="primaire ${basesModifie ? 'attention' : ''}" id="btn-sauver-bases" onclick="sauvegarderBases()">Sauvegarder les bases${basesModifie ? ' •' : ''}</button></div>`;
|
||||
const srv = bdServeurs.length ? `<div class="grille-cartes">${bdServeurs.map((s, i) => carteBaseServeur(s, i)).join('')}</div>` : '<div class="vide" style="padding:12px">Aucun serveur de BD.</div>';
|
||||
const bd = bdApplis.length ? `<div class="grille-cartes">${bdApplis.map((a, i) => carteBaseDonnee(a, i)).join('')}</div>` : '<div class="vide" style="padding:12px">Aucune base.</div>';
|
||||
cible.innerHTML = `<div class="section-grille"><div class="section-tete">Serveurs de bases <span class="compte">${bdServeurs.length}</span><button type="button" class="fantome" style="margin-left:auto" onclick="ajouterServeurBd()">+ Serveur BD</button></div>${srv}</div>
|
||||
<div class="section-grille"><div class="section-tete">Bases applicatives <span class="compte">${bdApplis.length}</span><button type="button" class="fantome" style="margin-left:auto" onclick="ajouterBaseBd()">+ Base</button></div>${bd}</div>`;
|
||||
}
|
||||
function inspecteurBase() {
|
||||
const cible = document.getElementById('detail');
|
||||
const info = selBaseInfo();
|
||||
if (!info) { cible.innerHTML = '<div class="detail"><div class="vide"><b>Aucun élément sélectionné</b>Choisis un serveur de base ou une base à gauche.</div></div>'; return; }
|
||||
if (info.kind === 'srv') {
|
||||
const s = bdServeurs[info.i];
|
||||
if (!s) { cible.innerHTML = '<div class="detail"><div class="vide">Introuvable.</div></div>'; return; }
|
||||
const optHote = '<option value="">—</option>' + hotes.map(h => h.nom).filter(Boolean).map(n => `<option value="${echapper(n)}" ${n === s.hote ? 'selected' : ''}>${echapper(n)}</option>`).join('');
|
||||
const optGroupe = '<option value="">—</option>' + groupes.map(g => `<option value="${echapper(g)}" ${g === s.groupe ? 'selected' : ''}>${echapper(g)}</option>`).join('');
|
||||
const ici = bdApplis.filter(a => a.serveur === s.nom);
|
||||
const iciHtml = ici.length ? `<div class="ch-bases">${ici.map(a => `<div class="ch-base"><span class="badge-bd">${echapper(a.portee || 'groupe')}</span> <span class="mono">${echapper(a.base || '?')}</span> (${echapper(a.cle)})</div>`).join('')}</div>` : '<div class="ch-stub">aucune base sur ce serveur</div>';
|
||||
cible.innerHTML = `<article class="detail">
|
||||
<div class="detail-tete"><div class="detail-nom"><span class="detail-nom-txt">${echapper(s.nom || '(sans nom)')}</span></div><span class="grandit"></span><span class="badge-bd">${echapper(s.type || 'bd')}</span><button type="button" class="danger" title="Retirer" onclick="retirerServeurBd()">✕</button></div>
|
||||
<div class="grille">
|
||||
<label class="champ"><span>Nom</span><input value="${echapper(s.nom)}" placeholder="pg-principal" oninput="definirServeurBd('nom', this.value)"></label>
|
||||
<label class="champ"><span>Type</span><input value="${echapper(s.type)}" placeholder="postgres" oninput="definirServeurBd('type', this.value)"></label>
|
||||
<label class="champ"><span>Hôte (VM)</span><select onchange="definirServeurBd('hote', this.value)">${optHote}</select></label>
|
||||
<label class="champ"><span>Port</span><input type="number" value="${echapper(s.port)}" oninput="definirServeurBd('port', this.value)"></label>
|
||||
<label class="champ"><span>Groupe Ansible</span><select onchange="definirServeurBd('groupe', this.value)">${optGroupe}</select></label>
|
||||
</div>
|
||||
<div class="section-tete" style="margin:12px 2px 2px">Bases hébergées ici</div>${iciHtml}
|
||||
<div class="pied"><span class="hint-dep">Sauvegarde (Ctrl+S) puis « Appliquer le plan ».</span></div>
|
||||
</article>`;
|
||||
} else {
|
||||
const a = bdApplis[info.i];
|
||||
if (!a) { cible.innerHTML = '<div class="detail"><div class="vide">Introuvable.</div></div>'; return; }
|
||||
const source = a.portee === 'hote' ? hotes.map(h => h.nom).filter(Boolean) : a.portee === 'application' ? applications.map(x => x.id) : groupes;
|
||||
const optConso = '<option value="">—</option>' + source.map(v => `<option value="${echapper(v)}" ${v === a.consommateur ? 'selected' : ''}>${echapper(v)}</option>`).join('');
|
||||
const optPortee = ['groupe', 'application', 'hote'].map(p => `<option value="${p}" ${p === (a.portee || 'groupe') ? 'selected' : ''}>${p}</option>`).join('');
|
||||
const optServeur = '<option value="">—</option>' + bdServeurs.map(x => `<option value="${echapper(x.nom)}" ${x.nom === a.serveur ? 'selected' : ''}>${echapper(x.nom)}</option>`).join('');
|
||||
cible.innerHTML = `<article class="detail">
|
||||
<div class="detail-tete"><div class="detail-nom"><span class="detail-nom-txt">${echapper(a.cle || '(sans id)')}</span></div><span class="grandit"></span><button type="button" class="danger" title="Retirer" onclick="retirerBaseBd()">✕</button></div>
|
||||
<div class="grille">
|
||||
<label class="champ"><span>Identifiant</span><input value="${echapper(a.cle)}" placeholder="bd_forgejo" oninput="definirBaseBd('cle', this.value)"></label>
|
||||
<label class="champ"><span>Portée</span><select onchange="definirBaseBd('portee', this.value)">${optPortee}</select></label>
|
||||
<label class="champ"><span>Consommateur</span><select onchange="definirBaseBd('consommateur', this.value)">${optConso}</select></label>
|
||||
<label class="champ"><span>Serveur</span><select onchange="definirBaseBd('serveur', this.value)">${optServeur}</select></label>
|
||||
<label class="champ"><span>Base</span><input value="${echapper(a.base)}" placeholder="forgejo" oninput="definirBaseBd('base', this.value)"></label>
|
||||
<label class="champ"><span>Propriétaire</span><input value="${echapper(a.proprietaire)}" oninput="definirBaseBd('proprietaire', this.value)"></label>
|
||||
<label class="champ"><span>Usage</span><input value="${echapper(a.usage)}" placeholder="principale" oninput="definirBaseBd('usage', this.value)"></label>
|
||||
<label class="champ"><span>Secret (Vault)</span><input value="${echapper(a.secret)}" placeholder="vault_bd_app" oninput="definirBaseBd('secret', this.value)"></label>
|
||||
</div>
|
||||
<div class="dsn mono" style="margin-top:8px">${echapper(dsnBase(a))}</div>
|
||||
<div class="pied"><span class="hint-dep">Sauvegarde (Ctrl+S) puis « Appliquer le plan ».</span></div>
|
||||
</article>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function sauvegarderBases() {
|
||||
|
|
@ -1179,39 +1122,74 @@ HTML = r"""<!doctype html>
|
|||
message('Bases de données sauvegardées.', 'ok');
|
||||
}
|
||||
|
||||
function marquerApplicationsModifie() {
|
||||
applicationsModifie = true;
|
||||
const b = document.getElementById('btn-sauver-applications');
|
||||
if (b) { b.classList.add('attention'); b.textContent = 'Sauvegarder les applications •'; }
|
||||
function marquerApplicationsModifie() { applicationsModifie = true; majIndicateurSauvegarde(); }
|
||||
function choisirApp(i) { selApp = i; dessiner(); }
|
||||
function definirApplication(champ, v) {
|
||||
const a = applications[selApp]; if (!a) return;
|
||||
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 === 'hote' || champ === 'groupe' || champ === 'port') dessinerDetail();
|
||||
}
|
||||
function ajouterApplication() {
|
||||
applications.unshift({id: '', groupe: (groupes[0] || ''), hote: '', port: '', requiert: '', expose: ''});
|
||||
selApp = 0; marquerApplicationsModifie(); dessiner();
|
||||
const c = document.querySelector('#detail .grille input'); if (c) c.focus();
|
||||
}
|
||||
function retirerApplication() {
|
||||
const a = applications[selApp]; if (!a) return;
|
||||
if (a.id && !confirm('Retirer l\'application « ' + a.id + ' » ?')) return;
|
||||
applications.splice(selApp, 1);
|
||||
selApp = applications.length ? Math.min(selApp, applications.length - 1) : null;
|
||||
marquerApplicationsModifie(); dessiner();
|
||||
}
|
||||
function definirApplication(i, champ, v) { applications[i][champ] = v; marquerApplicationsModifie(); if (champ === 'hote' || champ === 'groupe') dessinerApplications(); }
|
||||
function ajouterApplication() { applications.push({id: '', groupe: (groupes[0] || ''), hote: '', port: '', requiert: '', expose: ''}); marquerApplicationsModifie(); dessinerApplications(); }
|
||||
function retirerApplication(i) { applications.splice(i, 1); marquerApplicationsModifie(); dessinerApplications(); }
|
||||
|
||||
function carteApp(a, i) {
|
||||
const exp = listeDepuisTexte(a.expose);
|
||||
return `<button type="button" class="carte ${i === selApp ? 'selectionnee' : ''}" id="tuile-app-${i}" onclick="choisirApp(${i})">
|
||||
<div class="carte-haut"><span class="badge-bd">app</span>${a.port ? `<span class="carte-vmid">:${echapper(a.port)}</span>` : ''}</div>
|
||||
<div class="carte-nom ${a.id ? '' : 'sans'}">${echapper(a.id || '(sans id)')}</div>
|
||||
<div class="carte-ip">${echapper(a.hote || '— hôte ?')}</div>
|
||||
${exp.length ? `<div class="carte-grps"><span class="tag">${echapper(exp[0])}</span>${exp.length > 1 ? `<span class="tag plus">+${exp.length - 1}</span>` : ''}</div>` : ''}
|
||||
</button>`;
|
||||
}
|
||||
function dessinerApplications() {
|
||||
const cible = document.getElementById('grilles');
|
||||
const optGroupe = (sel) => groupes.map(g => `<option value="${echapper(g)}" ${g === sel ? 'selected' : ''}>${echapper(g)}</option>`).join('');
|
||||
const optHote = (sel) => '<option value="">—</option>' + hotes.map(h => h.nom).filter(Boolean).map(n => `<option value="${echapper(n)}" ${n === sel ? 'selected' : ''}>${echapper(n)}</option>`).join('');
|
||||
const lignes = applications.length ? applications.map((a, i) => {
|
||||
const dsns = basesDeApp(a, a.hote);
|
||||
const resume = dsns.length ? dsns.map(b => `<span class="mono">${echapper((b.portee || 'groupe'))}:${echapper(dsnBase(b))}</span>`).join('<br>') : '<span class="ch-stub">aucune base liée</span>';
|
||||
return `<div class="base-ligne">
|
||||
<label class="champ"><span>Identifiant</span><input value="${echapper(a.id)}" placeholder="boutique" oninput="definirApplication(${i}, 'id', this.value)"></label>
|
||||
<label class="champ"><span>Groupe (rôle)</span><select onchange="definirApplication(${i}, 'groupe', this.value)">${optGroupe(a.groupe)}</select></label>
|
||||
<label class="champ"><span>Hôte (VM)</span><select onchange="definirApplication(${i}, 'hote', this.value)">${optHote(a.hote)}</select></label>
|
||||
<label class="champ"><span>Port</span><input type="number" value="${echapper(a.port)}" placeholder="3000" oninput="definirApplication(${i}, 'port', this.value)"></label>
|
||||
<label class="champ"><span>Requiert (applis)</span><input value="${echapper(a.requiert)}" placeholder="pg-principal, …" oninput="definirApplication(${i}, 'requiert', this.value)"></label>
|
||||
<label class="champ"><span>Expose (FQDN)</span><input value="${echapper(a.expose)}" placeholder="forge.exemple.ca" oninput="definirApplication(${i}, 'expose', this.value)"></label>
|
||||
<button type="button" class="danger" title="Retirer" onclick="retirerApplication(${i})">✕</button>
|
||||
<div class="dsn">${resume}</div>
|
||||
</div>`;
|
||||
}).join('') : '<div class="vide" style="padding:14px">Aucune application. Une VM peut en porter plusieurs.</div>';
|
||||
cible.innerHTML = `
|
||||
<div class="section-grille">
|
||||
<div class="section-tete">Applications <span class="compte">${applications.length}</span><button type="button" class="fantome" style="margin-left:auto" onclick="ajouterApplication()">+ Application</button></div>
|
||||
<div class="bases-liste">${lignes}</div>
|
||||
const tuiles = applications.length
|
||||
? `<div class="grille-cartes">${applications.map((a, i) => carteApp(a, i)).join('')}</div>`
|
||||
: '<div class="vide" style="padding:14px">Aucune application. Une VM peut en porter plusieurs.</div>';
|
||||
cible.innerHTML = `<div class="section-grille">
|
||||
<div class="section-tete">Applications <span class="compte">${applications.length}</span><button type="button" class="fantome" style="margin-left:auto" onclick="ajouterApplication()">+ Application</button></div>
|
||||
${tuiles}</div>`;
|
||||
}
|
||||
function inspecteurApplication() {
|
||||
const cible = document.getElementById('detail');
|
||||
if (selApp == null || !applications[selApp]) {
|
||||
cible.innerHTML = '<div class="detail"><div class="vide"><b>Aucune application sélectionnée</b>Choisis une tuile à gauche, ou ajoute une application.</div></div>';
|
||||
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>`
|
||||
: '<div class="ch-stub">aucune base liée</div>';
|
||||
cible.innerHTML = `<article class="detail">
|
||||
<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" 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>
|
||||
<div style="margin:4px 2px 20px"><button type="button" class="primaire ${applicationsModifie ? 'attention' : ''}" id="btn-sauver-applications" onclick="sauvegarderApplications()">Sauvegarder les applications${applicationsModifie ? ' •' : ''}</button></div>`;
|
||||
<div class="section-tete" style="margin:12px 2px 4px">Bases consommées</div>
|
||||
${liens}
|
||||
<div class="pied"><span class="hint-dep">Sauvegarde (Ctrl+S) puis « Appliquer le plan » pour régénérer.</span></div>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
function listeDepuisTexte(v) { return String(v || '').split(',').map(s => s.trim()).filter(Boolean); }
|
||||
|
|
@ -1249,64 +1227,128 @@ HTML = r"""<!doctype html>
|
|||
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' ? '<span class="tag plus" title="Le plan diverge de l\'inventaire généré">⚠ divergent</span>'
|
||||
: s.statut === 'absent' ? '<span class="tag" title="Déclaré, pas encore instancié">○ non instancié</span>' : '';
|
||||
return `<button type="button" class="carte ${actif ? 'actif' : 'attente'} ${i === selServeur ? 'selectionnee' : ''}" id="tuile-srv-${i}" onclick="choisirServeur(${i})">
|
||||
<div class="carte-haut"><span class="badge-statut ${actif ? 'ok' : 'attente'}">${actif ? 'actif' : 'planifié'}</span>${s.vmid ? `<span class="carte-vmid">#${echapper(s.vmid)}</span>` : ''}</div>
|
||||
<div class="carte-nom ${s.nom ? '' : 'sans'}">${echapper(s.nom || '(sans nom)')}</div>
|
||||
<div class="carte-ip">${echapper(s.adresse_ip || '—')}</div>
|
||||
${recon ? `<div class="carte-grps">${recon}</div>` : ''}
|
||||
</button>`;
|
||||
}
|
||||
function dessinerServeurs() {
|
||||
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 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>
|
||||
<button type="button" class="fantome" onclick="appliquerPlan()">⚙ Appliquer le plan</button></div></div>`;
|
||||
if (!serveurs.length) { cible.innerHTML = entete + '<div class="vide" style="padding:14px">Aucun serveur. « + Serveur » ou <span class="mono">make serveurs-bootstrap</span>.</div>'; 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]) =>
|
||||
`<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() {
|
||||
const cible = document.getElementById('detail');
|
||||
if (selServeur == null || !serveurs[selServeur]) {
|
||||
hoteOpCourant = '';
|
||||
cible.innerHTML = '<div class="detail"><div class="vide"><b>Aucun serveur sélectionné</b>Choisis une tuile à gauche, ou ajoute un serveur.</div></div>';
|
||||
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 `<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><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" 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>`;
|
||||
}).join('') : '<div class="vide" style="padding:14px">Aucun serveur. Utilise « + Serveur » ou make serveurs-bootstrap.</div>';
|
||||
cible.innerHTML = `
|
||||
<div class="section-grille">
|
||||
<div class="section-tete">Serveurs du plan <span class="compte">${serveurs.length}</span><button type="button" class="fantome" style="margin-left:auto" onclick="ajouterServeur()">+ Serveur</button></div>
|
||||
<div class="bases-liste">${lignes}</div>
|
||||
<div style="margin:4px 2px 8px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||
<button type="button" class="primaire ${serveursModifie ? 'attention' : ''}" id="btn-sauver-serveurs" onclick="sauvegarderServeurs()">Sauvegarder les serveurs${serveursModifie ? ' •' : ''}</button>
|
||||
<button type="button" class="fantome" onclick="appliquerPlan()">⚙ Appliquer le plan → hosts.yml</button>
|
||||
</div>
|
||||
<div class="dsn" style="padding:6px 2px;color:#94a3b8">VMID / IP / VLAN sont <b>dérivés</b> de la fonction. « Appliquer le plan » régénère l'inventaire (refuse si conflit ; git est le filet).</div>
|
||||
</div>`;
|
||||
const optFonction = Object.keys(nomenclature.fonctions || {}).map(f => `<option value="${echapper(f)}" ${f === s.fonction ? 'selected' : ''}>${echapper(f)}</option>`).join('');
|
||||
const optEtat = ['planifie', 'actif'].map(e => `<option value="${e}" ${e === s.etat ? 'selected' : ''}>${e}</option>`).join('');
|
||||
const optDeListe = (liste, sel, lib) => `<option value="" ${sel ? '' : 'selected'}>${echapper(lib)}</option>`
|
||||
+ [...new Set([...(liste || []), ...(sel ? [sel] : [])])].map(v => `<option value="${echapper(v)}" ${v === sel ? 'selected' : ''}>${echapper(v)}</option>`).join('');
|
||||
const set = new Set(listeDepuisTexte(s.integrations));
|
||||
const tousRoles = [...new Set([...integrationsDispo, ...set])];
|
||||
const cases = tousRoles.length
|
||||
? tousRoles.map(role => `<label class="integ-case" title="${echapper(role)}"><input type="checkbox" ${set.has(role) ? 'checked' : ''} onchange="basculerIntegration('${echapper(role)}', this.checked)"><span>${echapper(role.replace(/^client_/, ''))}</span></label>`).join('')
|
||||
: '<span class="dsn">aucun rôle client_* disponible</span>';
|
||||
const info = infoFonction(s.fonction);
|
||||
const zoneHint = info ? `<span class="zone-hint" title="Dérivé de la nomenclature">${echapper(info.libelle)} · VLAN ${info.vlan}</span>` : '';
|
||||
const h = hotes.find(x => x.nom === s.nom);
|
||||
const grpHtml = h && (h.groupes || []).length
|
||||
? `<div class="groupes-liste">${h.groupes.map(g => renduGroupe(h, g)).join('')}</div>`
|
||||
: `<div class="ch-stub">${s.statut === 'absent' ? 'Pas encore instancié — « Appliquer le plan ».' : 'Aucun groupe.'}</div>`;
|
||||
const apps = applications.filter(a => a.hote === s.nom);
|
||||
const appsHtml = apps.length
|
||||
? `<div class="ch-bases">${apps.map(a => `<div class="ch-base"><span class="badge-bd">app</span> <button type="button" class="lien-objet" onclick="ouvrirApp('${echapper(a.id)}')">${echapper(a.id || '(sans id)')}</button>${a.expose ? ` <span class="mono">→ ${echapper(listeDepuisTexte(a.expose).join(', '))}</span>` : ''}</div>`).join('')}</div>`
|
||||
: '<div class="ch-stub">aucune application sur cet hôte</div>';
|
||||
const bds = bdServeurs.filter(x => x.hote === s.nom);
|
||||
const bdHtml = bds.length
|
||||
? `<div class="ch-bases">${bds.map(x => `<div class="ch-base"><span class="badge-bd">${echapper(x.type)}</span> <button type="button" class="lien-objet" onclick="ouvrirBaseServeur('${echapper(x.nom)}')">${echapper(x.nom)}</button> <span class="mono">:${echapper(x.port)}</span></div>`).join('')}</div>`
|
||||
: '<div class="ch-stub">aucun serveur de base ici</div>';
|
||||
hoteOpCourant = s.nom;
|
||||
let pied;
|
||||
if (h && h.etat === 'actif' && !hoteBloque(h) && estProduction) {
|
||||
const hi = hotes.indexOf(h);
|
||||
pied = `<div class="pied"><span class="grandit"></span>
|
||||
<button type="button" id="btn-verifier" onclick="lancer('verifier', ${hi})">Vérifier</button>
|
||||
<button type="button" id="btn-deployer" class="primaire" onclick="lancer('deployer', ${hi})">Déployer</button></div>`;
|
||||
} 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 = `<div class="pied"><span class="hint-dep">${txt}</span></div>`;
|
||||
}
|
||||
const statBadge = s.statut === 'divergence' ? '<span class="badge-statut bloque">divergent</span>'
|
||||
: s.statut === 'absent' ? '<span class="badge-statut attente">non instancié</span>'
|
||||
: '<span class="badge-statut ok">réconcilié</span>';
|
||||
cible.innerHTML = `<article class="detail">
|
||||
<div class="detail-tete"><div class="detail-nom"><span class="detail-nom-txt">${echapper(s.nom || '(sans nom)')}</span>${zoneHint}</div>
|
||||
<span class="grandit"></span>${statBadge}
|
||||
<button type="button" class="danger" title="Retirer ce serveur" onclick="retirerServeur()">✕</button></div>
|
||||
<div class="stats">${tuile(s.vmid, 'VMID')}${tuile(s.adresse_ip, 'IP')}${tuile(s.vlan, 'VLAN')}</div>
|
||||
<div class="section-tete" style="margin:4px 2px 2px">Identité — saisie du plan (surcharge les défauts)</div>
|
||||
<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">${cases}</div></label>
|
||||
</div>
|
||||
<div class="section-tete" style="margin:12px 2px 2px">Groupes (dérivés)</div>${grpHtml}
|
||||
<div class="section-tete" style="margin:12px 2px 2px">Applications ici</div>${appsHtml}
|
||||
<div class="section-tete" style="margin:12px 2px 2px">Bases ici</div>${bdHtml}
|
||||
<div class="dsn" style="padding:6px 2px;color:#94a3b8">VMID / IP / VLAN sont <b>dérivés</b>. Sauvegarde (Ctrl+S) puis « Appliquer le plan » pour régénérer l'inventaire.</div>
|
||||
${pied}
|
||||
</article>`;
|
||||
majBoutonsDeploiement();
|
||||
}
|
||||
|
||||
function serveurValeurValide(cle, v) {
|
||||
|
|
@ -1325,7 +1367,7 @@ HTML = r"""<!doctype html>
|
|||
}
|
||||
|
||||
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"""<!doctype html>
|
|||
}).join('');
|
||||
}
|
||||
|
||||
function champLecture(h, cle, libelle, opts={}) {
|
||||
const brut = h[cle];
|
||||
const v = (brut === '' || brut == null) ? '—' : String(brut);
|
||||
const unite = opts.unite ? ` <span class="unite">· ${echapper(opts.unite)}</span>` : '';
|
||||
return `<label class="champ"><span>${echapper(libelle)}${unite}</span>
|
||||
<div class="champ-val ${v === '—' ? 'vide' : ''}">${echapper(v)}</div></label>`;
|
||||
}
|
||||
|
||||
function tuile(valeur, libelle) {
|
||||
const v = (valeur === '' || valeur == null) ? '—' : String(valeur);
|
||||
return `<div class="stat"><div class="v ${v === '—' ? 'vide' : ''}">${echapper(v)}</div><div class="l">${echapper(libelle)}</div></div>`;
|
||||
}
|
||||
|
||||
function dessinerDetail() {
|
||||
const cible = document.getElementById('detail');
|
||||
if (selection < 0 || !hotes[selection]) {
|
||||
cible.innerHTML = '<div class="detail"><div class="vide"><b>Aucun hôte sélectionné</b>Choisissez une carte pour l\'inspecter.</div></div>';
|
||||
return;
|
||||
}
|
||||
const index = selection, h = hotes[index];
|
||||
const actif = h.etat === 'actif';
|
||||
const info = infoFonction(fonctionDe(h.nom).fonction);
|
||||
const zoneHint = info ? `<span class="zone-hint" title="Dérivé de la nomenclature">${echapper(info.libelle)} · VLAN ${info.vlan}</span>` : '';
|
||||
const st = statutHote(h);
|
||||
const libelle = actif ? (hoteBloque(h) ? 'prérequis manquant' : 'déployable') : 'planifié';
|
||||
let pied;
|
||||
if (actif && !hoteBloque(h) && estProduction) {
|
||||
pied = `<div class="pied"><span class="grandit"></span>
|
||||
<button type="button" id="btn-verifier" onclick="lancer('verifier', ${index})">Vérifier</button>
|
||||
<button type="button" id="btn-deployer" class="primaire" onclick="lancer('deployer', ${index})">Déployer</button></div>`;
|
||||
} else {
|
||||
const txt = !actif ? 'Hôte actif requis pour déployer' : (!estProduction ? 'Déploiement : inventaire production requis' : 'Prérequis manquant');
|
||||
pied = `<div class="pied"><span class="hint-dep">${txt}</span><span class="grandit"></span><span class="badge-statut ${st}">${libelle}</span></div>`;
|
||||
}
|
||||
const onglet = ONGLETS[ongletActif];
|
||||
let corps;
|
||||
if (onglet) {
|
||||
corps = `<div class="grille">${onglet.map(([c, l, o]) => champLecture(h, c, l, o)).join('')}</div>`;
|
||||
} else if (ongletActif === 'chaine') {
|
||||
corps = renduChaine(h);
|
||||
} else {
|
||||
const gs = h.groupes || [];
|
||||
corps = gs.length
|
||||
? `<div class="groupes-liste">${gs.map(g => renduGroupe(h, g)).join('')}</div>`
|
||||
: '<div class="vide" style="padding:14px">Aucun groupe rattaché à cet hôte.</div>';
|
||||
}
|
||||
cible.innerHTML = `
|
||||
<article class="detail ${hoteBloque(h) ? 'bloque' : ''}">
|
||||
<div class="detail-tete">
|
||||
<div class="detail-nom"><span class="detail-nom-txt">${echapper(h.nom || '(sans nom)')}</span>${zoneHint}</div>
|
||||
<span class="grandit"></span>
|
||||
<span class="badge-statut ${st}" title="État défini dans le plan (vue Serveurs)">${libelleStatut(st)}</span>
|
||||
</div>
|
||||
<div class="stats">
|
||||
${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')}
|
||||
</div>
|
||||
<div class="onglets">
|
||||
<button type="button" class="onglet ${ongletActif === 'reseau' ? 'actif' : ''}" onclick="setOnglet('reseau')">Réseau</button>
|
||||
<button type="button" class="onglet ${ongletActif === 'proxmox' ? 'actif' : ''}" onclick="setOnglet('proxmox')">Proxmox</button>
|
||||
<button type="button" class="onglet ${ongletActif === 'groupes' ? 'actif' : ''}" onclick="setOnglet('groupes')">Groupes</button>
|
||||
<button type="button" class="onglet ${ongletActif === 'chaine' ? 'actif' : ''}" onclick="setOnglet('chaine')">Chaîne</button>
|
||||
</div>
|
||||
<div class="onglet-corps">${corps}</div>
|
||||
${pied}
|
||||
</article>`;
|
||||
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"""<!doctype html>
|
|||
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"""<!doctype html>
|
|||
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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue