2026-03-03 10:20:07 -06:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" / >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" / >
< title > Signature Manager — Dashboard< / title >
< style >
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: Arial, sans-serif; background: #1a1a1a; color: #eee; }
header { background: #111; border-bottom: 2px solid #C9A84C; padding: 14px 24px; display: flex; align-items: center; gap: 16px; }
header h1 { font-size: 18px; color: #C9A84C; }
nav a { color: #ccc; text-decoration: none; margin-left: 16px; font-size: 14px; }
nav a:hover { color: #C9A84C; }
main { padding: 24px; max-width: 1100px; margin: auto; }
.cards { display: flex; gap: 16px; margin-bottom: 24px; flex-wrap: wrap; }
.card { background: #2a2a2a; border: 1px solid #444; border-radius: 8px; padding: 20px; flex: 1; min-width: 200px; }
.card h2 { font-size: 13px; color: #aaa; margin-bottom: 8px; }
.card .val { font-size: 28px; font-weight: bold; color: #C9A84C; }
.actions { display: flex; gap: 12px; margin-bottom: 24px; flex-wrap: wrap; align-items: center; }
button { background: #C9A84C; color: #111; border: none; padding: 10px 20px; border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 14px; }
button:hover { background: #e0bc5c; }
button.secondary { background: #333; color: #eee; border: 1px solid #555; }
button.secondary:hover { background: #444; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th { text-align: left; padding: 10px 12px; background: #222; color: #C9A84C; border-bottom: 2px solid #C9A84C; }
td { padding: 8px 12px; border-bottom: 1px solid #333; vertical-align: top; }
tr:hover td { background: #2a2a2a; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: bold; }
.badge.success { background: #1a4a1a; color: #4caf50; }
.badge.error { background: #4a1a1a; color: #f44336; }
.badge.skipped { background: #3a3a1a; color: #aaa; }
2026-03-03 16:23:30 -06:00
#status-msg { margin-bottom: 12px; padding: 10px 14px; border-radius: 6px; }
#status-msg.ok { background: #1a4a1a; color: #4caf50; }
#status-msg.err { background: #4a1a1a; color: #f44336; }
2026-03-03 10:20:07 -06:00
input[type=email] { padding: 9px 12px; border-radius: 6px; border: 1px solid #555; background: #2a2a2a; color: #eee; font-size: 13px; width: 240px; }
.ts { color: #777; font-size: 11px; }
< / style >
< / head >
< body >
< header >
< h1 > ✉ Email Signature Manager< / h1 >
< nav >
< a href = "/dashboard" > Dashboard< / a >
< a href = "/editor" > Template Editor< / a >
< / nav >
< / header >
< main >
< div class = "cards" >
< div class = "card" > < h2 > Total Users< / h2 > < div class = "val" id = "stat-users" > —< / div > < / div >
< div class = "card" > < h2 > Last Push< / h2 > < div class = "val" id = "stat-last" style = "font-size:14px;margin-top:6px;" > —< / div > < / div >
< div class = "card" > < h2 > Last Run Success< / h2 > < div class = "val" id = "stat-success" > —< / div > < / div >
< div class = "card" > < h2 > Last Run Errors< / h2 > < div class = "val" id = "stat-errors" > —< / div > < / div >
< / div >
< div class = "actions" >
< button onclick = "pushAll()" > ▶ Push to All Users< / button >
< button class = "secondary" onclick = "loadLogs()" > ↻ Refresh Logs< / button >
< input id = "single-email" type = "email" placeholder = "user@domain.com" / >
< button class = "secondary" onclick = "pushSingle()" > Push Single User< / button >
< / div >
2026-03-03 16:23:30 -06:00
< div id = "status-msg" style = "display:none;" > < / div >
2026-03-03 10:20:07 -06:00
< table >
< thead > < tr > < th > Timestamp< / th > < th > User< / th > < th > Name< / th > < th > Status< / th > < th > Message< / th > < / tr > < / thead >
< tbody id = "log-body" > < tr > < td colspan = "5" style = "color:#777;padding:20px;" > Loading...< / td > < / tr > < / tbody >
< / table >
< / main >
< script >
async function loadStats() {
const users = await fetch('/api/admin/users').then(r=>r.json()).catch(()=>[]);
document.getElementById('stat-users').textContent = Array.isArray(users) ? users.length : '—';
const logs = await fetch('/api/admin/logs?limit=500').then(r=>r.json()).catch(()=>[]);
if (logs.length) {
document.getElementById('stat-last').textContent = new Date(logs[0].timestamp).toLocaleString();
const recent = logs.slice(0,50);
document.getElementById('stat-success').textContent = recent.filter(l=>l.status==='success').length;
document.getElementById('stat-errors').textContent = recent.filter(l=>l.status==='error').length;
}
}
async function loadLogs() {
const logs = await fetch('/api/admin/logs?limit=200').then(r=>r.json()).catch(()=>[]);
const tbody = document.getElementById('log-body');
if (!logs.length) { tbody.innerHTML='< tr > < td colspan = "5" style = "color:#777;" > No logs yet.< / td > < / tr > '; return; }
tbody.innerHTML = logs.map(l=>`
< tr >
< td class = "ts" > ${new Date(l.timestamp).toLocaleString()}< / td >
< td > ${l.user_email}< / td >
< td > ${l.display_name||''}< / td >
< td > < span class = "badge ${l.status}" > ${l.status}< / span > < / td >
< td style = "color:#888;font-size:12px;" > ${l.message||''}< / td >
< / tr > `).join('');
}
function showStatus(msg, ok) {
const el = document.getElementById('status-msg');
2026-03-03 16:23:30 -06:00
el.textContent = msg;
el.className = ok ? 'ok' : 'err';
el.style.display = 'block';
setTimeout(() => { el.style.display = 'none'; }, 7000);
2026-03-03 10:20:07 -06:00
}
async function pushAll() {
if (!confirm('Push signatures to ALL users?')) return;
showStatus('Pushing to all users... this may take a minute.', true);
const res = await fetch('/api/push/all',{method:'POST'}).then(r=>r.json()).catch(e=>({ok:false,error:e.message}));
if (res.ok) {
const s=res.results.filter(r=>r.status==='success').length;
const e=res.results.filter(r=>r.status==='error').length;
showStatus(`Done! ${s} success, ${e} errors.`, e===0);
} else { showStatus('Error: '+res.error, false); }
loadLogs(); loadStats();
}
async function pushSingle() {
const email = document.getElementById('single-email').value.trim();
if (!email) return alert('Enter a user email first.');
showStatus(`Pushing to ${email}...`, true);
const res = await fetch('/api/push/user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email})}).then(r=>r.json()).catch(e=>({ok:false,error:e.message}));
if (res.ok & & res.results[0]?.status==='success') {
showStatus(`Success: signature pushed to ${email}`, true);
} else { showStatus('Error: '+(res.error||res.results?.[0]?.message||'unknown'), false); }
loadLogs();
}
loadStats(); loadLogs();
< / script >
< / body >
< / html >