← back to Agent Ad Network
Add DW Display Network admin UI (/admin/display): KPIs, live slot preview, embed generator, promo/site/paid tables + /admin/api/display aggregator + site/slot event logging
ae1dde9e14b055a3130822df8d07bb5a29c961b4 · 2026-08-04 11:54:52 -0700 · Steve Abrams
Files touched
A display-admin.htmlM server.js
Diff
commit ae1dde9e14b055a3130822df8d07bb5a29c961b4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Aug 4 11:54:52 2026 -0700
Add DW Display Network admin UI (/admin/display): KPIs, live slot preview, embed generator, promo/site/paid tables + /admin/api/display aggregator + site/slot event logging
---
display-admin.html | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++++
server.js | 49 +++++++++++++++--
2 files changed, 197 insertions(+), 4 deletions(-)
diff --git a/display-admin.html b/display-admin.html
new file mode 100644
index 0000000..d8d9448
--- /dev/null
+++ b/display-admin.html
@@ -0,0 +1,152 @@
+<!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>DW Display Network — Ad Admin</title>
+<style>
+ :root{--bg:#101216;--card:#181b21;--accent:#4fd1c5;--clk:#f472b6;--usr:#a78bfa;--line:#2a2f38;--fg:#e6e6e6;--dim:#8a93a3}
+ *{box-sizing:border-box}
+ body{margin:0;background:radial-gradient(ellipse at 50% -10%,#171c26 0%,#0b0d12 60%);color:var(--fg);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
+ header{display:flex;align-items:center;gap:16px;padding:18px 24px;border-bottom:1px solid var(--line)}
+ header h1{font-size:18px;margin:0;font-weight:700;letter-spacing:.02em}
+ header .pill{font-size:11px;color:var(--accent);border:1px solid var(--accent);border-radius:999px;padding:2px 10px;letter-spacing:.06em;text-transform:uppercase}
+ header nav{margin-left:auto;display:flex;gap:14px;font-size:13px}
+ header nav a{color:var(--dim);text-decoration:none}header nav a:hover{color:var(--fg)}
+ .wrap{max-width:1160px;margin:0 auto;padding:24px}
+ .kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:14px}
+ .kpi{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px}
+ .kpi .n{font-size:26px;font-weight:700;letter-spacing:-.01em}
+ .kpi .l{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.06em;margin-top:4px}
+ .kpi.acc .n{color:var(--accent)}.kpi.clk .n{color:var(--clk)}
+ .grid2{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin-top:22px}
+ @media(max-width:820px){.grid2{grid-template-columns:1fr}}
+ .panel{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:18px}
+ .panel h2{font-size:13px;text-transform:uppercase;letter-spacing:.06em;color:var(--dim);margin:0 0 14px}
+ table{width:100%;border-collapse:collapse;font-size:13px}
+ th{text-align:left;color:var(--dim);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.04em;padding:6px 8px;border-bottom:1px solid var(--line)}
+ td{padding:8px;border-bottom:1px solid #20242c}
+ td a{color:var(--accent);text-decoration:none}
+ .bar{height:6px;background:#20242c;border-radius:4px;overflow:hidden}.bar>i{display:block;height:100%;background:var(--accent)}
+ /* live preview */
+ .preview{position:relative;min-height:70px;border:1px dashed var(--line);border-radius:8px;background:#0d0f13;padding:10px;margin-top:10px}
+ .slot-tabs{display:flex;gap:8px;margin-bottom:6px}
+ .slot-tabs button{background:#20242c;border:1px solid var(--line);color:var(--fg);border-radius:8px;padding:5px 10px;font-size:12px;cursor:pointer}
+ .slot-tabs button.on{background:var(--accent);color:#08130f;border-color:var(--accent);font-weight:600}
+ /* embed generator */
+ .gen{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:10px}
+ .gen select,.gen input{background:#0d0f13;border:1px solid var(--line);color:var(--fg);border-radius:8px;padding:7px 10px;font-size:13px}
+ pre{background:#0d0f13;border:1px solid var(--line);border-radius:8px;padding:12px;overflow:auto;font-size:12px;color:#cfe;margin:0}
+ .copy{background:var(--accent);color:#08130f;border:0;border-radius:8px;padding:7px 14px;font-weight:600;cursor:pointer;font-size:13px;margin-top:8px}
+ .foot{color:var(--dim);font-size:12px;margin-top:24px;text-align:center}
+</style></head><body>
+<header>
+ <h1>DW Display Network</h1><span class="pill">Ad Admin</span>
+ <nav>
+ <a href="/admin">◧ Agent-wait dashboard</a>
+ <a href="#" onclick="load();return false">↻ Refresh</a>
+ <span id="live" style="color:var(--accent)">● live</span>
+ </nav>
+</header>
+<div class="wrap">
+ <div class="kpis" id="kpis"></div>
+
+ <div class="grid2">
+ <div class="panel">
+ <h2>Live ad-slot preview</h2>
+ <div class="slot-tabs" id="slotTabs">
+ <button data-s="footer-strip" class="on">Footer strip</button>
+ <button data-s="in-grid-card">In-grid card</button>
+ <button data-s="sidebar-rail">Sidebar rail</button>
+ </div>
+ <div class="preview" id="preview"><span style="color:var(--dim)">loading a live fill…</span></div>
+ <div style="color:var(--dim);font-size:12px;margin-top:8px">Renders a real fill from <code>/serve/display</code> — paid ad → DW promo → external. Every ad link is <code>rel="nofollow sponsored"</code>.</div>
+ </div>
+
+ <div class="panel">
+ <h2>Embed generator</h2>
+ <div class="gen">
+ <select id="genSlot"><option>footer-strip</option><option>in-grid-card</option><option>sidebar-rail</option></select>
+ <input id="genSite" placeholder="site domain (e.g. silkwallpaper.com)" style="flex:1;min-width:180px">
+ <input id="genCtx" placeholder="context keywords (silk,wallcovering)" style="flex:1;min-width:160px">
+ </div>
+ <pre id="genOut"></pre>
+ <button class="copy" onclick="copyEmbed()">Copy embed snippet</button>
+ <div style="color:var(--dim);font-size:12px;margin-top:8px">Drop one line into any site's page. In-grid-card renders where the tag sits; strip/rail are fixed.</div>
+ </div>
+ </div>
+
+ <div class="grid2">
+ <div class="panel">
+ <h2>DW cross-promos</h2>
+ <table><thead><tr><th>Promo</th><th>Wt</th><th>Impr</th><th>Clicks</th><th>CTR</th></tr></thead><tbody id="promos"></tbody></table>
+ </div>
+ <div class="panel">
+ <h2>Top sites (by impressions)</h2>
+ <table><thead><tr><th>Site</th><th>Impressions</th><th></th></tr></thead><tbody id="sites"></tbody></table>
+ </div>
+ </div>
+
+ <div class="panel" style="margin-top:18px">
+ <h2>Paid display ads (active)</h2>
+ <table><thead><tr><th>Headline</th><th>Sponsor</th><th>Impr</th><th>Clicks</th><th>Spent</th><th>Budget</th></tr></thead><tbody id="paid"></tbody></table>
+ </div>
+
+ <div class="foot">Fill priority: paid ad → DW promo → external. Data from the append-only event log. Basic-auth gated · noindex.</div>
+</div>
+<script>
+const ORIGIN = location.origin;
+const $ = id => document.getElementById(id);
+const esc = s => String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
+const fmt = n => (n||0).toLocaleString();
+
+async function load(){
+ let d; try{ d = await (await fetch('/admin/api/display')).json(); }catch(e){ return; }
+ const t = d.totals;
+ $('kpis').innerHTML = [
+ ['acc', fmt(t.impressions), 'Impressions'],
+ ['', t.fillRate+'%', 'Fill rate'],
+ ['clk', fmt(t.clicks), 'Clicks'],
+ ['', t.ctr+'%', 'CTR'],
+ ['', fmt(t.activeSites), 'Active sites'],
+ ['acc', fmt(t.paidImpr)+' / '+fmt(t.promoImpr), 'Paid / Promo'],
+ ].map(([c,n,l])=>`<div class="kpi ${c}"><div class="n">${n}</div><div class="l">${l}</div></div>`).join('');
+
+ const maxP = Math.max(1, ...d.promos.map(p=>p.impr));
+ $('promos').innerHTML = d.promos.map(p=>`<tr><td><a href="${esc(p.target)}" target="_blank" rel="nofollow noopener">${esc(p.headline)}</a></td><td>${p.weight}</td><td>${fmt(p.impr)}</td><td>${fmt(p.clicks)}</td><td>${p.impr?((p.clicks/p.impr*100).toFixed(1)+'%'):'—'}</td></tr>`).join('') || '<tr><td colspan=5 style="color:var(--dim)">no promo data yet</td></tr>';
+
+ const maxS = Math.max(1, ...d.sites.map(s=>s.impr));
+ $('sites').innerHTML = d.sites.map(s=>`<tr><td>${esc(s.site)}</td><td>${fmt(s.impr)}</td><td style="width:120px"><div class="bar"><i style="width:${(s.impr/maxS*100).toFixed(0)}%"></i></div></td></tr>`).join('') || '<tr><td colspan=3 style="color:var(--dim)">no site data yet</td></tr>';
+
+ $('paid').innerHTML = d.paidAds.map(a=>`<tr><td>${esc(a.headline)}</td><td>${esc(a.sponsor||'—')}</td><td>${fmt(a.impressions)}</td><td>${fmt(a.clicks)}</td><td>$${a.spent}</td><td>$${a.budget}</td></tr>`).join('') || '<tr><td colspan=6 style="color:var(--dim)">no paid display ads yet — DW promos are filling</td></tr>';
+}
+
+// live preview: fetch a real fill and render it
+let curSlot = 'footer-strip';
+async function renderPreview(){
+ const el = $('preview');
+ try{
+ const d = await (await fetch(`/serve/display?slot=${curSlot}&site=preview.admin&context=wallcovering`)).json();
+ const ad = d.ad;
+ if(!ad){ el.innerHTML='<span style="color:var(--dim)">no fill (empty network)</span>'; return; }
+ el.innerHTML = `<span style="font-size:9px;letter-spacing:.08em;text-transform:uppercase;color:var(--dim);margin-right:8px">Sponsored</span>
+ <a href="${esc(ad.clickUrl)}" target="_blank" rel="nofollow sponsored noopener" style="color:var(--accent);text-decoration:none"><b>${esc(ad.headline)}</b> ${esc(ad.body)}</a>
+ <span style="font-size:11px;color:var(--dim);margin-left:8px">${esc(ad.sponsor||'')}</span>`;
+ }catch(e){ el.innerHTML='<span style="color:var(--dim)">preview unavailable</span>'; }
+}
+$('slotTabs').addEventListener('click', e=>{ const b=e.target.closest('button'); if(!b)return;
+ [...$('slotTabs').children].forEach(x=>x.classList.remove('on')); b.classList.add('on'); curSlot=b.dataset.s; renderPreview(); });
+
+function buildEmbed(){
+ const slot=$('genSlot').value, site=$('genSite').value.trim(), ctx=$('genCtx').value.trim();
+ let s=`<script src="${ORIGIN}/embed.js" data-slot="${slot}"`;
+ if(site) s+=` data-site="${site}"`;
+ if(ctx) s+=` data-context="${ctx}"`;
+ s+=`></`+`script>`;
+ $('genOut').textContent=s; return s;
+}
+function copyEmbed(){ navigator.clipboard.writeText(buildEmbed()); const b=event.target; const o=b.textContent; b.textContent='Copied ✓'; setTimeout(()=>b.textContent=o,1200); }
+['genSlot','genSite','genCtx'].forEach(id=>$(id).addEventListener('input',buildEmbed));
+
+load(); renderPreview(); buildEmbed();
+setInterval(load, 15000); setInterval(renderPreview, 8000);
+</script></body></html>
diff --git a/server.js b/server.js
index 2a8513e..402f6e2 100644
--- a/server.js
+++ b/server.js
@@ -221,21 +221,23 @@ function pickPromo() {
}
// external/affiliate fallback hook — returns null until Steve wires a real feed (gated).
function pickExternalFill() { return null; }
-function pickDisplayFill(context, req) {
+function pickDisplayFill(context, req, site, slot) {
+ const meta = { site: site || '', slot: slot || '' };
const paid = pickAd(context);
if (paid) {
paid.spent_usd = +(paid.spent_usd + paid.cpm_usd / 1000).toFixed(6);
paid.impressions = (paid.impressions || 0) + 1;
if (paid.spent_usd >= paid.budget_usd) paid.status = 'exhausted';
saveAds();
- logEvent({ type: 'impression', kind: 'display', adId: paid.id, advertiserId: paid.advertiserId, ip: clientIp(req) });
+ logEvent({ type: 'impression', kind: 'display', adId: paid.id, advertiserId: paid.advertiserId, ip: clientIp(req), ...meta });
return { headline: paid.headline, body: paid.body, sponsor: paid.sponsor, clickUrl: `${PUBLIC_URL}/c/${paid.id}` };
}
const promo = pickPromo();
if (promo) {
- logEvent({ type: 'impression', kind: 'promo', promoId: promo.id, ip: clientIp(req) });
+ logEvent({ type: 'impression', kind: 'promo', promoId: promo.id, ip: clientIp(req), ...meta });
return { headline: promo.headline, body: promo.body, sponsor: promo.sponsor, clickUrl: `${PUBLIC_URL}/cp/${promo.id}` };
}
+ logEvent({ type: 'unfilled', kind: 'display', ip: clientIp(req), ...meta });
return pickExternalFill();
}
@@ -279,7 +281,9 @@ const server = http.createServer(async (req, res) => {
if (req.method === 'GET' && p === '/serve/display') {
if (rateLimited(req, res, 'display', 240, 60000)) return;
const context = (u.searchParams.get('context') || '').toLowerCase().slice(0, 500).split(/[,\s]+/).filter(Boolean);
- const fill = pickDisplayFill(context, req);
+ const site = (u.searchParams.get('site') || '').slice(0, 120);
+ const slot = (u.searchParams.get('slot') || '').slice(0, 40);
+ const fill = pickDisplayFill(context, req, site, slot);
const H = { 'Access-Control-Allow-Origin': '*', ...SEC_HEADERS };
if (!fill) { res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', ...H }); return res.end(JSON.stringify({ ad: null })); }
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', ...H });
@@ -471,6 +475,43 @@ const server = http.createServer(async (req, res) => {
if (p === '/admin/api/overview') {
return json(res, 200, ads.map(a => ({ ...a, advertiser: (advertisers.find(v => v.id === a.advertiserId) || {}).name, ...adStats(a) })));
}
+ // ----- DISPLAY network dashboard (web ad slots on sister sites) -----
+ if (p === '/admin/api/display') {
+ const evs = readEventsTail(6 * 1024 * 1024);
+ const isImp = e => e.type === 'impression';
+ let paidImpr = 0, promoImpr = 0, unfilled = 0, promoClicks = 0, paidDisplayClicks = 0;
+ const promoStat = {}, siteStat = {}, slotStat = {};
+ DW_PROMOS.forEach(pr => promoStat[pr.id] = { id: pr.id, headline: pr.headline, target: pr.target, weight: pr.weight || 1, impr: 0, clicks: 0 });
+ const displayAdIds = new Set(ads.map(a => a.id)); // for paid-display click attribution
+ for (const e of evs) {
+ const site = e.site || '(unknown)', slot = e.slot || '(none)';
+ if (isImp(e) && e.kind === 'display') { paidImpr++; siteStat[site] = (siteStat[site]||0)+1; slotStat[slot]=(slotStat[slot]||0)+1; }
+ else if (isImp(e) && e.kind === 'promo') { promoImpr++; siteStat[site]=(siteStat[site]||0)+1; slotStat[slot]=(slotStat[slot]||0)+1; if (promoStat[e.promoId]) promoStat[e.promoId].impr++; }
+ else if (e.type === 'unfilled' && e.kind === 'display') { unfilled++; }
+ else if (e.type === 'click' && e.kind === 'promo') { promoClicks++; if (promoStat[e.promoId]) promoStat[e.promoId].clicks++; }
+ else if (e.type === 'click' && e.adId && displayAdIds.has(e.adId)) { paidDisplayClicks++; }
+ }
+ const totalImpr = paidImpr + promoImpr;
+ const totalClicks = promoClicks + paidDisplayClicks;
+ return json(res, 200, {
+ totals: {
+ impressions: totalImpr, paidImpr, promoImpr, unfilled,
+ fillRate: totalImpr + unfilled ? +(totalImpr / (totalImpr + unfilled) * 100).toFixed(1) : 0,
+ clicks: totalClicks, promoClicks, paidDisplayClicks,
+ ctr: totalImpr ? +(totalClicks / totalImpr * 100).toFixed(2) : 0,
+ activeSites: Object.keys(siteStat).filter(s => s !== '(unknown)').length,
+ promoPool: DW_PROMOS.length,
+ },
+ promos: Object.values(promoStat).sort((a,b) => b.impr - a.impr),
+ sites: Object.entries(siteStat).map(([site, impr]) => ({ site, impr })).sort((a,b) => b.impr - a.impr).slice(0, 40),
+ slots: Object.entries(slotStat).map(([slot, impr]) => ({ slot, impr })).sort((a,b) => b.impr - a.impr),
+ paidAds: ads.filter(a => a.approved && a.status === 'active').map(a => ({ id: a.id, headline: a.headline, sponsor: a.sponsor, impressions: a.impressions||0, clicks: a.clicks||0, spent: +(a.spent_usd||0).toFixed(2), budget: a.budget_usd })),
+ });
+ }
+ if (p === '/admin/display') {
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', ...SEC_HEADERS });
+ return res.end(fs.readFileSync(path.join(__dirname, 'display-admin.html')));
+ }
// live-viz feed: per-minute (60m) + per-hour (24h) buckets from the
// events tail — impressions, clicks, submissions, and unique users
// (distinct IPs; older events without ip fall back to advertiserId).
← d6af20e Add DW-network web display surface: /serve/display + /embed.
·
back to Agent Ad Network
·
add creds-in-URL fetch guard to gated pages (TK-10984) c065b88 →