server : API (état, dry-run, apply avec empreinte et confirmation SDN, journal, paramètres), page de connexion

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015a8P6WMRPeqdpd5VCh6jAy
This commit is contained in:
claude BotandClaude Opus 5.5 committed 2026-10-13 16:03:56 +02:00
1 parent 3aae9ce925
commit 010034b988
7 files changed
+1014

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
// Saved theme, language and density applied before the first paint (no flash).
// External file: the Content-Security-Policy forbids inline scripts.
try {
var t = localStorage.getItem('confsync.theme');
if (t === 'light' || t === 'dark') document.documentElement.dataset.theme = t;
var l = localStorage.getItem('confsync.lang');
if (l) document.documentElement.lang = l;
var d = localStorage.getItem('confsync.density');
if (d === 'compact') document.documentElement.dataset.density = d;
} catch (e) {}
+9
View File
@@ -0,0 +1,9 @@
// Package web holds the pages of the web UI, embedded in the binary.
package web
import "embed"
// FS is index.html, login.html, style.css, boot.js, app.js and favicon.svg.
//
//go:embed *.html *.css *.js *.svg
var FS embed.FS
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="8" fill="#6366f1"/><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" fill="none" stroke="#fff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 312 B

+47
View File
@@ -0,0 +1,47 @@
<!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 class="login-page">
<div class="login-tools">
<div class="seg" role="radiogroup" id="langSwitch">
<button type="button" role="radio" data-lang="fr">FR</button>
<button type="button" role="radio" data-lang="en">EN</button>
</div>
<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>
</div>
<main class="login-card">
<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>
</div>
<p class="muted" data-i18n="intro">Synchronisation de configuration Proxmox VE</p>
<a id="ssoBtn" class="btn primary" href="auth/oidc" hidden data-i18n="sso">Se connecter avec le SSO</a>
<div id="sep" class="login-sep" hidden data-i18n="or">ou</div>
<form id="localForm" method="post" action="auth/login" hidden>
<input type="hidden" name="r" id="ret">
<label for="user" data-i18n="user">Utilisateur</label>
<input id="user" name="user" type="text" autocomplete="username" autocapitalize="none" spellcheck="false" required autofocus>
<label for="pass" data-i18n="pass">Mot de passe</label>
<input id="pass" name="pass" type="password" autocomplete="current-password" required>
<p id="err" class="login-err" role="alert" data-i18n="error" hidden>Utilisateur ou mot de passe incorrect.</p>
<button id="submitBtn" class="btn" type="submit" data-i18n="submit">Se connecter</button>
</form>
<p id="demoHint" class="demo-hint" hidden></p>
</main>
<script src="login.js"></script>
</body>
</html>
+67
View File
@@ -0,0 +1,67 @@
// Login page: language, theme, local form and/or OIDC button (from /auth/config).
(function () {
var I18N = {
fr: { intro: 'Synchronisation de configuration Proxmox VE', user: 'Utilisateur', pass: 'Mot de passe',
submit: 'Se connecter', sso: 'Se connecter avec le SSO', or: 'ou',
error: 'Utilisateur ou mot de passe incorrect.', theme: 'Thème clair / sombre',
demo: 'Mode démo : comptes {a}, {o} et {l}, mot de passe {p}.' },
en: { intro: 'Proxmox VE configuration synchronization', user: 'User', pass: 'Password',
submit: 'Sign in', sso: 'Sign in with SSO', or: 'or',
error: 'Wrong user or password.', theme: 'Light / dark theme',
demo: 'Demo mode: accounts {a}, {o} and {l}, password {p}.' },
};
function get(k) { try { return localStorage.getItem('confsync.' + k); } catch (e) { return null; } }
function set(k, v) { try { localStorage.setItem('confsync.' + k, v); } catch (e) {} }
var lang = get('lang');
if (!I18N[lang]) lang = /^en\b/i.test(navigator.language || '') ? 'en' : 'fr';
var cfg = { local: true, oidc: false, demo: false };
function el(id) { return document.getElementById(id); }
function code(s) { var c = document.createElement('code'); c.textContent = s; return c; }
function applyLang() {
var d = I18N[lang];
document.documentElement.lang = lang;
document.querySelectorAll('[data-i18n]').forEach(function (e) { e.textContent = d[e.dataset.i18n]; });
document.querySelectorAll('[data-i18n-title]').forEach(function (e) { e.title = d[e.dataset.i18nTitle]; });
document.querySelectorAll('[data-i18n-aria]').forEach(function (e) { e.setAttribute('aria-label', d[e.dataset.i18nAria]); });
document.querySelectorAll('#langSwitch [data-lang]').forEach(function (b) { b.setAttribute('aria-checked', String(b.dataset.lang === lang)); });
var h = el('demoHint');
h.textContent = '';
if (cfg.demo) {
// "{a}" placeholders become <code> elements, never HTML from a string
d.demo.split(/(\{[aolp]\})/).forEach(function (part) {
var v = { '{a}': 'admin', '{o}': 'operateur', '{l}': 'lecteur', '{p}': 'confsync-demo' }[part];
h.appendChild(v ? code(v) : document.createTextNode(part));
});
}
}
document.querySelectorAll('#langSwitch [data-lang]').forEach(function (b) {
b.addEventListener('click', function () { lang = b.dataset.lang; set('lang', lang); applyLang(); });
});
el('themeBtn').addEventListener('click', function () {
var root = document.documentElement;
var dark = root.dataset.theme ? root.dataset.theme === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches;
root.dataset.theme = dark ? 'light' : 'dark';
set('theme', root.dataset.theme);
});
var q = new URLSearchParams(location.search);
var ret = q.get('r') || '/';
el('ret').value = ret;
el('err').hidden = q.get('e') !== '1';
el('ssoBtn').href = 'auth/oidc?r=' + encodeURIComponent(ret);
function show() {
el('localForm').hidden = !cfg.local;
el('ssoBtn').hidden = !cfg.oidc;
el('sep').hidden = !(cfg.local && cfg.oidc);
el('submitBtn').className = cfg.oidc ? 'btn' : 'btn primary';
el('demoHint').hidden = !cfg.demo;
applyLang();
}
show();
fetch('auth/config', { headers: { Accept: 'application/json' } })
.then(function (r) { return r.json(); })
.then(function (c) { cfg = c; show(); })
.catch(function () {});
})();