← 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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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>