[object Object]

← back to Ads Dashboard

ads-dashboard v1: host-aware Node ad-ops dashboard (DW + Abrams), Basic-Auth, zero-dep

3ec677ec208fcdc9316a1f8e1e28d1f903ab26ef · 2026-08-02 06:47:44 -0700 · Steve Abrams

Files touched

Diff

commit 3ec677ec208fcdc9316a1f8e1e28d1f903ab26ef
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Aug 2 06:47:44 2026 -0700

    ads-dashboard v1: host-aware Node ad-ops dashboard (DW + Abrams), Basic-Auth, zero-dep
---
 .gitignore          |   8 +++
 data/campaigns.json |   1 +
 ecosystem.config.js |  10 +++
 package.json        |   9 +++
 public/index.html   | 143 ++++++++++++++++++++++++++++++++++++++++++
 server.js           | 175 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 346 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1924158
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/data/campaigns.json b/data/campaigns.json
new file mode 100644
index 0000000..4ac4fa0
--- /dev/null
+++ b/data/campaigns.json
@@ -0,0 +1 @@
+{ "campaigns": [] }
diff --git a/ecosystem.config.js b/ecosystem.config.js
new file mode 100644
index 0000000..66661d9
--- /dev/null
+++ b/ecosystem.config.js
@@ -0,0 +1,10 @@
+module.exports = {
+  apps: [{
+    name: 'ads-dashboard',
+    script: 'server.js',
+    cwd: __dirname,
+    env: { PORT: 9764, ADS_USER: 'admin', ADS_PASS: 'DW2024!' },
+    max_restarts: 10,
+    autorestart: true,
+  }],
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..61fb912
--- /dev/null
+++ b/package.json
@@ -0,0 +1,9 @@
+{
+  "name": "ads-dashboard",
+  "version": "0.1.0",
+  "private": true,
+  "description": "Host-aware internal ad-ops command center for ads.designerwallcoverings.com + ads.agentabrams.com",
+  "main": "server.js",
+  "scripts": { "start": "node server.js" },
+  "engines": { "node": ">=18" }
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..9e1a835
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,143 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Ad-Ops</title>
+<style>
+  :root { --accent:#555; --bg:#0f1115; --card:#171a21; --line:#262b36; --fg:#e7eaf0; --mut:#8b94a7; }
+  @media (prefers-color-scheme: light){ :root{ --bg:#f5f6f8; --card:#fff; --line:#e4e7ec; --fg:#1a1d24; --mut:#68707f; } }
+  *{ box-sizing:border-box; }
+  body{ margin:0; font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; background:var(--bg); color:var(--fg); }
+  header{ display:flex; align-items:center; gap:14px; padding:16px 22px; border-bottom:1px solid var(--line); position:sticky; top:0; background:var(--bg); z-index:5; }
+  header .dot{ width:12px; height:12px; border-radius:50%; background:var(--accent); flex:0 0 auto; }
+  header h1{ font-size:17px; margin:0; font-weight:650; letter-spacing:-.2px; }
+  header .sub{ color:var(--mut); font-size:13px; }
+  header .spacer{ flex:1; }
+  header a.host{ color:var(--mut); font-size:12px; text-decoration:none; }
+  main{ padding:22px; max-width:1200px; margin:0 auto; }
+  .grid{ display:grid; gap:16px; grid-template-columns:repeat(auto-fill,minmax(var(--min,240px),1fr)); }
+  .card{ background:var(--card); border:1px solid var(--line); border-radius:12px; padding:16px 18px; }
+  .card h2{ font-size:12px; text-transform:uppercase; letter-spacing:.6px; color:var(--mut); margin:0 0 10px; }
+  .kpi{ font-size:30px; font-weight:680; letter-spacing:-.5px; }
+  .kpi a{ color:inherit; text-decoration:none; border-bottom:2px solid transparent; }
+  .kpi a:hover{ border-color:var(--accent); }
+  .when{ display:inline-block; margin-top:8px; font-size:11px; color:var(--mut); }
+  .controls{ display:flex; gap:12px; align-items:center; margin:22px 0 12px; flex-wrap:wrap; }
+  .controls label{ font-size:12px; color:var(--mut); }
+  select,input[type=range]{ accent-color:var(--accent); }
+  select{ background:var(--card); color:var(--fg); border:1px solid var(--line); border-radius:8px; padding:5px 8px; }
+  table{ width:100%; border-collapse:collapse; font-size:13px; }
+  th,td{ text-align:left; padding:8px 10px; border-bottom:1px solid var(--line); }
+  th{ color:var(--mut); font-weight:600; cursor:pointer; user-select:none; }
+  td a{ color:var(--fg); text-decoration:none; border-bottom:1px dotted var(--mut); }
+  td a:hover{ border-color:var(--accent); }
+  .badge{ display:inline-block; padding:2px 8px; border-radius:99px; font-size:11px; border:1px solid var(--line); }
+  .empty{ color:var(--mut); font-size:13px; padding:16px 0; }
+  .section-h{ font-size:13px; text-transform:uppercase; letter-spacing:.6px; color:var(--mut); margin:28px 0 6px; }
+</style>
+</head>
+<body>
+<header>
+  <span class="dot" id="dot"></span>
+  <div>
+    <h1 id="brandName">Ad-Ops</h1>
+    <div class="sub" id="brandSub">loading…</div>
+  </div>
+  <span class="spacer"></span>
+  <a class="host" id="hostChip" href="/healthz">/healthz</a>
+</header>
+<main>
+  <div class="grid" id="kpis"></div>
+
+  <div class="section-h">Campaigns</div>
+  <div class="controls">
+    <label>Sort <select id="sort">
+      <option value="newest">Newest</option>
+      <option value="name">Name A→Z</option>
+      <option value="status">Status</option>
+      <option value="spend">Spend ↓</option>
+    </select></label>
+    <label>Density <input type="range" id="density" min="220" max="420" value="240"></label>
+  </div>
+  <div class="card">
+    <table id="campTable"><thead><tr>
+      <th data-k="name">Campaign</th><th data-k="domain">Domain</th>
+      <th data-k="status">Status</th><th data-k="spend">Spend</th><th data-k="created_at">Created</th>
+    </tr></thead><tbody id="campBody"></tbody></table>
+    <div class="empty" id="campEmpty" hidden>No campaigns wired yet — /yoloforever will populate this. Data source: /api/campaigns</div>
+  </div>
+
+  <div class="section-h">Competitor Ad Signals</div>
+  <div class="card">
+    <div id="signals"></div>
+  </div>
+</main>
+
+<script>
+const fmtDate = (iso) => { try { return new Date(iso).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); } catch { return iso||'—'; } };
+const $ = (id)=>document.getElementById(id);
+
+let campaigns = [];
+async function boot(){
+  const brand = await fetch('/api/brand').then(r=>r.json());
+  document.documentElement.style.setProperty('--accent', brand.accent || '#555');
+  $('brandName').textContent = brand.name + ' — Ad-Ops';
+  $('brandSub').textContent = (brand.domains||[]).join(' · ') || 'internal ad-operations command center';
+  $('hostChip').textContent = location.host;
+  document.title = brand.short || 'Ad-Ops';
+
+  const [ov, spend, camps] = await Promise.all([
+    fetch('/api/overview').then(r=>r.json()),
+    fetch('/api/spend').then(r=>r.json()),
+    fetch('/api/campaigns').then(r=>r.json()),
+  ]);
+
+  // KPIs — every number hrefs to its deeper source
+  const kpis = [
+    { h:'Active Campaigns', v:ov.counts.active, href:'/api/campaigns' },
+    { h:'Total Campaigns', v:ov.counts.campaigns, href:'/api/campaigns' },
+    { h:'Domains Covered', v:ov.counts.domains, href:'/api/brand' },
+    { h:'Spend (7d)', v: spend.present ? ('$'+spend.total_usd) : '$0', href:'/api/spend', when: spend.present?'from cost-ledger':'ledger not present' },
+  ];
+  $('kpis').innerHTML = kpis.map(k=>`<div class="card"><h2>${k.h}</h2>
+     <div class="kpi"><a href="${k.href}">${k.v}</a></div>
+     <span class="when" title="${new Date().toISOString()}">🕓 ${fmtDate(new Date().toISOString())}${k.when?' · '+k.when:''}</span></div>`).join('');
+
+  campaigns = camps.campaigns || [];
+  renderCampaigns();
+
+  // signals
+  const sig = await fetch('/api/signals').then(r=>r.json());
+  const items = sig.signals || [];
+  $('signals').innerHTML = items.length
+    ? items.map(s=>`<div><a href="${s.url||'#'}">${s.advertiser||s.title||'signal'}</a> <span class="badge">${s.platform||'—'}</span> <span class="when">🕓 ${fmtDate(s.seen_at||s.created_at)}</span></div>`).join('')
+    : `<div class="empty">No competitor ad signals captured yet — feeds from the advertising-signals skill. Source: /api/signals</div>`;
+}
+
+let sortKey='newest';
+function renderCampaigns(){
+  const b=$('campBody'), sorters={
+    newest:(a,c)=> (Date.parse(c.created_at||0))-(Date.parse(a.created_at||0)),
+    name:(a,c)=> (a.name||'').localeCompare(c.name||''),
+    status:(a,c)=> (a.status||'').localeCompare(c.status||''),
+    spend:(a,c)=> (c.spend||0)-(a.spend||0),
+  };
+  const rows=[...campaigns].sort(sorters[sortKey]||sorters.newest);
+  $('campEmpty').hidden = rows.length>0;
+  b.innerHTML = rows.map(c=>`<tr>
+    <td><a href="/api/campaigns">${c.name||'—'}</a></td>
+    <td><a href="https://${c.domain||''}">${c.domain||'—'}</a></td>
+    <td><span class="badge">${c.status||'—'}</span></td>
+    <td>${c.spend!=null?('$'+c.spend):'—'}</td>
+    <td><span class="when" title="${c.created_at||''}">🕓 ${fmtDate(c.created_at)}</span></td>
+  </tr>`).join('');
+}
+$('sort').onchange = e=>{ sortKey=e.target.value; renderCampaigns(); };
+$('density').oninput = e=>document.documentElement.style.setProperty('--min', e.target.value+'px');
+document.querySelectorAll('#campTable th').forEach(th=>th.onclick=()=>{ const m={name:'name',status:'status',spend:'spend',created_at:'newest'}; sortKey=m[th.dataset.k]||'newest'; $('sort').value=sortKey; renderCampaigns(); });
+
+boot().catch(e=>{ $('brandSub').textContent='error: '+e.message; });
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..f0eaad5
--- /dev/null
+++ b/server.js
@@ -0,0 +1,175 @@
+#!/usr/bin/env node
+/**
+ * ads-dashboard — host-aware internal ad-ops command center.
+ *
+ * One app serves two live hosts (barber.aa PUBLIC_HOSTS pattern):
+ *   ads.designerwallcoverings.com  -> DW ad-ops
+ *   ads.agentabrams.com            -> Abrams-portfolio ad-ops
+ *
+ * Zero external deps (node:http/fs/path/crypto only) so deploy = copy + `pm2 start`.
+ * Basic-Auth gates everything except /healthz; the 401 IS the healthy gate.
+ */
+'use strict';
+
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+
+const PORT = parseInt(process.env.PORT || '9764', 10);
+const AUTH_USER = process.env.ADS_USER || 'admin';
+const AUTH_PASS = process.env.ADS_PASS || 'DW2024!';
+const PUBLIC_DIR = path.join(__dirname, 'public');
+const DATA_DIR = path.join(__dirname, 'data');
+
+// ── Host → brand map ────────────────────────────────────────────────────────
+// Add a hostname here (and it just works) — this is the PUBLIC_HOSTS registry.
+const BRANDS = {
+  'ads.designerwallcoverings.com': {
+    key: 'dw',
+    name: 'Designer Wallcoverings',
+    short: 'DW Ad-Ops',
+    accent: '#8b7355',
+    domains: ['designerwallcoverings.com', 'apartmentwallpaper.com', 'philipperomano.com'],
+  },
+  'ads.agentabrams.com': {
+    key: 'abrams',
+    name: 'Agent Abrams',
+    short: 'Abrams Ad-Ops',
+    accent: '#2c5f8a',
+    domains: ['agentabrams.com', 'rentv.agentabrams.com', 'lawyers.agentabrams.com'],
+  },
+};
+const DEFAULT_BRAND = {
+  key: 'default', name: 'Ad-Ops', short: 'Ad-Ops', accent: '#555', domains: [],
+};
+
+function brandForHost(hostHeader) {
+  const host = String(hostHeader || '').toLowerCase().split(':')[0];
+  return BRANDS[host] || DEFAULT_BRAND;
+}
+
+// ── helpers ─────────────────────────────────────────────────────────────────
+function readJSON(file, fallback) {
+  try { return JSON.parse(fs.readFileSync(path.join(DATA_DIR, file), 'utf8')); }
+  catch { return fallback; }
+}
+
+function timingSafeEqual(a, b) {
+  const ab = Buffer.from(a), bb = Buffer.from(b);
+  if (ab.length !== bb.length) return false;
+  return crypto.timingSafeEqual(ab, bb);
+}
+
+function checkAuth(req) {
+  const h = req.headers['authorization'] || '';
+  if (!h.startsWith('Basic ')) return false;
+  const [u, p] = Buffer.from(h.slice(6), 'base64').toString('utf8').split(':');
+  return timingSafeEqual(u || '', AUTH_USER) && timingSafeEqual(p || '', AUTH_PASS);
+}
+
+function send(res, code, body, headers = {}) {
+  res.writeHead(code, { 'Cache-Control': 'no-store', ...headers });
+  res.end(body);
+}
+function sendJSON(res, code, obj) {
+  send(res, code, JSON.stringify(obj), { 'Content-Type': 'application/json' });
+}
+
+const MIME = {
+  '.html': 'text/html; charset=utf-8', '.css': 'text/css', '.js': 'text/javascript',
+  '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png',
+  '.ico': 'image/x-icon', '.woff2': 'font/woff2',
+};
+
+// ── data endpoints (real sources where present, honest-empty otherwise) ───────
+// Spend: read the global cost-ledger if it exists; summarize last 7d by provider.
+function spendSummary() {
+  const ledgerPath = path.join(process.env.HOME || '/root', '.claude', 'cost-ledger.jsonl');
+  const out = { source: 'cost-ledger.jsonl', window_days: 7, total_usd: 0, by_provider: {}, present: false };
+  try {
+    const lines = fs.readFileSync(ledgerPath, 'utf8').trim().split('\n');
+    out.present = true;
+    const cutoff = fixedNowMs() - 7 * 864e5;
+    for (const ln of lines) {
+      let e; try { e = JSON.parse(ln); } catch { continue; }
+      const t = Date.parse(e.ts || e.time || e.date || 0);
+      if (t && t < cutoff) continue;
+      const usd = Number(e.usd || e.cost || e.amount || 0);
+      const prov = e.provider || e.service || 'unknown';
+      out.total_usd += usd;
+      out.by_provider[prov] = (out.by_provider[prov] || 0) + usd;
+    }
+    out.total_usd = Math.round(out.total_usd * 100) / 100;
+  } catch { /* honest-empty: present stays false */ }
+  return out;
+}
+
+// fixedNow: server boot time baseline (Date is fine at runtime on the server;
+// the workflow-script restriction does not apply to a running node process).
+function fixedNowMs() { return Date.now(); }
+
+function apiOverview(brand) {
+  const campaigns = readJSON(`campaigns.${brand.key}.json`, null)
+    || readJSON('campaigns.json', { campaigns: [] });
+  const list = Array.isArray(campaigns) ? campaigns : (campaigns.campaigns || []);
+  const active = list.filter(c => (c.status || '').toLowerCase() === 'active');
+  return {
+    brand: brand.key,
+    generated_at: new Date().toISOString(),
+    counts: { campaigns: list.length, active: active.length, domains: brand.domains.length },
+    domains: brand.domains,
+  };
+}
+
+// ── request handler ───────────────────────────────────────────────────────────
+const server = http.createServer((req, res) => {
+  const brand = brandForHost(req.headers.host);
+  const url = new URL(req.url, 'http://x');
+  const pathname = url.pathname;
+
+  // open health check (no auth) — the fleet probe reads this
+  if (pathname === '/healthz') {
+    return sendJSON(res, 200, {
+      ok: true, service: 'ads-dashboard', brand: brand.key,
+      host: (req.headers.host || '').split(':')[0], ts: new Date().toISOString(),
+    });
+  }
+
+  // everything else is gated
+  if (!checkAuth(req)) {
+    return send(res, 401, 'Authentication required', {
+      'WWW-Authenticate': 'Basic realm="Ad-Ops"',
+      'Content-Type': 'text/plain',
+    });
+  }
+
+  // API
+  if (pathname === '/api/overview') return sendJSON(res, 200, apiOverview(brand));
+  if (pathname === '/api/spend') return sendJSON(res, 200, spendSummary());
+  if (pathname === '/api/signals') {
+    return sendJSON(res, 200, readJSON(`signals.${brand.key}.json`, { source: 'advertising-signals', signals: [], present: false }));
+  }
+  if (pathname === '/api/campaigns') {
+    const c = readJSON(`campaigns.${brand.key}.json`, null) || readJSON('campaigns.json', { campaigns: [] });
+    return sendJSON(res, 200, Array.isArray(c) ? { campaigns: c } : c);
+  }
+  if (pathname === '/api/brand') return sendJSON(res, 200, brand);
+
+  // static
+  let rel = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
+  const file = path.join(PUBLIC_DIR, rel);
+  if (!file.startsWith(PUBLIC_DIR)) return send(res, 403, 'forbidden');
+  fs.readFile(file, (err, buf) => {
+    if (err) {
+      // SPA fallback
+      return fs.readFile(path.join(PUBLIC_DIR, 'index.html'), (e2, idx) =>
+        e2 ? send(res, 404, 'not found') : send(res, 200, idx, { 'Content-Type': MIME['.html'] }));
+    }
+    send(res, 200, buf, { 'Content-Type': MIME[path.extname(file)] || 'application/octet-stream' });
+  });
+});
+
+server.listen(PORT, () => {
+  console.log(`[ads-dashboard] listening on :${PORT}  hosts=${Object.keys(BRANDS).join(', ')}`);
+});

(oldest)  ·  back to Ads Dashboard  ·  set prod port to 9766 (9764 taken on Kamatera) 429c5bd →