← back to George Gmail
public-fleet-ui/index.html
170 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fleet Auto-Responder · Dashboard</title>
<style>
:root { --bg:#14110d; --card:#1d1913; --ink:#f3efe8; --mut:#a89c88;
--accent:#7d3c00; --gold:#c9a35e; --rule:#352e23;
--ok:#3f7d4a; --warn:#9a6b1f; --bad:#8a3030; }
* { box-sizing:border-box; margin:0; padding:0; }
body { background:var(--bg); color:var(--ink); font:14px/1.5 -apple-system,Inter,system-ui,sans-serif; }
a { color:var(--gold); }
header { padding:20px 28px; border-bottom:1px solid var(--rule);
display:flex; align-items:center; gap:20px; flex-wrap:wrap; }
h1 { font:600 18px/1 Georgia,serif; letter-spacing:.4px; }
h1 small { color:var(--mut); font:400 12px/1 -apple-system,sans-serif; margin-left:8px; }
.wrap { padding:24px 28px; max-width:1200px; }
.row { display:flex; gap:16px; flex-wrap:wrap; }
.card { background:var(--card); border:1px solid var(--rule); border-radius:8px; padding:16px 18px; }
.stat { flex:1 1 140px; }
.stat .n { font:600 26px/1 Georgia,serif; }
.stat .l { color:var(--mut); font-size:12px; text-transform:uppercase; letter-spacing:.6px; margin-top:6px; }
section { margin-top:24px; }
section h2 { font:600 13px/1 -apple-system,sans-serif; text-transform:uppercase;
letter-spacing:.8px; color:var(--gold); margin-bottom:10px; }
table { width:100%; border-collapse:collapse; font-size:13px; }
th { text-align:left; color:var(--mut); font-weight:600; font-size:11px;
text-transform:uppercase; letter-spacing:.5px; padding:8px 10px; border-bottom:1px solid var(--rule); }
td { padding:7px 10px; border-bottom:1px solid var(--rule); vertical-align:top; }
tr:last-child td { border-bottom:none; }
.mono { font-family:ui-monospace,Menlo,monospace; font-size:12px; }
.pill { display:inline-block; padding:2px 8px; border-radius:10px; font-size:11px; font-weight:600; }
.pill.ok { background:rgba(63,125,74,.2); color:#86d693; }
.pill.skip { background:rgba(154,107,31,.2); color:#e0b56a; }
.pill.off { background:rgba(138,48,48,.25); color:#e09090; }
.toggle { display:flex; align-items:center; gap:10px; margin-left:auto; }
.switch { position:relative; width:48px; height:26px; }
.switch input { opacity:0; width:0; height:0; }
.slider { position:absolute; inset:0; background:#5a5145; border-radius:26px; cursor:pointer; transition:.2s; }
.slider:before { content:""; position:absolute; height:20px; width:20px; left:3px; top:3px;
background:#fff; border-radius:50%; transition:.2s; }
input:checked + .slider { background:var(--ok); }
input:checked + .slider:before { transform:translateX(22px); }
.preview { background:#faf7f2; border:1px solid var(--rule); border-radius:6px; overflow:hidden; }
.preview iframe { width:100%; height:340px; border:0; background:#faf7f2; }
.muted { color:var(--mut); }
.scroll { max-height:380px; overflow:auto; }
.empty { color:var(--mut); padding:14px 10px; font-style:italic; }
select { background:var(--card); color:var(--ink); border:1px solid var(--rule);
border-radius:5px; padding:5px 8px; font-size:12px; }
</style>
</head>
<body>
<header>
<h1>Fleet Auto-Responder <small id="fleetcount"></small></h1>
<div class="muted" id="lastrun">last run: —</div>
<div class="toggle">
<span class="muted">Responder</span>
<label class="switch">
<input type="checkbox" id="enabled">
<span class="slider"></span>
</label>
<span id="enabledLabel" class="pill off">…</span>
</div>
</header>
<div class="wrap">
<div class="row">
<div class="card stat"><div class="n" id="s-inq">—</div><div class="l">Inquiries seen</div></div>
<div class="card stat"><div class="n" id="s-rep">—</div><div class="l">Auto-replies sent</div></div>
<div class="card stat"><div class="n" id="s-skip">—</div><div class="l">Spam / skipped</div></div>
<div class="card stat"><div class="n" id="s-snd">—</div><div class="l">Unique senders (ledger)</div></div>
</div>
<section>
<h2>Auto-replies sent</h2>
<div class="card scroll">
<table><thead><tr><th>Time</th><th>Recipient</th><th>Domain</th><th>Subject</th></tr></thead>
<tbody id="replies"></tbody></table>
</div>
</section>
<section>
<h2>Spam-skips & reasons</h2>
<div class="card scroll">
<table><thead><tr><th>Time</th><th>Sender</th><th>Domain</th><th>Skip reason</th></tr></thead>
<tbody id="skips"></tbody></table>
</div>
</section>
<section>
<h2>Inquiries per fleet domain</h2>
<div class="card scroll">
<table><thead><tr><th>Domain</th><th>Site</th><th>Received</th><th>Replied</th><th>Skipped</th></tr></thead>
<tbody id="perdomain"></tbody></table>
</div>
</section>
<section>
<h2>Current reply template (HTML preview)
<select id="tplDomain"></select></h2>
<div class="preview"><iframe id="tplFrame" title="reply preview"></iframe></div>
</section>
</div>
<script>
const $ = id => document.getElementById(id);
function esc(s){ return String(s==null?'':s).replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c])); }
function fmt(ts){ if(!ts) return '—'; const d=new Date(ts); return d.toLocaleString(); }
async function load() {
const d = await fetch('/api/data').then(r=>r.json());
$('fleetcount').textContent = d.fleetCount + ' domains';
$('lastrun').textContent = 'last run: ' + fmt(d.state.lastRun) +
(d.state.lastRunResult ? ' · replied ' + (d.state.lastRunResult.replied||0) +
' / skipped ' + (d.state.lastRunResult.skipped||0) : '');
$('s-inq').textContent = d.totals.inquiries;
$('s-rep').textContent = d.totals.replied;
$('s-skip').textContent = d.totals.skipped;
$('s-snd').textContent = d.totals.uniqueSenders;
const en = d.state.enabled !== false;
$('enabled').checked = en;
$('enabledLabel').textContent = en ? 'ON' : 'OFF';
$('enabledLabel').className = 'pill ' + (en ? 'ok' : 'off');
$('replies').innerHTML = d.replies.length ? d.replies.map(r =>
`<tr><td class="mono">${fmt(r.ts)}</td><td>${esc(r.recipient)}</td>
<td>${esc(r.fleetDomain)}</td><td>${esc(r.subject)}${r.dryRun?' <span class="pill skip">dry-run</span>':''}</td></tr>`
).join('') : '<tr><td colspan="4" class="empty">No replies logged yet.</td></tr>';
$('skips').innerHTML = d.skips.length ? d.skips.map(r =>
`<tr><td class="mono">${fmt(r.ts)}</td><td>${esc(r.sender)}</td>
<td>${esc(r.fleetDomain||'—')}</td><td><span class="pill skip">skip</span> ${esc(r.reason)}</td></tr>`
).join('') : '<tr><td colspan="4" class="empty">No skips logged yet.</td></tr>';
const pd = d.perDomain.filter(x=>x.received>0);
$('perdomain').innerHTML = pd.length ? pd.map(x =>
`<tr><td class="mono">${esc(x.domain)}</td><td>${esc(x.siteName)}</td>
<td>${x.received}</td><td>${x.replied}</td><td>${x.skipped}</td></tr>`
).join('') : '<tr><td colspan="5" class="empty">No fleet inquiries seen yet.</td></tr>';
if (!$('tplDomain').dataset.filled) {
$('tplDomain').innerHTML = d.perDomain.map(x =>
`<option value="${esc(x.domain)}">${esc(x.domain)}</option>`).join('');
$('tplDomain').dataset.filled = '1';
loadTemplate();
}
}
async function loadTemplate() {
const dom = $('tplDomain').value;
const t = await fetch('/api/template?domain=' + encodeURIComponent(dom)).then(r=>r.json());
$('tplFrame').srcdoc = t.html;
}
$('enabled').addEventListener('change', async e => {
await fetch('/api/toggle', { method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ enabled: e.target.checked }) });
load();
});
$('tplDomain').addEventListener('change', loadTemplate);
load();
setInterval(load, 15000);
</script>
</body>
</html>