Files
pve-confsync/web/index.html
T
cedricandClaude Opus 5.5 1e667ce8b9 Réplication de nœuds : réseau et DNS par l'API, multipath, chrony, sysctl par SSH
Deuxième mode de réplication, séparé de la réplication cluster vers cluster
(sélecteur « Clusters (API) | Nœuds (fichiers) » en haut de page) : un profil
réplique la configuration système d'un nœud maître vers des nœuds cibles.

- réseau par /nodes/{node}/network (interfaces.new puis rechargement), adresses
  et passerelles propres à chaque nœud, mapping des noms d'interfaces par cible ;
  DNS par /nodes/{node}/dns ; multipath.conf, wwids (ajout seul), chrony,
  sysctl.d, interfaces.d (hors sdn) par SSH
- découverte des nœuds (/cluster/status), accès SSH par nœud (mot de passe du
  cluster ou propre), clé d'hôte épinglée, contrôle du nom du nœud
- dry-run avec diff des fichiers ; apply (admin) avec contrôle SHA-256,
  sauvegarde et restore.sh sur le nœud, écriture atomique, rechargements,
  restauration si un rechargement échoue, timer systemd de restauration si le
  nœud est injoignable après le rechargement du réseau, confirmation tapée
- vue Nœuds et profils, journal séparé par mode, démo avec nœuds simulés

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 20:29:55 +02:00

547 lines
32 KiB
HTML

