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)
|
## 2026-07-07 (soir)
|
||||||
|
|
||||||
### Ajouté
|
### 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
|
- **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
|
(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
|
horodaté `<instance>/logs/<hôte>-<action>-<date>.log` (gitignoré). Bonus : si le navigateur se
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ from inventory_rules import (
|
||||||
reconcilier_serveur,
|
reconcilier_serveur,
|
||||||
valider_applications,
|
valider_applications,
|
||||||
valider_bases,
|
valider_bases,
|
||||||
|
valider_domaines,
|
||||||
valider_serveurs,
|
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)
|
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:
|
def donnees_hote(hote: dict) -> dict:
|
||||||
variables: dict = {}
|
variables: dict = {}
|
||||||
for cle, var, typ in CHAMPS_PROVISION:
|
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-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-applications" onclick="setVue('applications')">Applications</button>
|
||||||
<button type="button" id="btn-vue-bases" onclick="setVue('bases')">Bases</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-flux" onclick="setVue('flux')">Flux</button>
|
||||||
<button type="button" id="btn-vue-couches" onclick="setVue('couches')">Couches</button>
|
<button type="button" id="btn-vue-couches" onclick="setVue('couches')">Couches</button>
|
||||||
</div>
|
</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 hoteOpCourant = ''; // nom de l'hôte dont les boutons Vérifier/Déployer sont affichés
|
||||||
let applications = [];
|
let applications = [];
|
||||||
let applicationsModifie = false;
|
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 serveurs = [];
|
||||||
let serveursModifie = false;
|
let serveursModifie = false;
|
||||||
let ongletActif = 'reseau';
|
let ongletActif = 'reseau';
|
||||||
|
|
@ -1005,6 +1024,7 @@ HTML = r"""<!doctype html>
|
||||||
integrationsDispo = data.integrations_disponibles || [];
|
integrationsDispo = data.integrations_disponibles || [];
|
||||||
chargerBases(data);
|
chargerBases(data);
|
||||||
chargerApplications(data);
|
chargerApplications(data);
|
||||||
|
chargerDomaines(data);
|
||||||
chargerServeurs(data);
|
chargerServeurs(data);
|
||||||
hotes = data.hotes;
|
hotes = data.hotes;
|
||||||
estProduction = !!data.production;
|
estProduction = !!data.production;
|
||||||
|
|
@ -1046,10 +1066,11 @@ HTML = r"""<!doctype html>
|
||||||
function majIndicateurSauvegarde() {
|
function majIndicateurSauvegarde() {
|
||||||
const b = document.getElementById('btn-sauver');
|
const b = document.getElementById('btn-sauver');
|
||||||
if (!b) return;
|
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)
|
const sale = (vuePrincipale === 'serveurs' && serveursModifie)
|
||||||
|| (vuePrincipale === 'applications' && applicationsModifie)
|
|| (vuePrincipale === 'applications' && applicationsModifie)
|
||||||
|| (vuePrincipale === 'bases' && basesModifie);
|
|| (vuePrincipale === 'bases' && basesModifie)
|
||||||
|
|| (vuePrincipale === 'domaines' && domainesModifie);
|
||||||
b.classList.toggle('attention', sale);
|
b.classList.toggle('attention', sale);
|
||||||
b.textContent = sale ? 'Sauvegarder •' : 'Sauvegarder';
|
b.textContent = sale ? 'Sauvegarder •' : 'Sauvegarder';
|
||||||
b.disabled = !editable;
|
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-serveurs').classList.toggle('on', v === 'serveurs');
|
||||||
document.getElementById('btn-vue-applications').classList.toggle('on', v === 'applications');
|
document.getElementById('btn-vue-applications').classList.toggle('on', v === 'applications');
|
||||||
document.getElementById('btn-vue-bases').classList.toggle('on', v === 'bases');
|
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-flux').classList.toggle('on', v === 'flux');
|
||||||
document.getElementById('btn-vue-couches').classList.toggle('on', v === 'couches');
|
document.getElementById('btn-vue-couches').classList.toggle('on', v === 'couches');
|
||||||
dessinerGrilles();
|
dessinerGrilles();
|
||||||
|
|
@ -1106,6 +1128,7 @@ HTML = r"""<!doctype html>
|
||||||
function dessinerGrilles() {
|
function dessinerGrilles() {
|
||||||
if (vuePrincipale === 'applications') { dessinerApplications(); return; }
|
if (vuePrincipale === 'applications') { dessinerApplications(); return; }
|
||||||
if (vuePrincipale === 'bases') { dessinerBases(); return; }
|
if (vuePrincipale === 'bases') { dessinerBases(); return; }
|
||||||
|
if (vuePrincipale === 'domaines') { dessinerDomaines(); return; }
|
||||||
if (vuePrincipale === 'flux') { dessinerFlux(); return; }
|
if (vuePrincipale === 'flux') { dessinerFlux(); return; }
|
||||||
if (vuePrincipale === 'couches') { dessinerCouches(); return; }
|
if (vuePrincipale === 'couches') { dessinerCouches(); return; }
|
||||||
dessinerServeurs();
|
dessinerServeurs();
|
||||||
|
|
@ -1179,6 +1202,102 @@ HTML = r"""<!doctype html>
|
||||||
basesModifie = false;
|
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) {
|
function chargerApplications(data) {
|
||||||
const a = (data.applications || {}).applications || {};
|
const a = (data.applications || {}).applications || {};
|
||||||
applications = Object.entries(a).map(([id, x]) => ({
|
applications = Object.entries(a).map(([id, x]) => ({
|
||||||
|
|
@ -1634,6 +1753,7 @@ HTML = r"""<!doctype html>
|
||||||
if (vuePrincipale === 'flux' || vuePrincipale === 'couches') return legendeLecture();
|
if (vuePrincipale === 'flux' || vuePrincipale === 'couches') return legendeLecture();
|
||||||
if (vuePrincipale === 'applications') return inspecteurApplication();
|
if (vuePrincipale === 'applications') return inspecteurApplication();
|
||||||
if (vuePrincipale === 'bases') return inspecteurBase();
|
if (vuePrincipale === 'bases') return inspecteurBase();
|
||||||
|
if (vuePrincipale === 'domaines') return inspecteurDomaine();
|
||||||
return inspecteurServeur();
|
return inspecteurServeur();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1879,7 +1999,8 @@ HTML = r"""<!doctype html>
|
||||||
if (vuePrincipale === 'serveurs') sauvegarderServeurs();
|
if (vuePrincipale === 'serveurs') sauvegarderServeurs();
|
||||||
else if (vuePrincipale === 'applications') sauvegarderApplications();
|
else if (vuePrincipale === 'applications') sauvegarderApplications();
|
||||||
else if (vuePrincipale === 'bases') sauvegarderBases();
|
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() {
|
function ordreServeursVisibles() {
|
||||||
|
|
@ -1919,7 +2040,7 @@ HTML = r"""<!doctype html>
|
||||||
|
|
||||||
// Garde-fou : prévenir la perte d'éditions de plan non sauvegardées.
|
// Garde-fou : prévenir la perte d'éditions de plan non sauvegardées.
|
||||||
window.addEventListener('beforeunload', e => {
|
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 ------
|
// ------ 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))
|
valider_serveurs(donnees, charger_nomenclature(FICHIER_NOMENCLATURE))
|
||||||
ecrire_serveurs(FICHIER_SERVEURS, donnees)
|
ecrire_serveurs(FICHIER_SERVEURS, donnees)
|
||||||
self.repondre_json(200, inventaire_api(self.inventaire))
|
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":
|
elif chemin == "/api/intrants":
|
||||||
ecrire_intrants(donnees)
|
ecrire_intrants(donnees)
|
||||||
self.repondre_json(200, inventaire_api(self.inventaire))
|
self.repondre_json(200, inventaire_api(self.inventaire))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue