← back to Agent Ad Network
display-admin.html
177 lines
<!DOCTYPE html>
<html lang="en"><head>
<script>
// Credential-safe fetch guard (fleet drop-in) — if this page is opened with
// credentials in the URL (a saved bookmark / Chrome-remembered basic-auth:
// https://user:pass@host/…), the browser poisons document.baseURI, so any
// relative or root-relative fetch('/api/…') throws "Request cannot be constructed
// from a URL that includes credentials" and callers silently fall to an empty
// state. Resolve every non-absolute request URL against the credential-free
// location instead of baseURI. Placed first so it wraps window.fetch before any
// app script runs. Ref: creds-in-url-fetch-guard-fleet-pattern.
(function () {
var _fetch = window.fetch.bind(window);
var cleanBase = function () { return location.origin + location.pathname; };
window.fetch = function (input, init) {
try {
if (typeof input === 'string' && !/^[a-z]+:\/\//i.test(input) && input.indexOf('//') !== 0) {
input = new URL(input, cleanBase()).href;
} else if (input instanceof Request && !/^[a-z]+:\/\//i.test(input.url)) {
input = new Request(new URL(input.url, cleanBase()).href, input);
}
} catch (_) { /* fall through to native */ }
return _fetch(input, init);
};
})();
</script>
<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>