<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>pve-confsync</title>
<link rel="icon" href="favicon.svg">
<link rel="stylesheet" href="style.css">
<script src="boot.js"></script>
</head>
<body>
<header class="topbar">
<div class="brand">
<svg viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="8"/><path d="M8.5 14.5a8 8 0 0 1 13.6-4.6M22.6 6v4.2h-4.2M23.5 17.5a8 8 0 0 1-13.6 4.6M9.4 26v-4.2h4.2"/></svg>
<span>pve-confsync</span>
<span id="demoBadge" class="demo-badge" hidden data-i18n="demo">DÉMO</span>
</div>
<!-- the two replications: cluster to cluster (API), node to node (system files) -->
<div id="modeSw" class="seg mode-sw" role="radiogroup" data-i18n-aria="modeAriaSw">
<button type="button" role="radio" data-mode="cluster" data-i18n-title="modeClusterTitle">
<!-- Lucide "layers" (ISC license) -->
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/></svg>
<span data-i18n="modeCluster">Clusters (API)</span>
</button>
<button type="button" role="radio" data-mode="node" data-i18n-title="modeNodeTitle">
<!-- Lucide "hard-drive" (ISC license) -->
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M22 12H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/><path d="M6 16h.01M10 16h.01"/></svg>
<span data-i18n="modeNode">Nœuds (fichiers)</span>
</button>
</div>
<div class="search">
<svg class="ico" viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
<input id="q" type="search" autocomplete="off" spellcheck="false" data-i18n-ph="searchPh" data-i18n-aria="searchPh">
</div>
<div class="actions">
<button id="refreshBtn" class="btn" type="button" data-i18n-title="refreshTitle">
<!-- Lucide "refresh-cw" (ISC license) -->
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>
<span class="lbl" data-i18n="refresh">Actualiser</span>
</button>
<button id="settingsBtn" class="icon-btn" type="button" data-i18n-title="settings" data-i18n-aria="settings">
<!-- Lucide "settings" (ISC license) -->
<svg viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
<button id="themeBtn" class="icon-btn" type="button" data-i18n-title="theme" data-i18n-aria="theme">
<svg class="sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
<svg class="moon" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
</button>
<span id="whoami" class="whoami" hidden><span class="who-name"></span><span class="who-badge"></span></span>
<a id="logoutBtn" class="icon-btn" href="auth/logout" data-i18n-title="logout" data-i18n-aria="logout">
<!-- Lucide "log-out" (ISC license) -->
<svg viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/></svg>
</a>
</div>
<div class="progress" aria-hidden="true"></div>
</header>
<section class="filters">
<div id="viewMode" class="seg view-mode" role="radiogroup" data-i18n-aria="viewAria">
<button type="button" role="radio" data-view="diff">
<!-- Lucide "git-compare" (ISC license) -->
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M13 6h3a2 2 0 0 1 2 2v7"/><path d="M11 18H8a2 2 0 0 1-2-2V9"/></svg>
<span data-i18n="viewDiff">Écarts</span>
</button>
<button type="button" role="radio" data-view="clusters">
<!-- Lucide "server" (ISC license) -->
<svg viewBox="0 0 24 24" aria-hidden="true"><rect width="20" height="8" x="2" y="2" rx="2"/><rect width="20" height="8" x="2" y="14" rx="2"/><path d="M6 6h.01M6 18h.01"/></svg>
<span data-i18n="viewClusters">Clusters</span>
</button>
<button type="button" role="radio" data-view="nodes" hidden>
<!-- Lucide "server" (ISC license) -->
<svg viewBox="0 0 24 24" aria-hidden="true"><rect width="20" height="8" x="2" y="2" rx="2"/><rect width="20" height="8" x="2" y="14" rx="2"/><path d="M6 6h.01M6 18h.01"/></svg>
<span data-i18n="viewNodes">Nœuds et profils</span>
</button>
<button type="button" role="radio" data-view="runs">
<!-- Lucide "history" (ISC license) -->
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/></svg>
<span data-i18n="viewRuns">Journal</span>
</button>
<button type="button" role="radio" data-view="audit" id="auditTab" hidden>
<!-- Lucide "shield-check" (ISC license) -->
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></svg>
<span data-i18n="viewAudit">Audit</span>
</button>
</div>
<select id="profileSel" data-diff data-node-only data-i18n-aria="profileAria"></select>
<select id="kindSel" data-diff data-i18n-aria="kindAria"></select>
<select id="statusSel" data-diff data-i18n-aria="statusAria"></select>
<select id="runModeSel" data-runs data-i18n-aria="modeAria" hidden></select>
<span class="spacer"></span>
<span id="count" class="muted"></span>
<div id="globalActs" class="global-acts" data-diff hidden>
<details id="targetMenu" class="menu right">
<summary>
<!-- Lucide "target" (ISC license) -->
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
<span data-i18n="targetMenu">Cibles</span><span class="n" id="targetMenuN"></span>
</summary>
<div class="pop" id="targetMenuPop"></div>
</details>
<button id="dryAllBtn" class="btn tool" type="button">
<!-- Lucide "scan-search" (ISC license) -->
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="12" cy="12" r="3"/><path d="m16 16-1.9-1.9"/></svg>
<span data-i18n="dryAll">Dry-run global</span>
</button>
<button id="syncAllBtn" class="btn tool sync" type="button">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12a9 9 0 0 1 15.7-6L21 8M21 3v5h-5M21 12a9 9 0 0 1-15.7 6L3 16M3 21v-5h5"/></svg>
<span data-i18n="syncAll">Répliquer</span>
</button>
</div>
</section>
<div id="error" class="error-banner" hidden></div>
<div id="setup" class="setup" hidden>
<p id="setupText"></p>
<button id="setupBtn" class="btn primary" type="button"></button>
</div>
<!-- Differences: clusters, then the objects of the selected client -->
<section id="diffView">
<div id="strip" class="strip" role="tablist" data-i18n-aria="stripAria"></div>
<div id="clientBar" class="client-bar" hidden></div>
<div id="notes" class="notes"></div>
<div id="objTable" class="table objects" hidden>
<div class="list-head" role="presentation">
<span data-i18n="colKind">Type</span>
<span data-i18n="colObject">Objet</span>
<span data-i18n="colStatus">Statut</span>
<span data-i18n="colFields">Champs</span>
<span></span>
</div>
<main id="objList" class="list"></main>
</div>
<div id="objEmpty" class="empty" hidden>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>
<p><strong id="objEmptyTitle"></strong></p>
<p class="muted" id="objEmptyHint"></p>
</div>
</section>
<!-- Clusters: connection (root), primary / targets, connection tests -->
<section id="clustersView" class="cl-view" hidden>
<div class="cl-tools">
<p class="muted small" data-i18n="clustersHelp"></p>
<button id="addCluster" class="btn tool primary" type="button" data-admin-only data-i18n="addCluster">+ Ajouter un cluster</button>
<button id="addBulk" class="btn tool" type="button" data-admin-only data-i18n="addBulk">+ Ajouter une liste</button>
<button id="testAll" class="btn tool" type="button" data-admin-only data-i18n="testAll">Tout tester</button>
</div>
<div id="clusterList" class="clist"></div>
</section>
<!-- Nodes (SSH access) and node replication profiles -->
<section id="nodesView" class="cl-view" hidden>
<div class="cl-tools">
<p class="muted small" data-i18n="nodesHelp"></p>
<button id="addProfile" class="btn tool primary" type="button" data-admin-only data-i18n="addProfile">+ Nouveau profil</button>
<button id="discoverBtn" class="btn tool" type="button" data-admin-only data-i18n="discover">Découvrir les nœuds</button>
<button id="testNodes" class="btn tool" type="button" data-admin-only data-i18n="testAll">Tout tester</button>
</div>
<h3 class="sec-title" data-i18n="profilesTitle">Profils de réplication</h3>
<div id="profileList" class="clist"></div>
<h3 class="sec-title" data-i18n="nodesTitle">Nœuds</h3>
<div id="nodeList" class="clist"></div>
</section>
<!-- Execution journal -->
<section id="runsView" hidden>
<div class="table runs">
<div class="list-head" role="presentation">
<span data-i18n="colDate">Date</span>
<span data-i18n="colUser">Utilisateur</span>
<span data-i18n="colMode">Mode</span>
<span data-i18n="colScope">Portée</span>
<span data-i18n="colClients">Clients</span>
<span data-i18n="colStatus">Statut</span>
<span data-i18n="colSummary">Résumé</span>
</div>
<main id="runList" class="list"></main>
</div>
<div class="more-row"><button id="runMore" class="btn ghost" type="button" data-i18n="more" hidden>Plus</button></div>
</section>
<!-- Audit trail (administrators) -->
<section id="auditView" hidden>
<div class="table audit">
<div class="list-head" role="presentation">
<span data-i18n="colDate">Date</span>
<span data-i18n="colUser">Utilisateur</span>
<span data-i18n="colIP">Adresse</span>
<span data-i18n="colAction">Action</span>
<span data-i18n="colTarget">Cible</span>
<span data-i18n="colResult">Résultat</span>
<span data-i18n="colDetail">Détail</span>
</div>
<main id="auditList" class="list"></main>
</div>
<div class="more-row"><button id="auditMore" class="btn ghost" type="button" data-i18n="more" hidden>Plus</button></div>
</section>
<footer class="status">
<span id="conn" class="conn"></span>
<span id="stats" class="muted"></span>
<span class="sp"></span>
<span id="ver" class="muted"></span>
<button id="toTop" class="icon-btn to-top" type="button" data-i18n-title="toTop" data-i18n-aria="toTop" hidden>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg>
</button>
</footer>
<!-- Dry-run, confirmation and progress of a synchronization -->
<dialog id="planDlg" aria-labelledby="planTitle">
<div class="dlg">
<header>
<h2 id="planTitle"></h2>
<button class="icon-btn" type="button" data-close data-i18n-aria="close"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
</header>
<div id="planKinds" class="plan-kinds" hidden></div>
<div id="planBody" class="dlg-body"></div>
<footer>
<span id="planNote" class="muted small"></span>
<span class="sp"></span>
<button id="planClose" class="btn ghost" type="button" data-close data-i18n="close">Fermer</button>
<button id="planApply" class="btn primary" type="button" hidden>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12a9 9 0 0 1 15.7-6L21 8M21 3v5h-5M21 12a9 9 0 0 1-15.7 6L3 16M3 21v-5h5"/></svg>
<span data-i18n="applyBtn">Répliquer</span>
</button>
</footer>
</div>
</dialog>
<!-- Cluster editor (administrators) -->
<dialog id="clusterDlg" aria-labelledby="clusterTitle">
<form class="dlg" id="clusterForm" method="dialog">
<header>
<h2 id="clusterTitle"></h2>
<button class="icon-btn" type="button" data-close data-i18n-aria="close"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
</header>
<div class="dlg-body">
<div class="field-grid">
<label for="cName" data-i18n="cName">Nom</label>
<input id="cName" class="inp" required autocomplete="off" spellcheck="false">
<span class="lbl" data-i18n="cRole">Rôle</span>
<div id="cRole" class="seg" role="radiogroup">
<button type="button" role="radio" data-v="primary" data-i18n="rolePrimary">Primaire</button>
<button type="button" role="radio" data-v="target" data-i18n="roleTarget">Cible</button>
<button type="button" role="radio" data-v="disabled" data-i18n="roleDisabled">Inactif</button>
</div>
<label for="cURL">URL</label>
<input id="cURL" class="inp mono" required placeholder="https://pve1.example.org:8006" autocomplete="off" spellcheck="false">
<label for="cUser" data-i18n="cUser">Utilisateur</label>
<input id="cUser" class="inp mono" value="root@pam" readonly tabindex="-1">
<label for="cPassword" data-i18n="cPassword">Mot de passe root</label>
<input id="cPassword" class="inp mono" type="password" autocomplete="new-password" spellcheck="false">
<p class="muted small hint" id="cPasswordHint"></p>
<span class="lbl" data-i18n="cTLS">TLS</span>
<select id="cTLS" class="field">
<option value="fp" data-i18n="tlsFp">Empreinte du certificat (enregistrée au premier test)</option>
<option value="ca" data-i18n="tlsCa">Certificat d'autorité (PEM)</option>
<option value="sys" data-i18n="tlsSys">Autorités du système</option>
<option value="insecure" data-i18n="tlsInsecure">Sans vérification (labo uniquement)</option>
</select>
<label for="cFp" data-tls="fp" data-i18n="cFp">Empreinte</label>
<input id="cFp" class="inp mono" data-tls="fp" data-i18n-ph="fpPh" autocomplete="off" spellcheck="false">
<label for="cCA" data-tls="ca">PEM</label>
<textarea id="cCA" class="inp" data-tls="ca" spellcheck="false" placeholder="-----BEGIN CERTIFICATE-----"></textarea>
<label for="cSSHHost" data-i18n="cSSHHost">Nœud SSH</label>
<input id="cSSHHost" class="inp mono" data-i18n-ph="sshHostPh" autocomplete="off" spellcheck="false">
<label for="cSSHPort" data-i18n="cSSHPort">Port SSH</label>
<input id="cSSHPort" class="inp mono" type="number" min="1" max="65535" placeholder="22">
<span class="lbl" data-i18n="cSSHKey">Clé d'hôte SSH</span>
<span id="cSSHKey" class="mono small muted"></span>
<label for="cTimeout" data-i18n="cTimeout">Délai</label>
<input id="cTimeout" class="inp mono" placeholder="30s" autocomplete="off">
</div>
<div id="cClientOnly">
<div class="sec-head mt"><h3 class="lbl" data-i18n="cMapping">Mapping (valeurs propres à la cible)</h3></div>
<p class="muted small" data-i18n="cMappingHelp"></p>
<div class="erow-head map"><span data-i18n="colKind">Type</span><span data-i18n="colObject">Objet</span><span data-i18n="colField">Champ</span><span data-i18n="colValue">Valeur</span><span></span></div>
<div id="cMapRows" class="erows"></div>
<div class="toolbar"><button id="cMapAdd" class="btn tool" type="button" data-i18n="addRow">+ Ajouter</button></div>
<div class="sec-head mt"><h3 class="lbl" data-i18n="cPolicies">Politiques propres à la cible</h3></div>
<p class="muted small" data-i18n="cPoliciesHelp"></p>
<div class="erow-head pol"><span data-i18n="colKind">Type</span><span data-i18n="colField">Champ</span><span data-i18n="colPolicy">Politique</span><span></span></div>
<div id="cPolRows" class="erows"></div>
<div class="toolbar"><button id="cPolAdd" class="btn tool" type="button" data-i18n="addRow">+ Ajouter</button></div>
</div>
<p id="cErr" class="login-err" role="alert" hidden></p>
</div>
<footer>
<button id="cDelete" class="btn danger" type="button" data-i18n="delete">Supprimer</button>
<button id="cForget" class="btn ghost" type="button" data-i18n="forget">Oublier les empreintes</button>
<span class="sp"></span>
<button class="btn ghost" type="button" data-close data-i18n="cancel">Annuler</button>
<button class="btn primary" type="submit" data-i18n="save">Enregistrer</button>
</footer>
</form>
</dialog>
<!-- Several clusters at once (administrators) -->
<dialog id="bulkDlg" aria-labelledby="bulkTitle">
<form class="dlg" id="bulkForm" method="dialog">
<header>
<h2 id="bulkTitle" data-i18n="bulkTitle">Ajouter une liste de clusters</h2>
<button class="icon-btn" type="button" data-close data-i18n-aria="close"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
</header>
<div class="dlg-body">
<p class="muted small" data-i18n="bulkHelp"></p>
<textarea id="bulkText" class="inp" spellcheck="false" required placeholder="pve-rennes https://10.0.0.11:8006&#10;pve-nantes https://10.0.1.11:8006&#10;https://pve-brest.lab:8006"></textarea>
<div class="field-grid mt-s">
<label for="bulkPassword" data-i18n="bulkPassword">Mot de passe root</label>
<input id="bulkPassword" class="inp mono" type="password" autocomplete="new-password" required>
<span class="lbl" data-i18n="cTLS">TLS</span>
<select id="bulkTLS" class="field">
<option value="fp" data-i18n="tlsFp">Empreinte du certificat (enregistrée au premier test)</option>
<option value="sys" data-i18n="tlsSys">Autorités du système</option>
<option value="insecure" data-i18n="tlsInsecure">Sans vérification (labo uniquement)</option>
</select>
</div>
<ul id="bulkRes" class="bulk-res"></ul>
</div>
<footer>
<span class="sp"></span>
<button class="btn ghost" type="button" data-close data-i18n="close">Fermer</button>
<button id="bulkGo" class="btn primary" type="submit" data-i18n="bulkGo">Ajouter et tester</button>
</footer>
</form>
</dialog>
<!-- SSH access of a node (administrators) -->
<dialog id="nodeDlg" aria-labelledby="nodeTitle">
<form class="dlg" id="nodeForm" method="dialog">
<header>
<h2 id="nodeTitle"></h2>
<button class="icon-btn" type="button" data-close data-i18n-aria="close"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
</header>
<div class="dlg-body">
<p class="muted small" data-i18n="nodeHelp"></p>
<div class="field-grid">
<label for="nHost" data-i18n="nHost">Adresse SSH</label>
<input id="nHost" class="inp mono" autocomplete="off" spellcheck="false">
<label for="nPort" data-i18n="cSSHPort">Port SSH</label>
<input id="nPort" class="inp mono" type="number" min="1" max="65535" placeholder="22">
<label for="nPassword" data-i18n="nPassword">Mot de passe root</label>
<input id="nPassword" class="inp mono" type="password" autocomplete="new-password" spellcheck="false">
<span></span>
<label class="switch-row"><input id="nClear" type="checkbox" class="switch"><span data-i18n="nClear">Utiliser le mot de passe du cluster</span></label>
<span class="lbl" data-i18n="cSSHKey">Clé d'hôte SSH</span>
<span id="nKey" class="mono small muted"></span>
</div>
<p id="nErr" class="login-err" role="alert" hidden></p>
</div>
<footer>
<button id="nForget" class="btn ghost" type="button" data-i18n="forgetKey">Oublier la clé</button>
<span class="sp"></span>
<button class="btn ghost" type="button" data-close data-i18n="cancel">Annuler</button>
<button class="btn primary" type="submit" data-i18n="save">Enregistrer</button>
</footer>
</form>
</dialog>
<!-- Node replication profile (administrators) -->
<dialog id="profileDlg" class="wide" aria-labelledby="profileTitle">
<form class="dlg" id="profileForm" method="dialog">
<header>
<h2 id="profileTitle"></h2>
<button class="icon-btn" type="button" data-close data-i18n-aria="close"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
</header>
<div class="dlg-body">
<div class="field-grid">
<label for="pName" data-i18n="cName">Nom</label>
<input id="pName" class="inp" required autocomplete="off" spellcheck="false">
<label for="pMaster" data-i18n="pMaster">Nœud maître</label>
<select id="pMaster" class="field" required></select>
</div>
<div class="sec-head mt"><h3 class="lbl" data-i18n="pTargets">Nœuds cibles</h3></div>
<div id="pTargets" class="chip-field"></div>
<div class="sec-head mt"><h3 class="lbl" data-i18n="pItems">Éléments répliqués</h3></div>
<div id="pItems" class="chip-field"></div>
<p class="muted small mt-s" data-i18n="pItemsHelp"></p>
<label class="switch-row mt-s"><input id="pPrune" type="checkbox" class="switch"><span data-i18n="pPrune">Supprimer sur les cibles ce que le maître n'a pas</span></label>
<p class="muted small" data-i18n="pPruneHelp"></p>
<div class="sec-head mt"><h3 class="lbl" data-i18n="polTitle">Politiques par champ</h3></div>
<p class="muted small" data-i18n="pPolHelp"></p>
<div class="erow-head pol"><span data-i18n="colKind">Type</span><span data-i18n="colField">Champ</span><span data-i18n="colPolicy">Politique</span><span></span></div>
<div id="pPolRows" class="erows"></div>
<div class="toolbar"><button id="pPolAdd" class="btn tool" type="button" data-i18n="addRow">+ Ajouter</button></div>
<div class="sec-head mt"><h3 class="lbl" data-i18n="pProtect">Lignes protégées des fichiers</h3></div>
<p class="muted small" data-i18n="pProtectHelp"></p>
<div id="pProtect" class="erows"></div>
<div class="sec-head mt"><h3 class="lbl" data-i18n="pMapping">Mapping par cible</h3></div>
<p class="muted small" data-i18n="pMappingHelp"></p>
<div id="pMapRows" class="erows"></div>
<p id="pErr" class="login-err" role="alert" hidden></p>
</div>
<footer>
<button id="pDelete" class="btn danger" type="button" data-i18n="delete">Supprimer</button>
<span class="sp"></span>
<button class="btn ghost" type="button" data-close data-i18n="cancel">Annuler</button>
<button class="btn primary" type="submit" data-i18n="save">Enregistrer</button>
</footer>
</form>
</dialog>
<dialog id="settingsDlg" class="settings" aria-labelledby="settingsTitle">
<div class="dlg">
<header>
<h2 id="settingsTitle" data-i18n="settings">Paramètres</h2>
<button class="icon-btn" type="button" data-close data-i18n-aria="close"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
</header>
<div class="set-body">
<div class="set-side">
<nav id="setNav" class="set-nav" role="tablist" aria-orientation="vertical">
<button type="button" role="tab" data-tab="sync">
<svg viewBox="0 0 24 24"><path d="M3 12a9 9 0 0 1 15.7-6L21 8M21 3v5h-5M21 12a9 9 0 0 1-15.7 6L3 16M3 21v-5h5"/></svg>
<span data-i18n="tabSync">Réplication</span>
</button>
<button type="button" role="tab" data-tab="users" data-admin>
<!-- Lucide "users" (ISC license) -->
<svg viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<span data-i18n="tabUsers">Utilisateurs</span>
</button>
<button type="button" role="tab" data-tab="account">
<!-- Lucide "key-round" (ISC license) -->
<svg viewBox="0 0 24 24"><path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"/><circle cx="16.5" cy="7.5" r=".5" fill="currentColor"/></svg>
<span data-i18n="tabAccount">Compte</span>
</button>
<button type="button" role="tab" data-tab="appearance">
<svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/></svg>
<span data-i18n="tabAppearance">Apparence</span>
</button>
</nav>
<div id="setActions" class="set-actions" hidden>
<button id="setSave" class="btn save" type="button" disabled>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5 10 17l9-10"/></svg>
<span data-i18n="save">Enregistrer</span>
</button>
<button id="setCancel" class="btn ghost" type="button" disabled data-i18n="cancel">Annuler</button>
<p id="setState" class="set-state" role="status"></p>
</div>
</div>
<div class="set-panels">
<!-- Synchronization settings -->
<div class="set-panel" role="tabpanel" data-panel="sync">
<p class="ro-note" data-ro data-i18n="roNote" hidden></p>
<section class="set-section" data-edit>
<h3 data-i18n="kindsTitle">Éléments répliqués</h3>
<div id="kindChips" class="chip-field"></div>
<p class="muted small mt-s" data-i18n="kindsHelp"></p>
</section>
<section class="set-section danger" data-edit>
<h3 data-i18n="pruneTitle">Suppression</h3>
<label class="switch-row"><input id="pruneSw" type="checkbox" class="switch"><span data-i18n="pruneLabel">Supprimer sur les cibles les objets absents du primaire</span></label>
<p class="muted small mt-s" data-i18n="pruneHelp"></p>
</section>
<section class="set-section span" data-edit>
<h3 data-i18n="selTitle">Sélection des objets</h3>
<p class="muted small" data-i18n="selHelp"></p>
<div class="erow-head sel"><span data-i18n="colKind">Type</span><span data-i18n="colInclude">Inclure</span><span data-i18n="colExclude">Exclure</span></div>
<div id="selRows" class="erows"></div>
</section>
<section class="set-section span" data-edit>
<h3 data-i18n="polTitle">Politiques par champ</h3>
<p class="muted small" data-i18n="polHelp"></p>
<ul id="polDefaults" class="muted small"></ul>
<div class="erow-head pol"><span data-i18n="colKind">Type</span><span data-i18n="colField">Champ</span><span data-i18n="colPolicy">Politique</span><span></span></div>
<div id="polRows" class="erows"></div>
<div class="toolbar"><button id="polAdd" class="btn tool" type="button" data-i18n="addRow">+ Ajouter</button></div>
</section>
</div>
<!-- Users -->
<div class="set-panel wide" role="tabpanel" data-panel="users" hidden>
<section class="set-section">
<h3 data-i18n="tabUsers">Utilisateurs</h3>
<p class="muted small" data-i18n="usersHelp"></p>
<div class="erow-head user"><span data-i18n="colUser">Utilisateur</span><span data-i18n="colRole">Rôle</span><span data-i18n="colPassword">Nouveau mot de passe</span><span data-i18n="colLastLogin">Dernière connexion</span><span></span></div>
<div id="userRows" class="erows"></div>
<div class="toolbar"><button id="userAdd" class="btn tool" type="button" data-i18n="addUser">+ Ajouter un utilisateur</button></div>
</section>
</div>
<!-- Account -->
<div class="set-panel" role="tabpanel" data-panel="account" hidden>
<section class="set-section">
<h3 data-i18n="accountTitle">Mon compte</h3>
<dl id="accountInfo" class="field-grid small"></dl>
</section>
<section class="set-section" id="pwdSection">
<h3 data-i18n="pwdTitle">Changer de mot de passe</h3>
<form id="pwdForm" class="field-grid">
<input type="text" autocomplete="username" hidden>
<label for="pwdOld" data-i18n="pwdOld">Mot de passe actuel</label>
<input id="pwdOld" class="inp" type="password" autocomplete="current-password" required>
<label for="pwdNew" data-i18n="pwdNew">Nouveau</label>
<input id="pwdNew" class="inp" type="password" autocomplete="new-password" minlength="8" required>
<label for="pwdNew2" data-i18n="pwdNew2">Confirmation</label>
<input id="pwdNew2" class="inp" type="password" autocomplete="new-password" minlength="8" required>
<span></span>
<div><button class="btn primary" type="submit" data-i18n="pwdBtn">Changer le mot de passe</button></div>
</form>
</section>
</div>
<!-- Appearance -->
<div class="set-panel" role="tabpanel" data-panel="appearance" hidden>
<section class="set-section">
<h3 data-i18n="language">Langue</h3>
<div id="langSwitch" class="seg" role="radiogroup" data-i18n-aria="language">
<button type="button" role="radio" data-lang="fr">Français</button>
<button type="button" role="radio" data-lang="en">English</button>
</div>
</section>
<section class="set-section">
<h3 data-i18n="themeTitle">Thème</h3>
<div id="themeSwitch" class="seg" role="radiogroup" data-i18n-aria="themeTitle">
<button type="button" role="radio" data-theme-opt="system" data-i18n="themeSystem">Système</button>
<button type="button" role="radio" data-theme-opt="light" data-i18n="themeLight">Clair</button>
<button type="button" role="radio" data-theme-opt="dark" data-i18n="themeDark">Sombre</button>
</div>
</section>
<section class="set-section">
<h3 data-i18n="density">Densité</h3>
<div id="densitySwitch" class="seg" role="radiogroup" data-i18n-aria="density">
<button type="button" role="radio" data-density="normal" data-i18n="densityNormal">Normale</button>
<button type="button" role="radio" data-density="compact" data-i18n="densityCompact">Compacte</button>
</div>
</section>
</div>
</div>
</div>
</div>
</dialog>
<div id="toast" class="toast" role="status" hidden></div>
<script src="app.js"></script>
</body>
</html>