← back to Whatsmystyle
public/admin-drifts.html
99 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex,nofollow" />
<title>WhatsMyStyle — Embedding-drift ledger</title>
<link rel="stylesheet" href="/css/app.css" />
<style>
body { font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; background: #faf7f2; color: #1d1d1f; max-width: 1100px; margin: 40px auto; padding: 0 24px; }
h1 { font-size: 32px; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 8px; }
.sub { color: #707070; margin-bottom: 32px; font-size: 14px; }
.card { background: #fff; border: 1px solid #e6e1d8; border-radius: 16px; padding: 18px 22px; margin: 14px 0; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { padding: 8px 10px; text-align: left; border-bottom: 1px solid #f0eadf; }
th { font-weight: 600; color: #707070; font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; }
.nav { margin-bottom: 24px; }
.nav a { color: #707070; text-decoration: none; font-size: 14px; }
.nav a:hover { color: #1d1d1f; }
.thumb { width: 36px; height: 44px; object-fit: cover; border-radius: 6px; border: 1px solid #e6e1d8; }
.sim { font-variant-numeric: tabular-nums; }
.sim-low { color: #b87a13; font-weight: 600; }
.sim-very-low { color: #b91c1c; font-weight: 700; }
.muted { color: #707070; }
.pill { display: inline-block; background: #faf7f2; border: 1px solid #e6e1d8; padding: 3px 10px; border-radius: 999px; font-size: 11px; color: #555; }
.empty { text-align: center; padding: 60px 20px; color: #707070; font-style: italic; }
h2 { font-size: 18px; font-weight: 600; margin: 0 0 14px; letter-spacing: -0.01em; }
</style>
</head>
<body>
<div class="nav"><a href="/admin-config">← admin config</a> · <a href="/">app home</a></div>
<h1>Embedding-drift ledger</h1>
<p class="sub">Every catalog item whose llava-projected vector deviated >20% cosine from its stored value. Populated by the 24h drift cron + manual drift sweeps.</p>
<div class="card">
<h2>Summary</h2>
<p><strong id="total">0</strong> drift events recorded · most-drifted item: <span id="top-item" class="muted">none yet</span></p>
</div>
<div class="card">
<h2>Most-drifted items (top 50 by drift count)</h2>
<table id="per-item-tbl">
<thead><tr><th></th><th>Item</th><th>Drift count</th><th>Min similarity</th><th>Last seen</th></tr></thead>
<tbody></tbody>
</table>
</div>
<div class="card">
<h2>Recent events (last 500)</h2>
<table id="recent-tbl">
<thead><tr><th></th><th>Item</th><th>Similarity</th><th>Checked at</th></tr></thead>
<tbody></tbody>
</table>
</div>
<script>
const $ = (s) => document.querySelector(s);
function escapeHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function simClass(s) {
if (s == null) return '';
if (s < 0.5) return 'sim-very-low';
if (s < 0.7) return 'sim-low';
return '';
}
async function load() {
const r = await fetch('/api/admin/drifts');
if (!r.ok) { document.body.innerHTML = '<p class="empty">admin gate failed</p>'; return; }
const j = await r.json();
$('#total').textContent = j.total;
if (j.total === 0) {
$('#per-item-tbl tbody').innerHTML = '<tr><td colspan="5" class="empty">No drift events yet — the 24h cron writes rows when similarity drops below 0.80.</td></tr>';
$('#recent-tbl tbody').innerHTML = '';
return;
}
$('#top-item').innerHTML = j.per_item.length ? `item #${j.per_item[0].item_id} (${j.per_item[0].c} drifts)` : 'none';
$('#per-item-tbl tbody').innerHTML = j.per_item.map(r => `
<tr>
<td></td>
<td><strong>#${r.item_id}</strong></td>
<td>${r.c}</td>
<td class="sim ${simClass(r.min_sim)}">${(r.min_sim ?? 0).toFixed(3)}</td>
<td class="muted">${escapeHtml(r.last_seen)}</td>
</tr>
`).join('');
$('#recent-tbl tbody').innerHTML = j.recent.map(r => `
<tr>
<td>${r.image_url ? `<img class="thumb" src="${escapeHtml(r.image_url)}" loading="lazy" alt="">` : ''}</td>
<td>${r.product_url ? `<a href="${escapeHtml(r.product_url)}" target="_blank" rel="noopener">${escapeHtml(r.title || ('item #' + r.item_id))}</a>` : escapeHtml(r.title || ('item #' + r.item_id))}<br><span class="pill">${escapeHtml(r.brand || r.source || '')}</span></td>
<td class="sim ${simClass(r.similarity)}">${(r.similarity ?? 0).toFixed(3)}</td>
<td class="muted">${escapeHtml(r.checked_at)}</td>
</tr>
`).join('');
}
load();
setInterval(load, 60 * 1000);
</script>
</body>
</html>