The local mode now shows a login page in the colors of the UI (light/dark theme, FR/EN) with a signed session cookie and the log out button, like the OIDC mode. LOGIN_LOGO shows a PNG mounted in the container on that page. SESSION_TTL applies to both modes (OIDC_SESSION_TTL still works). HTTP Basic credentials are still accepted for scripts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
96 lines
4.9 KiB
HTML
96 lines
4.9 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Logstream</title>
|
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%236366f1'/%3E%3Cpath d='M8 10h16M8 16h11M8 22h14' stroke='white' stroke-width='3' stroke-linecap='round'/%3E%3C/svg%3E">
|
|
<link rel="stylesheet" href="style.css">
|
|
<script>
|
|
// Same saved theme and language as the UI, applied before first paint.
|
|
try {
|
|
var t = localStorage.getItem('logstream.theme');
|
|
if (t === 'light' || t === 'dark') document.documentElement.dataset.theme = t;
|
|
var l = localStorage.getItem('logstream.lang');
|
|
if (l) document.documentElement.lang = l;
|
|
} catch (e) {}
|
|
</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">
|
|
<img id="logo" class="login-logo" src="auth/logo" alt="" hidden>
|
|
<div class="brand">
|
|
<svg viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="8"/><path d="M8 10h16M8 16h11M8 22h14"/></svg>
|
|
<span>Logstream</span>
|
|
</div>
|
|
<p class="muted" data-i18n="intro">Sign in to view the logs.</p>
|
|
|
|
<form method="post" action="auth/login">
|
|
<input type="hidden" name="r" id="ret">
|
|
<label for="user" data-i18n="user">User</label>
|
|
<input id="user" name="user" type="text" autocomplete="username" autocapitalize="none" spellcheck="false" required autofocus>
|
|
<label for="pass" data-i18n="pass">Password</label>
|
|
<input id="pass" name="pass" type="password" autocomplete="current-password" required>
|
|
<p id="err" class="login-err" role="alert" data-i18n="error" hidden>Wrong user or password.</p>
|
|
<button class="btn primary" type="submit" data-i18n="submit">Sign in</button>
|
|
</form>
|
|
</main>
|
|
|
|
<script>
|
|
(function () {
|
|
var I18N = {
|
|
en: { intro: 'Sign in to view the logs.', user: 'User', pass: 'Password', submit: 'Sign in',
|
|
error: 'Wrong user or password.', theme: 'Light / dark theme' },
|
|
fr: { intro: 'Connectez-vous pour consulter les logs.', user: 'Utilisateur', pass: 'Mot de passe',
|
|
submit: 'Se connecter', error: 'Utilisateur ou mot de passe incorrect.', theme: 'Thème clair / sombre' },
|
|
};
|
|
function get(k) { try { return localStorage.getItem('logstream.' + k); } catch (e) { return null; } }
|
|
function set(k, v) { try { localStorage.setItem('logstream.' + k, v); } catch (e) {} }
|
|
var lang = get('lang');
|
|
if (!I18N[lang]) lang = /^fr\b/i.test(navigator.language || '') ? 'fr' : 'en';
|
|
|
|
function applyLang() {
|
|
var d = I18N[lang];
|
|
document.documentElement.lang = lang;
|
|
document.querySelectorAll('[data-i18n]').forEach(function (el) { el.textContent = d[el.dataset.i18n]; });
|
|
document.querySelectorAll('[data-i18n-title]').forEach(function (el) { el.title = d[el.dataset.i18nTitle]; });
|
|
document.querySelectorAll('[data-i18n-aria]').forEach(function (el) { el.setAttribute('aria-label', d[el.dataset.i18nAria]); });
|
|
document.querySelectorAll('#langSwitch [data-lang]').forEach(function (b) { b.setAttribute('aria-checked', String(b.dataset.lang === lang)); });
|
|
}
|
|
document.querySelectorAll('#langSwitch [data-lang]').forEach(function (b) {
|
|
b.addEventListener('click', function () { lang = b.dataset.lang; set('lang', lang); applyLang(); });
|
|
});
|
|
|
|
document.getElementById('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);
|
|
document.getElementById('ret').value = q.get('r') || '/';
|
|
document.getElementById('err').hidden = q.get('e') !== '1';
|
|
|
|
// LOGIN_LOGO: shown only when the server has one.
|
|
var logo = document.getElementById('logo');
|
|
logo.addEventListener('load', function () { logo.hidden = false; });
|
|
if (logo.complete && logo.naturalWidth) logo.hidden = false;
|
|
|
|
applyLang();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|