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:
Zac Gaetano 2026-05-17 13:10:36 -04:00
parent 72545126c4
commit f99f07e0e7
3 changed files with 130 additions and 292 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View file

@ -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();

View file

@ -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 &mdash; 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">&#127922;</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 &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> </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 &mdash; 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="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;"></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="&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> <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">&larr; 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>