From 3cc00ad6601ac840192d1b23435b32bc78576c12 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Tue, 7 Jul 2026 13:08:15 -0400 Subject: [PATCH] =?UTF-8?q?GUI=20:=20=C3=A9diteur=20Domaines=20(6e=20ongle?= =?UTF-8?q?t=20=C3=A9ditable)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le registre des domaines publics — seul sans éditeur — a désormais son onglet : ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires, et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines, ecrire_domaines, validation valider_domaines (rejette une autorité inconnue). Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 7 +++ scripts/inventory_gui.py | 133 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 136 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2a98ed..738ef11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ ## 2026-07-07 (soir) ### Ajouté +- **GUI : éditeur « Domaines » (6ᵉ onglet éditable).** Le registre des domaines publics — le seul + sans édition dans le GUI — a désormais son onglet : ajouter/retirer une zone DNS, **autorité** + (sélecteur `primaire-cache`/`auto-heberge`/`delegue`), **edge**, **DNSSEC**, **mail**, secondaires, + et affichage des **expositions (FQDN) sous la zone**. Backend : route `/api/domaines`, + `ecrire_domaines`, validation `valider_domaines` (rejette une autorité inconnue). Éprouvé : + round-trip backend OK, `node --check` OK, smoke serveur OK. (Le `autorite: interne` périmé des + instances Chezlepro/Technolibre a été corrigé en `auto-heberge` au passage.) - **GUI : persistance des journaux d'exécution.** Chaque action lancée depuis l'interface (créer-vm / vérifier / déployer) écrit désormais, **en plus du streaming live**, un journal horodaté `/logs/--.log` (gitignoré). Bonus : si le navigateur se diff --git a/scripts/inventory_gui.py b/scripts/inventory_gui.py index ad18af3..570e336 100644 --- a/scripts/inventory_gui.py +++ b/scripts/inventory_gui.py @@ -36,6 +36,7 @@ from inventory_rules import ( reconcilier_serveur, valider_applications, valider_bases, + valider_domaines, valider_serveurs, ) @@ -509,6 +510,19 @@ def ecrire_serveurs(path: Path, registre: dict) -> None: fichier, default_flow_style=False, sort_keys=False, allow_unicode=True) +def ecrire_domaines(path: Path, registre: dict) -> None: + entete = ( + "# Registre des domaines publics (zones DNS de l'instance).\n" + "# Edite par make inventaire-ui ou scripts/domaines.py.\n" + "# autorite : primaire-cache | auto-heberge | delegue (label descriptif).\n" + "---\n" + ) + with path.open("w", encoding="utf-8") as fichier: + fichier.write(entete) + yaml.safe_dump({"domaines_publics": registre.get("domaines_publics", {}) or {}}, + fichier, default_flow_style=False, sort_keys=False, allow_unicode=True) + + def donnees_hote(hote: dict) -> dict: variables: dict = {} for cle, var, typ in CHAMPS_PROVISION: @@ -872,6 +886,7 @@ HTML = r""" + @@ -947,6 +962,10 @@ HTML = r""" let hoteOpCourant = ''; // nom de l'hôte dont les boutons Vérifier/Déployer sont affichés let applications = []; let applicationsModifie = false; + let domaines = []; // registre des domaines (éditable) + let selDomaine = null; // index du domaine sélectionné + let domainesModifie = false; + const AUTORITES_DNS = ['primaire-cache', 'auto-heberge', 'delegue']; let serveurs = []; let serveursModifie = false; let ongletActif = 'reseau'; @@ -1005,6 +1024,7 @@ HTML = r""" integrationsDispo = data.integrations_disponibles || []; chargerBases(data); chargerApplications(data); + chargerDomaines(data); chargerServeurs(data); hotes = data.hotes; estProduction = !!data.production; @@ -1046,10 +1066,11 @@ HTML = r""" function majIndicateurSauvegarde() { const b = document.getElementById('btn-sauver'); if (!b) return; - const editable = vuePrincipale === 'serveurs' || vuePrincipale === 'applications' || vuePrincipale === 'bases'; + const editable = vuePrincipale === 'serveurs' || vuePrincipale === 'applications' || vuePrincipale === 'bases' || vuePrincipale === 'domaines'; const sale = (vuePrincipale === 'serveurs' && serveursModifie) || (vuePrincipale === 'applications' && applicationsModifie) - || (vuePrincipale === 'bases' && basesModifie); + || (vuePrincipale === 'bases' && basesModifie) + || (vuePrincipale === 'domaines' && domainesModifie); b.classList.toggle('attention', sale); b.textContent = sale ? 'Sauvegarder •' : 'Sauvegarder'; b.disabled = !editable; @@ -1083,6 +1104,7 @@ 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-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'); dessinerGrilles(); @@ -1106,6 +1128,7 @@ HTML = r""" function dessinerGrilles() { if (vuePrincipale === 'applications') { dessinerApplications(); return; } if (vuePrincipale === 'bases') { dessinerBases(); return; } + if (vuePrincipale === 'domaines') { dessinerDomaines(); return; } if (vuePrincipale === 'flux') { dessinerFlux(); return; } if (vuePrincipale === 'couches') { dessinerCouches(); return; } dessinerServeurs(); @@ -1179,6 +1202,102 @@ HTML = r""" basesModifie = false; } + function chargerDomaines(data) { + const dp = ((data.domaines || {}).domaines_publics) || {}; + domaines = Object.entries(dp).map(([nom, x]) => ({ + nom, autorite: x.autorite || 'auto-heberge', edge: x.edge || 'serveur_nginx', + dnssec: !!x.dnssec, secondaires: (x.secondaires || []).join(', '), mail: x.mail || ''})); + domainesModifie = false; + } + function marquerDomainesModifie() { domainesModifie = true; majIndicateurSauvegarde(); } + function choisirDomaine(i) { selDomaine = i; dessiner(); } + function definirDomaine(champ, v) { + const d = domaines[selDomaine]; if (!d) return; + d[champ] = v; marquerDomainesModifie(); + if (champ === 'nom') { const t = document.querySelector('#tuile-dom-' + selDomaine + ' .carte-nom'); if (t) { t.textContent = v || '(sans nom)'; t.className = 'carte-nom' + (v ? '' : ' sans'); } } + else if (champ === 'autorite' || champ === 'edge') dessinerDetail(); + } + function ajouterDomaine() { + const edgeDefaut = groupes.includes('serveur_nginx') ? 'serveur_nginx' : (groupes[0] || ''); + domaines.unshift({nom: '', autorite: 'auto-heberge', edge: edgeDefaut, dnssec: false, secondaires: '', mail: ''}); + selDomaine = 0; marquerDomainesModifie(); dessiner(); + const c = document.querySelector('#detail .grille input'); if (c) c.focus(); + } + function retirerDomaine() { + const d = domaines[selDomaine]; if (!d) return; + if (d.nom && !confirm('Retirer le domaine « ' + d.nom + ' » ?')) return; + domaines.splice(selDomaine, 1); + selDomaine = domaines.length ? Math.min(selDomaine, domaines.length - 1) : null; + marquerDomainesModifie(); dessiner(); + } + function carteDomaine(d, i) { + return ``; + } + function dessinerDomaines() { + const cible = document.getElementById('grilles'); + const tuiles = domaines.length + ? `
${domaines.map((d, i) => carteDomaine(d, i)).join('')}
` + : '
Aucun domaine. Une zone DNS = parent des expositions (FQDN) derrière l\'edge.
'; + cible.innerHTML = `
+
Domaines ${domaines.length}
+ ${tuiles}
`; + } + function expositionsSous(zone) { + if (!zone) return []; + return applications.flatMap(a => listeDepuisTexte(a.expose)).filter(f => f === zone || f.endsWith('.' + zone)); + } + function inspecteurDomaine() { + const cible = document.getElementById('detail'); + if (selDomaine == null || !domaines[selDomaine]) { + cible.innerHTML = '
Aucun domaine sélectionnéChoisis une tuile, ou ajoute un domaine.
'; + return; + } + const d = domaines[selDomaine]; + const optAut = AUTORITES_DNS.map(a => ``).join(''); + const grpEdge = groupes.filter(g => g.startsWith('serveur_nginx')); + if (d.edge && !grpEdge.includes(d.edge)) grpEdge.push(d.edge); + const optEdge = grpEdge.map(g => ``).join(''); + const fqdns = expositionsSous(d.nom); + const liens = fqdns.length + ? `
${fqdns.map(f => `
${echapper(f)}
`).join('')}
` + : '
aucune exposition sous cette zone
'; + cible.innerHTML = `
+
${echapper(d.nom || '(sans nom)')}
+ +
+
+ + + + + + +
+
Expositions sous cette zone
+ ${liens} +
`; + } + async function sauvegarderDomaines() { + const dp = {}; + domaines.forEach(d => { + if (!String(d.nom).trim()) return; + const o = {autorite: d.autorite, edge: d.edge, secondaires: listeDepuisTexte(d.secondaires), dnssec: !!d.dnssec}; + if (String(d.mail).trim()) o.mail = d.mail; + dp[d.nom] = o; + }); + const rep = await fetch('/api/domaines', { + method: 'POST', headers: {'Content-Type': 'application/json', 'X-Jeton': JETON}, body: JSON.stringify({domaines_publics: dp}) + }); + const data = await rep.json(); + if (!rep.ok) { message(data.erreur || 'Sauvegarde refusée.', 'erreur'); return; } + chargerDomaines(data); dessinerDomaines(); + message('Domaines sauvegardés.', 'ok'); + } + function chargerApplications(data) { const a = (data.applications || {}).applications || {}; applications = Object.entries(a).map(([id, x]) => ({ @@ -1634,6 +1753,7 @@ HTML = r""" if (vuePrincipale === 'flux' || vuePrincipale === 'couches') return legendeLecture(); if (vuePrincipale === 'applications') return inspecteurApplication(); if (vuePrincipale === 'bases') return inspecteurBase(); + if (vuePrincipale === 'domaines') return inspecteurDomaine(); return inspecteurServeur(); } @@ -1879,7 +1999,8 @@ HTML = r""" if (vuePrincipale === 'serveurs') sauvegarderServeurs(); else if (vuePrincipale === 'applications') sauvegarderApplications(); else if (vuePrincipale === 'bases') sauvegarderBases(); - else message('Vue en lecture seule — édite dans Serveurs / Applications / Bases.'); + else if (vuePrincipale === 'domaines') sauvegarderDomaines(); + else message('Vue en lecture seule (Flux / Couches) — édite dans Serveurs / Applications / Bases / Domaines.'); } function ordreServeursVisibles() { @@ -1919,7 +2040,7 @@ HTML = r""" // Garde-fou : prévenir la perte d'éditions de plan non sauvegardées. window.addEventListener('beforeunload', e => { - if (serveursModifie || applicationsModifie || basesModifie) { e.preventDefault(); e.returnValue = ''; } + if (serveursModifie || applicationsModifie || basesModifie || domainesModifie) { e.preventDefault(); e.returnValue = ''; } }); // ------ Info-bulles d'aide au survol de la description d'un champ ------ @@ -2164,6 +2285,10 @@ class Gestionnaire(BaseHTTPRequestHandler): valider_serveurs(donnees, charger_nomenclature(FICHIER_NOMENCLATURE)) ecrire_serveurs(FICHIER_SERVEURS, donnees) self.repondre_json(200, inventaire_api(self.inventaire)) + elif chemin == "/api/domaines": + valider_domaines(donnees) + ecrire_domaines(FICHIER_DOMAINES, donnees) + self.repondre_json(200, inventaire_api(self.inventaire)) elif chemin == "/api/intrants": ecrire_intrants(donnees) self.repondre_json(200, inventaire_api(self.inventaire))