From b5e2edd3de2394711e49540839faf0261db1b037 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Tue, 7 Jul 2026 11:34:55 -0400 Subject: [PATCH] GUI : vues Flux + Couches (lecture seule) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 8 +++ scripts/inventory_gui.py | 116 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2da79f1..f6f3e2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/scripts/inventory_gui.py b/scripts/inventory_gui.py index 8416576..dadbc6f 100644 --- a/scripts/inventory_gui.py +++ b/scripts/inventory_gui.py @@ -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""" 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; } @@ -801,6 +857,8 @@ HTML = r""" + + @@ -863,6 +921,8 @@ HTML = r""" 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""" 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""" 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""" 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 = `
Chiffrement
+
+
tls-requis chiffré + pair vérifié
+
tls chiffré
+
starttls mise à niveau opportuniste
+
ssh transport SSH
+
tls-cible TLS visé (roadmap)
+
clair non chiffré (local / edge)
+
Lecture seule. Éditer les meta/flux.yml des rôles, puis make flux.
`; + } else { + d.innerHTML = `
Reconstruction
+
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.
+
Lecture seule. Éditer docs/couches-deploiement.yml ; généré dans playbooks/site.yml par make site.
`; + } + } + + function dessinerFlux() { + const cible = document.getElementById('grilles'); + if (!fluxRegistre.length) { cible.innerHTML = '
Aucun flux (meta/flux.yml).
'; return; } + const lignes = fluxRegistre.map(f => ` + ${echapper(f.role)} + ${echapper(f.sens)} + ${echapper(f.port)}/${echapper(f.protocole)} + ${echapper(f.pair)} + ${echapper(f.chiffrement)} + ${echapper(f.raison)}`).join(''); + cible.innerHTML = `
+
Flux réseau — matrice d'audit ${fluxRegistre.length} + ingress = le rôle écoute · egress = il se connecte · source de nftables + audit
+ + ${lignes}
RôleSensPortPairChiffrementRaison
`; + } + + function dessinerCouches() { + const cible = document.getElementById('grilles'); + if (!couchesRegistre.length) { cible.innerHTML = '
Ordre de déploiement indisponible.
'; return; } + const blocs = couchesRegistre.map((c, i) => `
+
${i}${echapper(c.nom)}${c.groupes.length}
+
${c.groupes.map(g => `${echapper(g)}`).join('')}
` + ).join('
'); + cible.innerHTML = `
+
Ordre de déploiement — couches ${couchesRegistre.length} + make deployer-tout / myDay déroule dans cet ordre · tri topo intra-couche
+
${blocs}
`; + } + function basesDeApp(app, hoteNom) { return bdApplis.filter(a => { const p = a.portee || 'groupe'; @@ -1501,6 +1616,7 @@ HTML = r""" 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();