feat: AMPP Safe splash on login + first-visit overlay
Adds the BMG-branded "AMPP Safe" hardhat photo as the visual identity for the auth + first-load surfaces. * services/web-ui/public/img/ampp-safe.jpg (52 KB, 1200w optimized JPEG) * services/web-ui/public/login.html: full redesign as a two-column hero + sign-in panel. Hero shows the hardhat photo full-bleed with a subtle AMPP Safe pill badge and broadcast-safe caption. Login + first-run admin setup forms unchanged functionally. * services/web-ui/public/index.html: brief first-visit splash overlay (~1.4s) using the same image. Dismisses to the library and uses sessionStorage so it only shows once per session.
This commit is contained in:
parent
72545126c4
commit
f99f07e0e7
3 changed files with 130 additions and 292 deletions
BIN
services/web-ui/public/img/ampp-safe.jpg
Normal file
BIN
services/web-ui/public/img/ampp-safe.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
|
|
@ -294,9 +294,22 @@
|
||||||
}
|
}
|
||||||
.asset-action-btn:hover { background: oklch(8% 0.010 250 / 0.9); }
|
.asset-action-btn:hover { background: oklch(8% 0.010 250 / 0.9); }
|
||||||
.asset-action-btn svg { width: 13px; height: 13px; }
|
.asset-action-btn svg { width: 13px; height: 13px; }
|
||||||
|
|
||||||
|
.first-splash{position:fixed;inset:0;z-index:60;background:radial-gradient(ellipse at 50% 45%,#1a1d28 0%,#08090d 70%);display:flex;align-items:center;justify-content:center;flex-direction:column;gap:24px;opacity:1;transition:opacity .55s ease-out, visibility .55s}
|
||||||
|
.first-splash.hidden{opacity:0;visibility:hidden;pointer-events:none}
|
||||||
|
.first-splash-img{width:min(420px,46vw);aspect-ratio:1963/1236;background-image:url(img/ampp-safe.jpg);background-size:contain;background-position:center;background-repeat:no-repeat;filter:drop-shadow(0 20px 40px rgba(232,160,32,.15))}
|
||||||
|
.first-splash-stamp{display:flex;align-items:center;gap:10px;font-size:11px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:oklch(76% 0.178 52)}
|
||||||
|
.first-splash-dot{width:8px;height:8px;background:oklch(76% 0.178 52);border-radius:50%;animation:fsPulse 1.4s ease-in-out infinite}
|
||||||
|
@keyframes fsPulse{0%,100%{opacity:.35;transform:scale(.9)}50%{opacity:1;transform:scale(1.1)}}
|
||||||
|
.first-splash-title{font-size:13px;color:var(--text-secondary);letter-spacing:.04em}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="firstSplash" class="first-splash" aria-hidden="true">
|
||||||
|
<div class="first-splash-img"></div>
|
||||||
|
<div class="first-splash-stamp"><span class="first-splash-dot"></span><span>AMPP Safe</span></div>
|
||||||
|
<div class="first-splash-title">Z-AMPP — Media Asset Management</div>
|
||||||
|
</div>
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<nav class="sidebar" aria-label="Main navigation">
|
<nav class="sidebar" aria-label="Main navigation">
|
||||||
|
|
@ -499,6 +512,17 @@
|
||||||
|
|
||||||
// ── Init ────────────────────────────────
|
// ── Init ────────────────────────────────
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
// First-visit splash. After first dismiss in a session we skip it.
|
||||||
|
const splash = document.getElementById('firstSplash');
|
||||||
|
if (splash) {
|
||||||
|
if (sessionStorage.getItem('splashShown')) {
|
||||||
|
splash.remove();
|
||||||
|
} else {
|
||||||
|
sessionStorage.setItem('splashShown', '1');
|
||||||
|
setTimeout(() => splash.classList.add('hidden'), 1400);
|
||||||
|
setTimeout(() => splash.remove(), 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
await loadProjects();
|
await loadProjects();
|
||||||
setupDrag();
|
setupDrag();
|
||||||
setupSearch();
|
setupSearch();
|
||||||
|
|
|
||||||
|
|
@ -3,189 +3,86 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Wild Dragon MAM — Login</title>
|
<title>Sign in - Z-AMPP</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
:root{
|
||||||
:root {
|
--bg:#08090d;--surface:#11141b;--surface-2:#171a23;--border:#1f2330;
|
||||||
--bg: #0d0f14;
|
--accent:#e8a020;--accent-strong:#f0b740;--text:#e8eaf0;--text-dim:#7a8195;
|
||||||
--surface: #161921;
|
--error:#e05555;--success:#3ecf6a;--radius:8px;--input-h:42px;
|
||||||
--border: #262c3a;
|
|
||||||
--accent: #e8a020;
|
|
||||||
--text: #e8eaf0;
|
|
||||||
--text-dim: #6b7280;
|
|
||||||
--error: #e05555;
|
|
||||||
--success: #3ecf6a;
|
|
||||||
--radius: 6px;
|
|
||||||
--input-h: 40px;
|
|
||||||
}
|
}
|
||||||
|
html,body{height:100%;background:var(--bg);color:var(--text);font-family:'Inter',-apple-system,sans-serif;font-size:14px;line-height:1.4;-webkit-font-smoothing:antialiased}
|
||||||
|
body{display:grid;grid-template-columns:1fr 460px;min-height:100vh}
|
||||||
|
@media (max-width: 900px){ body{grid-template-columns:1fr;grid-template-rows:40vh 1fr} }
|
||||||
|
|
||||||
html, body {
|
/* === Hero === */
|
||||||
height: 100%;
|
.hero{position:relative;overflow:hidden;background:radial-gradient(ellipse at 30% 40%,#1a1d28 0%,#0a0b10 70%);display:flex;align-items:flex-end;padding:48px 56px}
|
||||||
background: var(--bg);
|
.hero-img{position:absolute;inset:0;background-image:url(img/ampp-safe.jpg);background-size:contain;background-position:center;background-repeat:no-repeat}
|
||||||
color: var(--text);
|
.hero-grad-bot{position:absolute;inset:auto 0 0 0;height:40%;background:linear-gradient(to top,rgba(8,9,13,.85),transparent);pointer-events:none}
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
.hero-stamp{position:absolute;top:32px;left:32px;display:flex;align-items:center;gap:10px;z-index:2;background:rgba(8,9,13,.55);backdrop-filter:blur(6px);padding:8px 14px 8px 12px;border:1px solid rgba(232,160,32,.25);border-radius:999px}
|
||||||
font-size: 14px;
|
.hero-stamp-dot{width:8px;height:8px;background:var(--accent);border-radius:50%;box-shadow:0 0 12px var(--accent)}
|
||||||
}
|
.hero-stamp-text{font-size:11px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--accent-strong)}
|
||||||
|
.hero-caption{position:relative;z-index:2;max-width:520px}
|
||||||
|
.hero-caption h2{font-size:28px;font-weight:600;letter-spacing:-.01em;line-height:1.15;margin-bottom:10px}
|
||||||
|
.hero-caption p{color:var(--text-dim);font-size:14px;line-height:1.55}
|
||||||
|
.hero-caption .accent{color:var(--accent-strong)}
|
||||||
|
|
||||||
body {
|
/* === Panel === */
|
||||||
display: flex;
|
.panel{display:flex;flex-direction:column;justify-content:center;padding:48px 56px;background:var(--surface);border-left:1px solid var(--border)}
|
||||||
align-items: center;
|
@media (max-width: 900px){ .panel{padding:32px 24px;border-left:none;border-top:1px solid var(--border)} }
|
||||||
justify-content: center;
|
.brand{display:flex;align-items:center;gap:10px;margin-bottom:36px}
|
||||||
}
|
.brand-icon{width:32px;height:32px;background:var(--accent);color:#0a0b10;border-radius:7px;display:flex;align-items:center;justify-content:center;font-weight:700}
|
||||||
|
.brand-name{font-weight:600;font-size:15px;letter-spacing:-.01em}
|
||||||
.card {
|
.brand-sub{font-size:10px;color:var(--text-dim);letter-spacing:.14em;text-transform:uppercase;margin-top:1px}
|
||||||
width: 360px;
|
h1{font-size:22px;font-weight:600;letter-spacing:-.01em;margin-bottom:6px}
|
||||||
background: var(--surface);
|
.subtitle{color:var(--text-dim);font-size:13px;margin-bottom:28px}
|
||||||
border: 1px solid var(--border);
|
.field{margin-bottom:16px}
|
||||||
border-radius: var(--radius);
|
label{display:block;font-size:11px;font-weight:500;color:var(--text-dim);text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px}
|
||||||
padding: 36px 32px 32px;
|
input{width:100%;height:var(--input-h);background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius);color:var(--text);font-size:14px;font-family:inherit;padding:0 14px;outline:none;transition:border-color .15s, background .15s}
|
||||||
}
|
input:focus{border-color:var(--accent);background:var(--bg)}
|
||||||
|
.btn{width:100%;height:42px;background:var(--accent);border:none;border-radius:var(--radius);color:#0a0b10;font-weight:600;font-size:14px;font-family:inherit;cursor:pointer;margin-top:10px;transition:background .15s}
|
||||||
.logo {
|
.btn:hover{background:var(--accent-strong)}
|
||||||
display: flex;
|
.btn:disabled{opacity:.5;cursor:not-allowed}
|
||||||
align-items: center;
|
.flash{border-radius:var(--radius);padding:10px 12px;font-size:13px;margin-bottom:18px;display:none}
|
||||||
gap: 10px;
|
.flash.error{background:rgba(224,85,85,.1);border:1px solid rgba(224,85,85,.4);color:var(--error);display:block}
|
||||||
margin-bottom: 28px;
|
.flash.success{background:rgba(62,207,106,.1);border:1px solid rgba(62,207,106,.4);color:var(--success);display:block}
|
||||||
}
|
.setup-link{text-align:center;margin-top:24px;font-size:12px;color:var(--text-dim)}
|
||||||
|
.setup-link a{color:var(--accent-strong);text-decoration:none}
|
||||||
.logo-icon {
|
.setup-link a:hover{color:var(--accent);text-decoration:underline}
|
||||||
width: 32px;
|
#setup-panel{display:none}
|
||||||
height: 32px;
|
|
||||||
background: var(--accent);
|
|
||||||
border-radius: 6px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-text {
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-name {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 15px;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-sub {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--text-dim);
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
color: var(--text-dim);
|
|
||||||
font-size: 13px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-dim);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 100%;
|
|
||||||
height: var(--input-h);
|
|
||||||
background: var(--bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
color: var(--text);
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 0 12px;
|
|
||||||
outline: none;
|
|
||||||
transition: border-color 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus {
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: 100%;
|
|
||||||
height: 40px;
|
|
||||||
background: var(--accent);
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
color: #0d0f14;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-top: 8px;
|
|
||||||
transition: opacity 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover { opacity: 0.88; }
|
|
||||||
.btn:active { opacity: 0.76; }
|
|
||||||
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
||||||
|
|
||||||
.flash {
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 10px 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash.error { background: rgba(224,85,85,.12); border: 1px solid var(--error); color: var(--error); display: block; }
|
|
||||||
.flash.success { background: rgba(62,207,106,.12); border: 1px solid var(--success); color: var(--success); display: block; }
|
|
||||||
|
|
||||||
.setup-link {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 20px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.setup-link a {
|
|
||||||
color: var(--accent);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setup-link a:hover { text-decoration: underline; }
|
|
||||||
|
|
||||||
/* Setup panel (hidden by default) */
|
|
||||||
#setup-panel { display: none; }
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="card">
|
<section class="hero" aria-hidden="true">
|
||||||
<div class="logo">
|
<div class="hero-img"></div>
|
||||||
<div class="logo-icon">🎲</div>
|
<div class="hero-grad-bot"></div>
|
||||||
<div class="logo-text">
|
<div class="hero-stamp">
|
||||||
<div class="logo-name">Wild Dragon</div>
|
<span class="hero-stamp-dot"></span>
|
||||||
<div class="logo-sub">Media Asset Management</div>
|
<span class="hero-stamp-text">AMPP Safe</span>
|
||||||
|
</div>
|
||||||
|
<div class="hero-caption">
|
||||||
|
<h2>Broadcast-safe media,<br><span class="accent">end to end.</span></h2>
|
||||||
|
<p>Z-AMPP is the Wild Dragon media-asset hub. Ingest live SRT & RTMP feeds, upload masters, and hand proxies to your editors in seconds.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div class="brand">
|
||||||
|
<div class="brand-icon">Z</div>
|
||||||
|
<div>
|
||||||
|
<div class="brand-name">Z-AMPP</div>
|
||||||
|
<div class="brand-sub">Media Asset Management</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Flash message -->
|
|
||||||
<div id="flash" class="flash"></div>
|
<div id="flash" class="flash"></div>
|
||||||
|
|
||||||
<!-- Login form -->
|
|
||||||
<div id="login-panel">
|
<div id="login-panel">
|
||||||
<h1>Sign in</h1>
|
<h1>Sign in</h1>
|
||||||
<p class="subtitle">Enter your credentials to continue</p>
|
<p class="subtitle">Enter your credentials to continue.</p>
|
||||||
|
|
||||||
<form id="login-form" autocomplete="on">
|
<form id="login-form" autocomplete="on">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
|
|
@ -197,142 +94,59 @@
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn" id="login-btn">Sign in</button>
|
<button type="submit" class="btn" id="login-btn">Sign in</button>
|
||||||
</form>
|
</form>
|
||||||
|
<div class="setup-link">First time? <a href="#" id="show-setup">Create admin account</a></div>
|
||||||
<div class="setup-link">
|
|
||||||
First time? <a href="#" id="show-setup">Create admin account</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- First-run setup form (shown only if no users exist) -->
|
|
||||||
<div id="setup-panel">
|
<div id="setup-panel">
|
||||||
<h1>Create admin</h1>
|
<h1>Create admin</h1>
|
||||||
<p class="subtitle">No accounts exist yet — create the first admin</p>
|
<p class="subtitle">No accounts exist yet - create the first admin.</p>
|
||||||
|
|
||||||
<form id="setup-form" autocomplete="off">
|
<form id="setup-form" autocomplete="off">
|
||||||
<div class="field">
|
<div class="field"><label for="su-username">Username</label><input id="su-username" name="username" type="text" required placeholder="admin"></div>
|
||||||
<label for="su-username">Username</label>
|
<div class="field"><label for="su-password">Password (min 8 chars)</label><input id="su-password" name="password" type="password" required minlength="8" placeholder="••••••••"></div>
|
||||||
<input id="su-username" name="username" type="text" required placeholder="admin">
|
<div class="field"><label for="su-display">Display name (optional)</label><input id="su-display" name="display_name" type="text" placeholder="Admin"></div>
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="su-password">Password (min 8 chars)</label>
|
|
||||||
<input id="su-password" name="password" type="password" required minlength="8" placeholder="••••••••">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="su-display">Display name (optional)</label>
|
|
||||||
<input id="su-display" name="display_name" type="text" placeholder="Admin">
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn" id="setup-btn">Create account</button>
|
<button type="submit" class="btn" id="setup-btn">Create account</button>
|
||||||
</form>
|
</form>
|
||||||
|
<div class="setup-link"><a href="#" id="show-login">Back to login</a></div>
|
||||||
<div class="setup-link">
|
|
||||||
<a href="#" id="show-login">← Back to login</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const API = '/api/v1/auth';
|
const API = '/api/v1/auth';
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
const flash = $('flash');
|
||||||
|
function showFlash(m,t){ flash.textContent=m; flash.className='flash '+t; }
|
||||||
|
function clearFlash(){ flash.className='flash'; flash.textContent=''; }
|
||||||
|
|
||||||
const flash = document.getElementById('flash');
|
$('show-setup').onclick = e => { e.preventDefault(); clearFlash(); $('login-panel').style.display='none'; $('setup-panel').style.display='block'; };
|
||||||
const loginPanel = document.getElementById('login-panel');
|
$('show-login').onclick = e => { e.preventDefault(); clearFlash(); $('setup-panel').style.display='none'; $('login-panel').style.display='block'; };
|
||||||
const setupPanel = document.getElementById('setup-panel');
|
|
||||||
const loginForm = document.getElementById('login-form');
|
|
||||||
const setupForm = document.getElementById('setup-form');
|
|
||||||
const loginBtn = document.getElementById('login-btn');
|
|
||||||
const setupBtn = document.getElementById('setup-btn');
|
|
||||||
|
|
||||||
function showFlash(msg, type) {
|
$('login-form').onsubmit = async (e) => {
|
||||||
flash.textContent = msg;
|
e.preventDefault(); clearFlash();
|
||||||
flash.className = 'flash ' + type;
|
const btn=$('login-btn'); btn.disabled=true; btn.textContent='Signing in...';
|
||||||
}
|
try{
|
||||||
function clearFlash() {
|
const res = await fetch(API + '/login', {method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',
|
||||||
flash.className = 'flash';
|
body: JSON.stringify({username:$('username').value.trim(),password:$('password').value})});
|
||||||
flash.textContent = '';
|
if(res.ok){ showFlash('Signed in - redirecting...','success'); setTimeout(()=>{location.href='/'},600); }
|
||||||
}
|
else{ const d=await res.json().catch(()=>({})); showFlash(d.error||'Login failed','error'); }
|
||||||
|
} catch(err){ showFlash('Network error: '+err.message,'error'); }
|
||||||
|
finally{ btn.disabled=false; btn.textContent='Sign in'; }
|
||||||
|
};
|
||||||
|
|
||||||
// Toggle between login and setup panels
|
$('setup-form').onsubmit = async (e) => {
|
||||||
document.getElementById('show-setup').addEventListener('click', (e) => {
|
e.preventDefault(); clearFlash();
|
||||||
e.preventDefault();
|
const btn=$('setup-btn'); btn.disabled=true; btn.textContent='Creating...';
|
||||||
clearFlash();
|
try{
|
||||||
loginPanel.style.display = 'none';
|
const res = await fetch(API + '/setup', {method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',
|
||||||
setupPanel.style.display = 'block';
|
body: JSON.stringify({username:$('su-username').value.trim(),password:$('su-password').value,display_name:$('su-display').value.trim()})});
|
||||||
});
|
if(res.ok){
|
||||||
document.getElementById('show-login').addEventListener('click', (e) => {
|
showFlash('Admin account created - you can now log in','success');
|
||||||
e.preventDefault();
|
setTimeout(()=>{ $('setup-panel').style.display='none'; $('login-panel').style.display='block'; },1200);
|
||||||
clearFlash();
|
} else{
|
||||||
setupPanel.style.display = 'none';
|
const d=await res.json().catch(()=>({}));
|
||||||
loginPanel.style.display = 'block';
|
showFlash(d.error||'Setup failed','error');
|
||||||
});
|
|
||||||
|
|
||||||
// Login
|
|
||||||
loginForm.addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
clearFlash();
|
|
||||||
loginBtn.disabled = true;
|
|
||||||
loginBtn.textContent = 'Signing in…';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API}/login`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
credentials: 'same-origin',
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: document.getElementById('username').value.trim(),
|
|
||||||
password: document.getElementById('password').value,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
showFlash('Signed in — redirecting…', 'success');
|
|
||||||
setTimeout(() => { window.location.href = '/'; }, 600);
|
|
||||||
} else {
|
|
||||||
const data = await res.json().catch(() => ({}));
|
|
||||||
showFlash(data.error || 'Login failed', 'error');
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch(err){ showFlash('Network error: '+err.message,'error'); }
|
||||||
showFlash('Network error: ' + err.message, 'error');
|
finally{ btn.disabled=false; btn.textContent='Create account'; }
|
||||||
} finally {
|
};
|
||||||
loginBtn.disabled = false;
|
|
||||||
loginBtn.textContent = 'Sign in';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// First-run setup
|
|
||||||
setupForm.addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
clearFlash();
|
|
||||||
setupBtn.disabled = true;
|
|
||||||
setupBtn.textContent = 'Creating…';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API}/setup`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
credentials: 'same-origin',
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: document.getElementById('su-username').value.trim(),
|
|
||||||
password: document.getElementById('su-password').value,
|
|
||||||
display_name: document.getElementById('su-display').value.trim(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
showFlash('Admin account created — you can now log in', 'success');
|
|
||||||
setTimeout(() => {
|
|
||||||
setupPanel.style.display = 'none';
|
|
||||||
loginPanel.style.display = 'block';
|
|
||||||
}, 1200);
|
|
||||||
} else {
|
|
||||||
const data = await res.json().catch(() => ({}));
|
|
||||||
showFlash(data.error || 'Setup failed', 'error');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
showFlash('Network error: ' + err.message, 'error');
|
|
||||||
} finally {
|
|
||||||
setupBtn.disabled = false;
|
|
||||||
setupBtn.textContent = 'Create account';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body></html>
|
||||||
</html>
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue