GUI : éditeur Domaines (6e onglet éditable)
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 <noreply@anthropic.com>
This commit is contained in:
parent
b4ca67289a
commit
3cc00ad660
2 changed files with 136 additions and 4 deletions
|
|
@ -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é `<instance>/logs/<hôte>-<action>-<date>.log` (gitignoré). Bonus : si le navigateur se
|
||||
|
|
|
|||
|
|
@ -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"""<!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-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>
|
||||
</div>
|
||||
|
|
@ -947,6 +962,10 @@ HTML = r"""<!doctype html>
|
|||
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"""<!doctype html>
|
|||
integrationsDispo = data.integrations_disponibles || [];
|
||||
chargerBases(data);
|
||||
chargerApplications(data);
|
||||
chargerDomaines(data);
|
||||
chargerServeurs(data);
|
||||
hotes = data.hotes;
|
||||
estProduction = !!data.production;
|
||||
|
|
@ -1046,10 +1066,11 @@ HTML = r"""<!doctype html>
|
|||
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"""<!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-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"""<!doctype html>
|
|||
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"""<!doctype html>
|
|||
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 `<button type="button" class="carte ${i === selDomaine ? 'selectionnee' : ''}" id="tuile-dom-${i}" onclick="choisirDomaine(${i})">
|
||||
<div class="carte-haut"><span class="badge-bd">zone</span>${d.dnssec ? '<span class="carte-vmid">dnssec</span>' : ''}</div>
|
||||
<div class="carte-nom ${d.nom ? '' : 'sans'}">${echapper(d.nom || '(sans nom)')}</div>
|
||||
<div class="carte-ip">${echapper(d.autorite || '—')}</div>
|
||||
</button>`;
|
||||
}
|
||||
function dessinerDomaines() {
|
||||
const cible = document.getElementById('grilles');
|
||||
const tuiles = domaines.length
|
||||
? `<div class="grille-cartes">${domaines.map((d, i) => carteDomaine(d, i)).join('')}</div>`
|
||||
: '<div class="vide" style="padding:14px">Aucun domaine. Une zone DNS = parent des expositions (FQDN) derrière l\'edge.</div>';
|
||||
cible.innerHTML = `<div class="section-grille">
|
||||
<div class="section-tete">Domaines <span class="compte">${domaines.length}</span><button type="button" class="fantome" style="margin-left:auto" onclick="ajouterDomaine()">+ Domaine</button></div>
|
||||
${tuiles}</div>`;
|
||||
}
|
||||
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 = '<div class="detail"><div class="vide"><b>Aucun domaine sélectionné</b>Choisis une tuile, ou ajoute un domaine.</div></div>';
|
||||
return;
|
||||
}
|
||||
const d = domaines[selDomaine];
|
||||
const optAut = AUTORITES_DNS.map(a => `<option value="${a}" ${a === d.autorite ? 'selected' : ''}>${a}</option>`).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 => `<option value="${echapper(g)}" ${g === d.edge ? 'selected' : ''}>${echapper(g)}</option>`).join('');
|
||||
const fqdns = expositionsSous(d.nom);
|
||||
const liens = fqdns.length
|
||||
? `<div class="ch-bases">${fqdns.map(f => `<div class="ch-base"><span class="mono">${echapper(f)}</span></div>`).join('')}</div>`
|
||||
: '<div class="ch-stub">aucune exposition sous cette zone</div>';
|
||||
cible.innerHTML = `<article class="detail">
|
||||
<div class="detail-tete"><div class="detail-nom"><span class="detail-nom-txt">${echapper(d.nom || '(sans nom)')}</span></div>
|
||||
<span class="grandit"></span>
|
||||
<button type="button" class="danger" title="Retirer ce domaine" onclick="retirerDomaine()">✕</button></div>
|
||||
<div class="grille">
|
||||
<label class="champ" style="grid-column:1/-1"><span>Zone DNS</span><input value="${echapper(d.nom)}" placeholder="exemple.ca" oninput="definirDomaine('nom', this.value)"></label>
|
||||
<label class="champ"><span>Autorité</span><select onchange="definirDomaine('autorite', this.value)">${optAut}</select></label>
|
||||
<label class="champ"><span>Edge</span><select onchange="definirDomaine('edge', this.value)">${optEdge}</select></label>
|
||||
<label class="champ"><span>DNSSEC</span><select onchange="definirDomaine('dnssec', this.value === 'oui')"><option value="non" ${d.dnssec ? '' : 'selected'}>non</option><option value="oui" ${d.dnssec ? 'selected' : ''}>oui</option></select></label>
|
||||
<label class="champ"><span>Mail (optionnel)</span><input value="${echapper(d.mail)}" placeholder="—" oninput="definirDomaine('mail', this.value)"></label>
|
||||
<label class="champ" style="grid-column:1/-1"><span>Secondaires (DNS)</span><input value="${echapper(d.secondaires)}" placeholder="ns2.exemple.ca, …" oninput="definirDomaine('secondaires', this.value)"></label>
|
||||
</div>
|
||||
<div class="section-tete" style="margin:12px 2px 4px">Expositions sous cette zone</div>
|
||||
${liens}
|
||||
</article>`;
|
||||
}
|
||||
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"""<!doctype html>
|
|||
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"""<!doctype html>
|
|||
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"""<!doctype html>
|
|||
|
||||
// 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))
|
||||
|
|
|
|||
Loading…
Reference in a new issue