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:
Daniel Allaire 2026-07-07 11:34:55 -04:00
parent 0363c3255c
commit b5e2edd3de
2 changed files with 124 additions and 0 deletions

View file

@ -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

View file

@ -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();