GUI : vues Flux + Couches (lecture seule)
Deux onglets qui rendent visibles dans l'interface deux registres jusque-là en CLI seulement : - Flux : matrice d'audit réseau (rôle/sens/port/pair/chiffrement coloré/raison, depuis les meta/flux.yml), + légende chiffrement dans le panneau latéral. - Couches : ordre de reconstruction (socle -> pki -> services -> apps -> agents), groupes triés topologiquement par couche. Alimentées par resoudre_flux/orchestrer via l'API inventaire. Éprouvé : node --check OK, smoke test serveur (63 flux, 6 couches servis, boutons présents). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0363c3255c
commit
b5e2edd3de
2 changed files with 124 additions and 0 deletions
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
## 2026-07-07 (soir)
|
||||
|
||||
### Ajouté
|
||||
- **GUI : vues « Flux » et « Couches » (lecture seule).** Deux nouveaux onglets rendent visibles
|
||||
dans l'interface deux registres jusque-là en ligne de commande seulement : **Flux** = la matrice
|
||||
d'audit réseau (rôle · sens · port · pair · chiffrement coloré · raison, depuis les `meta/flux.yml`),
|
||||
et **Couches** = l'ordre de reconstruction (socle → pki → services → apps → agents, avec les groupes
|
||||
triés topologiquement par couche). Alimentées par `resoudre_flux`/`orchestrer` via l'API. Éprouvé :
|
||||
`node --check` OK, smoke test serveur (63 flux, 6 couches servis).
|
||||
|
||||
### Modifié
|
||||
- **GUI : intégration des intrants de session.** Le panneau « Intrants de base » expose désormais
|
||||
**`nftables_admin_ssh`** (type liste, section « Sécurité ») — la garde anti-lockout du pare-feu
|
||||
|
|
|
|||
|
|
@ -283,10 +283,43 @@ def serveurs_reconcilies(data: dict) -> list:
|
|||
return resultat
|
||||
|
||||
|
||||
def flux_api() -> list:
|
||||
"""Matrice des flux réseau (meta/flux.yml de tous les rôles) — lecture seule."""
|
||||
try:
|
||||
import resoudre_flux
|
||||
flux = resoudre_flux.charger_flux()
|
||||
except Exception:
|
||||
return []
|
||||
lignes = []
|
||||
for role in sorted(flux):
|
||||
for fl in flux[role]:
|
||||
pair = fl.get("pair")
|
||||
pair = ", ".join(str(p) for p in pair) if isinstance(pair, list) else str(pair)
|
||||
port = fl.get("port")
|
||||
port = ", ".join(str(p) for p in port) if isinstance(port, list) else str(port)
|
||||
lignes.append({
|
||||
"role": role, "sens": fl.get("sens", ""), "port": port,
|
||||
"protocole": fl.get("protocole", ""), "pair": pair,
|
||||
"chiffrement": fl.get("chiffrement", ""), "raison": fl.get("raison", ""),
|
||||
})
|
||||
return lignes
|
||||
|
||||
|
||||
def couches_api() -> list:
|
||||
"""Ordre de déploiement en couches (couches-deploiement.yml + graphe) — lecture seule."""
|
||||
try:
|
||||
import orchestrer
|
||||
return [{"nom": nom, "groupes": list(groupes)} for nom, groupes in orchestrer.ordre_deploiement()]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def inventaire_api(path: Path) -> dict:
|
||||
data = charger_yaml(path)
|
||||
dependencies = charger_dependances(FICHIER_DEPENDANCES)
|
||||
return {
|
||||
"flux": flux_api(),
|
||||
"couches": couches_api(),
|
||||
"inventaire": os.path.relpath(path, RACINE),
|
||||
"production": instance_est_production(path),
|
||||
"groupes": groupes_disponibles(data),
|
||||
|
|
@ -787,6 +820,29 @@ HTML = r"""<!doctype html>
|
|||
main { grid-template-columns: 1fr; }
|
||||
aside { position: static; max-height: none; order: -1; }
|
||||
}
|
||||
/* Vues lecture seule : Flux + Couches */
|
||||
.tbl-lecture { width: 100%; border-collapse: collapse; font-size: 12.5px; }
|
||||
.tbl-lecture th { text-align: left; padding: 7px 10px; color: var(--muted); font-weight: 750; border-bottom: 1px solid var(--ligne-forte); position: sticky; top: 0; background: var(--surface); }
|
||||
.tbl-lecture td { padding: 6px 10px; border-bottom: 1px solid var(--ligne); vertical-align: top; }
|
||||
.tbl-lecture tr:hover td { background: var(--surface-2); }
|
||||
.flux-raison { color: var(--muted); max-width: 44ch; }
|
||||
.sens { font-size: 11px; font-weight: 750; padding: 1px 7px; border-radius: 999px; }
|
||||
.sens-ingress { background: color-mix(in srgb, var(--bleu) 16%, transparent); color: var(--bleu); }
|
||||
.sens-egress { background: color-mix(in srgb, var(--ambre) 18%, transparent); color: var(--ambre); }
|
||||
.chip.chi-tls-requis { background: color-mix(in srgb, var(--teal) 22%, transparent); color: var(--teal); }
|
||||
.chip.chi-tls { background: color-mix(in srgb, var(--teal) 13%, transparent); color: var(--teal); }
|
||||
.chip.chi-starttls { background: color-mix(in srgb, var(--ambre) 18%, transparent); color: var(--ambre); }
|
||||
.chip.chi-ssh { background: color-mix(in srgb, var(--bleu) 18%, transparent); color: var(--bleu); }
|
||||
.chip.chi-tls-cible { background: color-mix(in srgb, var(--ambre) 12%, transparent); color: var(--muted); }
|
||||
.chip.chi-clair { background: var(--surface-3); color: var(--muted); }
|
||||
.couches-pile { display: flex; flex-direction: column; gap: 0; padding: 6px; }
|
||||
.couche { border: 1px solid var(--ligne-forte); border-radius: 10px; padding: 9px 12px; background: var(--surface-2); }
|
||||
.couche-tete { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
||||
.couche-idx { display: inline-flex; width: 20px; height: 20px; align-items: center; justify-content: center; border-radius: 999px; background: var(--teal); color: var(--surface); font-size: 11px; font-weight: 800; }
|
||||
.couche-groupes { display: flex; flex-wrap: wrap; gap: 5px; }
|
||||
.couche-fleche { text-align: center; color: var(--muted); font-size: 14px; line-height: 1.5; }
|
||||
.legende { display: flex; flex-direction: column; gap: 6px; font-size: 12px; color: var(--muted); }
|
||||
.legende .chip { margin-right: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -801,6 +857,8 @@ HTML = r"""<!doctype html>
|
|||
<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>
|
||||
<button type="button" id="btn-vue-flux" onclick="setVue('flux')">Flux</button>
|
||||
<button type="button" id="btn-vue-couches" onclick="setVue('couches')">Couches</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>
|
||||
|
|
@ -863,6 +921,8 @@ HTML = r"""<!doctype html>
|
|||
let estProduction = false;
|
||||
let filtre = '';
|
||||
let vuePrincipale = 'serveurs';
|
||||
let fluxRegistre = []; // matrice des flux réseau (lecture seule)
|
||||
let couchesRegistre = []; // ordre de déploiement en couches (lecture seule)
|
||||
let bdServeurs = [];
|
||||
let bdApplis = [];
|
||||
let basesModifie = false;
|
||||
|
|
@ -924,6 +984,8 @@ HTML = r"""<!doctype html>
|
|||
dependances = data.dependances || {};
|
||||
nomenclature = data.nomenclature || {};
|
||||
chaine = data.chaine || {};
|
||||
fluxRegistre = data.flux || [];
|
||||
couchesRegistre = data.couches || [];
|
||||
intrants = data.intrants || null;
|
||||
integrationsDispo = data.integrations_disponibles || [];
|
||||
chargerBases(data);
|
||||
|
|
@ -1006,6 +1068,8 @@ HTML = r"""<!doctype html>
|
|||
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');
|
||||
document.getElementById('btn-vue-flux').classList.toggle('on', v === 'flux');
|
||||
document.getElementById('btn-vue-couches').classList.toggle('on', v === 'couches');
|
||||
dessinerGrilles();
|
||||
dessinerDetail(); // le panneau droit reflète la sélection de la vue courante
|
||||
majIndicateurSauvegarde();
|
||||
|
|
@ -1027,9 +1091,60 @@ HTML = r"""<!doctype html>
|
|||
function dessinerGrilles() {
|
||||
if (vuePrincipale === 'applications') { dessinerApplications(); return; }
|
||||
if (vuePrincipale === 'bases') { dessinerBases(); return; }
|
||||
if (vuePrincipale === 'flux') { dessinerFlux(); return; }
|
||||
if (vuePrincipale === 'couches') { dessinerCouches(); return; }
|
||||
dessinerServeurs();
|
||||
}
|
||||
|
||||
function legendeLecture() {
|
||||
const d = document.getElementById('detail');
|
||||
if (vuePrincipale === 'flux') {
|
||||
d.innerHTML = `<div class="detail"><div class="section-tete">Chiffrement</div>
|
||||
<div class="legende">
|
||||
<div><span class="chip chi-tls-requis">tls-requis</span> chiffré + pair vérifié</div>
|
||||
<div><span class="chip chi-tls">tls</span> chiffré</div>
|
||||
<div><span class="chip chi-starttls">starttls</span> mise à niveau opportuniste</div>
|
||||
<div><span class="chip chi-ssh">ssh</span> transport SSH</div>
|
||||
<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 {
|
||||
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>
|
||||
<div class="hint-dep" style="margin-top:10px">Lecture seule. Éditer <code>docs/couches-deploiement.yml</code> ; généré dans <code>playbooks/site.yml</code> par <code>make site</code>.</div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function dessinerFlux() {
|
||||
const cible = document.getElementById('grilles');
|
||||
if (!fluxRegistre.length) { cible.innerHTML = '<div class="vide" style="padding:14px">Aucun flux (meta/flux.yml).</div>'; return; }
|
||||
const lignes = fluxRegistre.map(f => `<tr>
|
||||
<td class="mono">${echapper(f.role)}</td>
|
||||
<td><span class="sens sens-${echapper(f.sens)}">${echapper(f.sens)}</span></td>
|
||||
<td class="mono">${echapper(f.port)}/${echapper(f.protocole)}</td>
|
||||
<td class="mono">${echapper(f.pair)}</td>
|
||||
<td><span class="chip chi-${echapper(f.chiffrement)}">${echapper(f.chiffrement)}</span></td>
|
||||
<td class="flux-raison">${echapper(f.raison)}</td></tr>`).join('');
|
||||
cible.innerHTML = `<div class="section-grille">
|
||||
<div class="section-tete">Flux réseau — matrice d'audit <span class="compte">${fluxRegistre.length}</span>
|
||||
<span class="hint-dep" style="margin-left:auto">ingress = le rôle écoute · egress = il se connecte · source de nftables + audit</span></div>
|
||||
<table class="tbl-lecture"><thead><tr><th>Rôle</th><th>Sens</th><th>Port</th><th>Pair</th><th>Chiffrement</th><th>Raison</th></tr></thead>
|
||||
<tbody>${lignes}</tbody></table></div>`;
|
||||
}
|
||||
|
||||
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; }
|
||||
const blocs = couchesRegistre.map((c, i) => `<div class="couche">
|
||||
<div class="couche-tete"><span class="couche-idx">${i}</span><b>${echapper(c.nom)}</b><span class="compte">${c.groupes.length}</span></div>
|
||||
<div class="couche-groupes">${c.groupes.map(g => `<span class="chip mono">${echapper(g)}</span>`).join('')}</div></div>`
|
||||
).join('<div class="couche-fleche">↓</div>');
|
||||
cible.innerHTML = `<div class="section-grille">
|
||||
<div class="section-tete">Ordre de déploiement — couches <span class="compte">${couchesRegistre.length}</span>
|
||||
<span class="hint-dep" style="margin-left:auto">make deployer-tout / myDay déroule dans cet ordre · tri topo intra-couche</span></div>
|
||||
<div class="couches-pile">${blocs}</div></div>`;
|
||||
}
|
||||
|
||||
function basesDeApp(app, hoteNom) {
|
||||
return bdApplis.filter(a => {
|
||||
const p = a.portee || 'groupe';
|
||||
|
|
@ -1501,6 +1616,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 === 'applications') return inspecteurApplication();
|
||||
if (vuePrincipale === 'bases') return inspecteurBase();
|
||||
return inspecteurServeur();
|
||||
|
|
|
|||
Loading…
Reference in a new issue