GUI : vue Reseau (devis switch) + bouton Copier

Nouvel onglet lecture seule affichant le devis make devis-reseau (VLANs+SVIs+ACLs)
via GET /api/devis-reseau, avec bouton Copier. node --check OK, endpoint eprouve
(2 tenants, 121 lignes). Un operateur genere et transmet la config sans CLI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-07-07 15:57:47 -04:00
parent 97670dd27c
commit 0580c931a7
2 changed files with 46 additions and 1 deletions

View file

@ -16,6 +16,10 @@
émet la config Cisco-like du réseau convergé — **VLANs + SVIs (passerelles) + ACLs d'isolation
inter-tenant** — dérivée des nomenclatures, jamais saisie à la main (toujours synchrone avec
le plan). One-shot précédemment ; maintenant reproductible.
- **GUI : vue « Réseau » (devis switch) + bouton Copier.** Nouvel onglet lecture seule qui
affiche le devis `make devis-reseau` (VLANs + SVIs + ACLs), via `GET /api/devis-reseau`, avec
un bouton **Copier** (prêt à coller sur le switch). Un opérateur génère et transmet la config
sans CLI.
- **GUI : erreurs de déploiement en LANGAGE CLAIR.** Quand une action (créer/vérifier/déployer)
échoue, le GUI n'oblige plus à lire le dump Ansible : un encadré résume les tâches en erreur —
**étape · hôte · type · message clair** — avec un bouton **« Copier »** (pour transmettre le

View file

@ -20,6 +20,7 @@ from urllib.parse import urlparse
import yaml
import devis_reseau
from inventory_rules import (
GROUPE_HOTES_ACTIFS,
GROUPE_HOTES_PLANIFIES,
@ -916,6 +917,8 @@ HTML = r"""<!doctype html>
.re-hote { color: var(--teal); font-weight: 700; }
.re-type { color: var(--muted); font-size: 11px; }
.re-msg { background: var(--surface-3); border-radius: 6px; padding: 4px 8px; margin-top: 3px; font-family: ui-monospace, monospace; font-size: 11.5px; white-space: pre-wrap; word-break: break-word; }
/* Devis réseau (config switch) */
.devis-reseau { margin: 8px 0 0; padding: 12px 14px; background: var(--surface-3); border-radius: 8px; font-family: ui-monospace, monospace; font-size: 11.5px; line-height: 1.5; white-space: pre; overflow: auto; max-height: calc(100vh - 210px); }
</style>
</head>
<body>
@ -933,6 +936,7 @@ HTML = r"""<!doctype html>
<button type="button" id="btn-vue-domaines" onclick="setVue('domaines')">Domaines</button>
<button type="button" id="btn-vue-flux" onclick="setVue('flux')">Flux</button>
<button type="button" id="btn-vue-couches" onclick="setVue('couches')">Couches</button>
<button type="button" id="btn-vue-reseau" onclick="setVue('reseau')">Réseau</button>
</div>
<span class="recherche"><input id="filtre" type="search" placeholder="Filtrer…" oninput="filtrer(this.value)"></span>
<button type="button" class="fantome" onclick="ouvrirIntrants()"> Intrants</button>
@ -1152,6 +1156,7 @@ HTML = r"""<!doctype html>
document.getElementById('btn-vue-domaines').classList.toggle('on', v === 'domaines');
document.getElementById('btn-vue-flux').classList.toggle('on', v === 'flux');
document.getElementById('btn-vue-couches').classList.toggle('on', v === 'couches');
document.getElementById('btn-vue-reseau').classList.toggle('on', v === 'reseau');
dessinerGrilles();
dessinerDetail(); // le panneau droit reflète la sélection de la vue courante
majIndicateurSauvegarde();
@ -1176,6 +1181,7 @@ HTML = r"""<!doctype html>
if (vuePrincipale === 'domaines') { dessinerDomaines(); return; }
if (vuePrincipale === 'flux') { dessinerFlux(); return; }
if (vuePrincipale === 'couches') { dessinerCouches(); return; }
if (vuePrincipale === 'reseau') { dessinerReseau(); return; }
dessinerServeurs();
}
@ -1191,6 +1197,10 @@ HTML = r"""<!doctype html>
<div><span class="chip chi-tls-cible">tls-cible</span> TLS visé (roadmap)</div>
<div><span class="chip chi-clair">clair</span> non chiffré (local / edge)</div></div>
<div class="hint-dep" style="margin-top:10px">Lecture seule. Éditer les <code>meta/flux.yml</code> des rôles, puis <code>make flux</code>.</div></div>`;
} else if (vuePrincipale === 'reseau') {
d.innerHTML = `<div class="detail"><div class="section-tete">Devis réseau</div>
<div class="hint-dep">VLANs + SVIs (passerelles) + ACLs d'isolation inter-tenant, dérivés des nomenclatures <code>ip-miroir</code> (VLAN = 1000 + index×10 + zone, unique sur le trunk).</div>
<div class="hint-dep" style="margin-top:10px">Lecture seule, régénéré du plan <code>make devis-reseau</code>. À adapter à ta plateforme (NX-OS/IOS-XE, HSRP, route par défaut vers OPNsense, port de trunk).</div></div>`;
} else {
d.innerHTML = `<div class="detail"><div class="section-tete">Reconstruction</div>
<div class="hint-dep">L'ordre part du socle et remonte : chaque couche suppose la précédente debout. Le tri intra-couche vient du graphe des dépendances.</div>
@ -1215,6 +1225,31 @@ HTML = r"""<!doctype html>
<tbody>${lignes}</tbody></table></div>`;
}
let devisReseauTexte = '';
async function dessinerReseau() {
const cible = document.getElementById('grilles');
cible.innerHTML = '<div class="vide" style="padding:14px">Génération du devis…</div>';
try {
const rep = await fetch('/api/devis-reseau', {headers: {'X-Jeton': JETON}});
const data = await rep.json();
if (!rep.ok) { cible.innerHTML = '<div class="vide" style="padding:14px">' + echapper(data.erreur || 'Indisponible.') + '</div>'; return; }
devisReseauTexte = data.config || '';
if (!devisReseauTexte) { cible.innerHTML = '<div class="vide" style="padding:14px">Aucune instance fédérée (schéma ip-miroir) trouvée.</div>'; return; }
const tenants = (data.tenants || []).map(t => echapper(t.nom) + ' (idx ' + t.index + ')').join(' · ');
cible.innerHTML = `<div class="section-grille">
<div class="section-tete">Devis config réseau switches
<span class="hint-dep" style="margin-left:auto">${tenants}</span>
<button type="button" class="fantome" style="margin-left:10px" onclick="copierDevis()">Copier</button></div>
<pre class="devis-reseau">${echapper(devisReseauTexte)}</pre></div>`;
} catch (e) {
cible.innerHTML = '<div class="vide" style="padding:14px">Erreur : ' + echapper(e.message) + '</div>';
}
}
function copierDevis() {
if (navigator.clipboard) navigator.clipboard.writeText(devisReseauTexte)
.then(() => message('Devis réseau copié — prêt à coller sur le switch.', 'ok'), () => {});
}
function dessinerCouches() {
const cible = document.getElementById('grilles');
if (!couchesRegistre.length) { cible.innerHTML = '<div class="vide" style="padding:14px">Ordre de déploiement indisponible.</div>'; return; }
@ -1795,7 +1830,7 @@ HTML = r"""<!doctype html>
function dessinerDetail() {
// Le panneau de droite reflète la sélection de la VUE COURANTE.
if (vuePrincipale === 'flux' || vuePrincipale === 'couches') return legendeLecture();
if (vuePrincipale === 'flux' || vuePrincipale === 'couches' || vuePrincipale === 'reseau') return legendeLecture();
if (vuePrincipale === 'applications') return inspecteurApplication();
if (vuePrincipale === 'bases') return inspecteurBase();
if (vuePrincipale === 'domaines') return inspecteurDomaine();
@ -2332,6 +2367,12 @@ class Gestionnaire(BaseHTTPRequestHandler):
self.repondre_json(200, inventaire_api(self.inventaire))
elif chemin == "/api/sondes":
self.repondre_json(200, sonder_hotes(self.inventaire))
elif chemin == "/api/devis-reseau":
tenants = devis_reseau.decouvrir()
self.repondre_json(200, {
"config": devis_reseau.generer(tenants) if tenants else "",
"tenants": [{"nom": nom, "index": n["index"]} for nom, _, n in tenants],
})
else:
self.repondre_json(404, {"erreur": "Introuvable"})
except Exception as exc: