dragonflight/services/web-ui/public/login.html
Zac f2b8d5dc4b feat(splash): transparent PNG so the subject composites cleanly
The source image had a black border baked in. Knocked out the dark pixels into an alpha channel so the figure now floats on whatever surface is behind it — the dark gradient on the splash, the panel surface on the loading indicator, anywhere.

Pipeline: source -> resize 1200w -> python/PIL alpha-from-luminance with soft 22-55 luminance ramp -> 8-bit RGBA PNG (267KB).
2026-05-17 18:39:21 -04:00

152 lines
9.2 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#08090d;--surface:#11141b;--surface-2:#171a23;--border:#1f2330;
--accent:#1f3ad0;--accent-strong:#3b50d6;--text:#e8eaf0;--text-dim:#7a8195;
--error:#e05555;--success:#3ecf6a;--radius:8px;--input-h:42px;
}
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} }
/* === Hero === */
.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}
.hero-img{position:absolute;inset:0;background-image:url(img/ampp-safe.png);background-size:contain;background-position:center;background-repeat:no-repeat}
.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}
.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(31,58,208,.25);border-radius:999px}
.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)}
/* === Panel === */
.panel{display:flex;flex-direction:column;justify-content:center;padding:48px 56px;background:var(--surface);border-left:1px solid var(--border)}
@media (max-width: 900px){ .panel{padding:32px 24px;border-left:none;border-top:1px solid var(--border)} }
.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}
.brand-sub{font-size:10px;color:var(--text-dim);letter-spacing:.14em;text-transform:uppercase;margin-top:1px}
h1{font-size:22px;font-weight:600;letter-spacing:-.01em;margin-bottom:6px}
.subtitle{color:var(--text-dim);font-size:13px;margin-bottom:28px}
.field{margin-bottom:16px}
label{display:block;font-size:11px;font-weight:500;color:var(--text-dim);text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px}
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}
.btn:hover{background:var(--accent-strong)}
.btn:disabled{opacity:.5;cursor:not-allowed}
.flash{border-radius:var(--radius);padding:10px 12px;font-size:13px;margin-bottom:18px;display:none}
.flash.error{background:rgba(224,85,85,.1);border:1px solid rgba(224,85,85,.4);color:var(--error);display:block}
.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}
.setup-link a:hover{color:var(--accent);text-decoration:underline}
#setup-panel{display:none}
</style>
</head>
<body>
<section class="hero" aria-hidden="true">
<div class="hero-img"></div>
<div class="hero-grad-bot"></div>
<div class="hero-stamp">
<span class="hero-stamp-dot"></span>
<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 &amp; 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 id="flash" class="flash"></div>
<div id="login-panel">
<h1>Sign in</h1>
<p class="subtitle">Enter your credentials to continue.</p>
<form id="login-form" autocomplete="on">
<div class="field">
<label for="username">Username</label>
<input id="username" name="username" type="text" autocomplete="username" required placeholder="username">
</div>
<div class="field">
<label for="password">Password</label>
<input id="password" name="password" type="password" autocomplete="current-password" required placeholder="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;">
</div>
<button type="submit" class="btn" id="login-btn">Sign in</button>
</form>
<div class="setup-link">First time? <a href="#" id="show-setup">Create admin account</a></div>
</div>
<div id="setup-panel">
<h1>Create admin</h1>
<p class="subtitle">No accounts exist yet - create the first admin.</p>
<form id="setup-form" autocomplete="off">
<div class="field"><label for="su-username">Username</label><input id="su-username" name="username" type="text" required placeholder="admin"></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="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;"></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>
</form>
<div class="setup-link"><a href="#" id="show-login">Back to login</a></div>
</div>
</section>
<script>
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=''; }
$('show-setup').onclick = e => { e.preventDefault(); clearFlash(); $('login-panel').style.display='none'; $('setup-panel').style.display='block'; };
$('show-login').onclick = e => { e.preventDefault(); clearFlash(); $('setup-panel').style.display='none'; $('login-panel').style.display='block'; };
$('login-form').onsubmit = async (e) => {
e.preventDefault(); clearFlash();
const btn=$('login-btn'); btn.disabled=true; btn.textContent='Signing in...';
try{
const res = await fetch(API + '/login', {method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',
body: JSON.stringify({username:$('username').value.trim(),password:$('password').value})});
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'; }
};
$('setup-form').onsubmit = async (e) => {
e.preventDefault(); clearFlash();
const btn=$('setup-btn'); btn.disabled=true; btn.textContent='Creating...';
try{
const res = await fetch(API + '/setup', {method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',
body: JSON.stringify({username:$('su-username').value.trim(),password:$('su-password').value,display_name:$('su-display').value.trim()})});
if(res.ok){
showFlash('Admin account created - you can now log in','success');
setTimeout(()=>{ $('setup-panel').style.display='none'; $('login-panel').style.display='block'; },1200);
} else{
const d=await res.json().catch(()=>({}));
showFlash(d.error||'Setup failed','error');
}
} catch(err){ showFlash('Network error: '+err.message,'error'); }
finally{ btn.disabled=false; btn.textContent='Create account'; }
};
</script>
</body></html>