← back to Adsense Fleet Viewer

build.mjs

250 lines

#!/usr/bin/env node
/* AdSense Fleet Status — live-probes every fleet domain and renders a status
 * dashboard to public/index.html. Re-run any time to refresh (probes are live).
 *
 * Status model (three honest layers — see the legend in the page):
 *   LOADER  — is adsbygoogle.js actually served on the live domain? (verifiable here)
 *   ADS.TXT — is /ads.txt present + authorizing a pub id? (verifiable here)
 *   SLOTS   — are any manual <ins> ad units active (non-empty __ADSLOTS)? (verifiable here)
 *   SERVING — real paid creative painting = account approved + Auto ads ON.
 *             NOT verifiable from here (AdSense dashboard, account-side).           */

import { writeFile, readFile } from 'node:fs/promises';

// Manual account status — the one thing probing CANNOT see (AdSense dashboard only).
// Steve edits account-status.json, then re-runs this build.
let ACCT = { pubId: 'pub-5278231299883833', approved: 'unknown', autoAdsOn: 'unknown', serving: 'unknown', checkedAt: '', notes: '' };
try { ACCT = { ...ACCT, ...JSON.parse(await readFile(new URL('./account-status.json', import.meta.url), 'utf8')) }; } catch {}

// LIST-DRIVEN: the domains probed come from sites.txt (one domain per line;
// '#' comments + blank lines ignored). Paste your AdSense Sites list in there and
// re-run. Known domains get policy/project metadata from POLICY_MAP below; any
// domain not in the map is probed as policy:'unknown' with a name derived from it.
//
// policy = the AdSense placement rule (DTD verdict 2026-08-05):
//   allow  — content/traffic surface, ads OK
//   forbid — customer-facing luxury/commerce surface, ads must NOT run
//   verify — thin/low-content, only after account check + real per-page content
//   na     — not a public ad surface
//   unknown— not yet classified (default for pasted domains)
const POLICY_MAP = {
  'nodailyworries.com':           { project: 'nodailyworries',           policy: 'allow',  flag: 'PARKED — app built+verified locally (:9871); DNS runbook prepped, awaiting NS swap (Steve-gated)' },
  'beverlyhillsvideos.com':       { project: 'beverlyhillsvideos',       policy: 'allow',  flag: 'NO DNS / not registered — points nowhere; register+build or drop from account' },
  'wholivedthere.com':            { project: 'WhoLivedThere',            policy: 'allow' },
  'claimmyaddress.com':           { project: 'ClaimMyAddress',           policy: 'allow' },
  'bubbe.ai':                     { project: 'bubbe.ai',                 policy: 'allow' },
  'bubbesblock.com':              { project: 'BubbesBlock',              policy: 'forbid', flag: 'Ads REMOVED on purpose (login-gated UGC = ban risk, commit d67ffa0) — drop from account, do NOT re-add' },
  'celebsignatures.com':          { project: 'CelebritySignatures',      policy: 'allow' },
  'chargeandexplore.com':         { project: 'tesla (Charge & Explore)', policy: 'verify', note: 'app landing — thin page' },
  'joshdultz.com':                { project: 'joshdultz',                policy: 'verify', flag: 'LIVE, not in account — needs loader+ads.txt before adding' },
  'allnewsdaily.com':             { project: 'allnewsdaily',            policy: 'allow',  flag: 'PARKED lander — app wired locally; needs DNS repoint (like nodailyworries)' },
  'fashionstyleguides.com':       { project: 'fashion-style-guides',    policy: 'allow',  flag: 'Live domain runs a DIFFERENT WordPress site — local app not deployed there; clarify ownership' },
  'venturacorridor.com':          { project: 'ventura-corridor',        policy: 'verify', flag: 'LIVE directory, not in account — thin pages, verify content before adding' },
  'interiordesignershowroom.com': { project: 'interiordesignershowroom', policy: 'forbid', flag: 'Loader REMOVED locally (commit e5b86aa) — deploy staged, Steve-gated' },
};

// AdSense dashboard snapshot (what Google's console reports — the truth probing can't see)
let ADSENSE = { account: '', pubId: '', snapshotAt: '', sites: {} };
try { ADSENSE = { ...ADSENSE, ...JSON.parse(await readFile(new URL('./adsense-sites.json', import.meta.url), 'utf8')) }; } catch {}

let domains = [];
try {
  const txt = await readFile(new URL('./sites.txt', import.meta.url), 'utf8');
  domains = [...new Set(txt.split('\n').map(l => l.trim().toLowerCase())
    .filter(l => l && !l.startsWith('#'))
    .map(l => l.replace(/^https?:\/\//, '').replace(/\/.*$/, '')))];
} catch { domains = Object.keys(POLICY_MAP); }

const FLEET = domains.map(domain => {
  const meta = POLICY_MAP[domain] || {};
  const project = meta.project || domain.replace(/\.(com|org|net|io|co)$/, '');
  return { domain, project, policy: meta.policy || 'unknown', note: meta.note, flag: meta.flag };
});

const timeout = (ms) => new AbortController(); // placeholder (see fetchT)
async function fetchT(url, ms = 9000) {
  const ac = new AbortController();
  const t = setTimeout(() => ac.abort(), ms);
  try { return await fetch(url, { signal: ac.signal, redirect: 'follow', headers: { 'user-agent': 'adsense-fleet-probe' } }); }
  finally { clearTimeout(t); }
}

async function probe(entry) {
  const r = { ...entry, http: null, loader: false, pubFromLoader: null, adsTxt: null, slots: [], err: null };
  if (!entry.domain) return r;
  const base = `https://${entry.domain}`;
  try {
    const res = await fetchT(base + '/');
    r.http = res.status;
    const html = await res.text();
    const m = html.match(/adsbygoogle\.js\?client=ca-(pub-\d+)/i);
    r.loader = !!m; r.pubFromLoader = m ? m[1] : null;
    const sm = html.match(/__ADSLOTS\s*=\s*(\{[^;]*\})/);
    if (sm) { try { const o = JSON.parse(sm[1]); r.slots = Object.entries(o).filter(([, v]) => v && String(v).trim()).map(([k]) => k); } catch {} }
  } catch (e) { r.err = e.name === 'AbortError' ? 'timeout' : e.message; }
  try {
    const res = await fetchT(base + '/ads.txt', 7000);
    if (res.ok) { const t = await res.text(); const pm = t.match(/pub-\d+/); r.adsTxt = pm ? pm[0] : null; }
  } catch {}
  return r;
}

const results = await Promise.all(FLEET.map(probe));
const now = new Date();
const stamp = now.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });

// derive fleet-level status per row
function verdict(r) {
  if (!r.domain) return { key: 'na', label: r.note?.includes('internal') ? 'N/A · internal' : 'No domain' };
  if (r.err) return { key: 'down', label: r.err };
  if (r.loader && r.adsTxt) return { key: r.slots.length ? 'full' : 'loader', label: r.slots.length ? `Loader + ${r.slots.length} unit(s)` : 'Loader live' };
  if (r.loader) return { key: 'partial', label: 'Loader, no ads.txt' };
  return { key: 'off', label: 'No ads on live site' };
}

const PILL = {
  full:    ['#0a7d33', '#e6f4ea', '● Loader + units'],
  loader:  ['#0a7d33', '#e9f6ee', '● Loader live'],
  partial: ['#a86a00', '#fdf3e2', '◐ Partial'],
  off:     ['#9a2323', '#fbeaea', '○ Not serving'],
  down:    ['#9a2323', '#fbeaea', '✕ Unreachable'],
  na:      ['#5b5b5b', '#eee', '– N/A'],
};

const liveCount = results.filter(r => r.loader).length;
const sitesInAccount = Object.keys(ADSENSE.sites || {}).length;
const readyCount = Object.values(ADSENSE.sites || {}).filter(s => s.review === 'Ready').length;
const gettingReady = Object.values(ADSENSE.sites || {}).filter(s => s.review === 'Getting ready').length;

const POLICY = {
  allow:   ['#0a7d33', '#e6f4ea', '✅ Ads OK'],
  forbid:  ['#9a2323', '#fbeaea', '⛔ No ads (luxury/commerce)'],
  verify:  ['#a86a00', '#fdf3e2', '⚠ Verify content first'],
  na:      ['#5b5b5b', '#eee', '– N/A'],
  unknown: ['#4a4a4a', '#eef0f2', '· Unclassified'],
};
function policyPill(r) {
  const [fg, bg, txt] = POLICY[r.policy] || POLICY.na;
  const flag = r.flag ? `<div class="sub warn">${r.flag}</div>` : '';
  return `<span class="pill" style="color:${fg};background:${bg}">${txt}</span>${flag}`;
}

// AdSense dashboard review-status pill (from the console snapshot)
const REVIEW = {
  'Ready':           ['#0a7d33', '#e6f4ea'],
  'Getting ready':   ['#0a6ba8', '#e4f1fa'],
  'Requires review': ['#a86a00', '#fdf3e2'],
  'Needs attention': ['#9a2323', '#fbeaea'],
};
function adsenseCells(domain, liveAdsTxt) {
  const s = ADSENSE.sites?.[domain];
  if (!s) return `<td><span class="muted">not in account</span></td><td><span class="muted">—</span></td>`;
  const [fg, bg] = REVIEW[s.review] || ['#4a4a4a', '#eef0f2'];
  const rev = `<span class="pill" style="color:${fg};background:${bg}">${s.review}</span><div class="sub">added ${s.added}</div>`;
  // reconcile Google-crawled ads.txt vs what we actually serve live
  const liveHas = !!liveAdsTxt;
  const gAuth = s.adsTxt === 'Authorized';
  let recon;
  if (gAuth) recon = `<span style="color:#0a7d33">✓ Authorized</span>`;
  else if (liveHas) recon = `<span style="color:#a86a00">Google: not found<br><span class="sub">but we serve it — awaiting re-crawl</span></span>`;
  else recon = `<span style="color:#9a2323">✗ not found</span>`;
  return `<td>${rev}</td><td>${recon}</td>`;
}

// Manual account-status card (dashboard-only truth)
const AS = { yes: ['#0a7d33', '#e6f4ea', 'YES'], no: ['#9a2323', '#fbeaea', 'NO'], unknown: ['#a86a00', '#fdf3e2', 'UNKNOWN'] };
const asBadge = (v) => { const [fg, bg, t] = AS[v] || AS.unknown; return `<span class="pill" style="color:${fg};background:${bg}">${t}</span>`; };
const acctCard = `<div class="acct">
  <h2>Account status — <span class="muted">dashboard-only, not probeable</span></h2>
  <div class="acct-row">
    <div><span>Approved</span>${asBadge(ACCT.approved)}</div>
    <div><span>Auto ads ON</span>${asBadge(ACCT.autoAdsOn)}</div>
    <div><span>Actually serving</span>${asBadge(ACCT.serving)}</div>
    <div><span>Account</span><code>${ACCT.account || '—'}</code></div>
    <div><span>Pub id</span><code>${ACCT.pubId}</code></div>
  </div>
  <div class="acct-foot">${ACCT.checkedAt ? `🕓 checked <code>${ACCT.checkedAt}</code>` : '<span class="warn">Not yet checked.</span>'} ${ACCT.notes ? '· ' + ACCT.notes : ''}
    <div class="muted">Fill these at <a href="https://adsense.google.com/" target="_blank" rel="noopener">adsense.google.com</a> → edit <code>account-status.json</code> → re-run <code>node build.mjs</code>.</div></div>
</div>`;

function cell(v, href, mono) {
  const t = v == null || v === '' ? '<span class="muted">—</span>' : (mono ? `<code>${v}</code>` : v);
  return href ? `<a href="${href}" target="_blank" rel="noopener">${t}</a>` : t;
}

const rows = results.map(r => {
  const vd = verdict(r);
  const [fg, bg, txt] = PILL[vd.key];
  const site = r.domain ? `<a href="https://${r.domain}/" target="_blank" rel="noopener">${r.domain}</a>` : `<span class="muted">${r.note || '—'}</span>`;
  return `<tr>
    <td><span class="proj">${r.project}</span><div class="sub">${site}</div></td>
    <td>${policyPill(r)}</td>
    ${adsenseCells(r.domain, r.adsTxt)}
    <td><span class="pill" style="color:${fg};background:${bg}">${txt}</span><div class="sub">${vd.label}</div></td>
    <td>${r.loader ? cell('yes', r.domain ? `view-source:https://${r.domain}/` : null) : cell(r.domain ? 'no' : null)}</td>
    <td>${r.slots.length ? cell(r.slots.join(', ')) : '<span class="muted">none (Auto only)</span>'}</td>
  </tr>`;
}).join('\n');

const html = `<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>AdSense Fleet Status</title>
<style>
:root{--ink:#1a1a1a;--line:#e6e3dc;--muted:#9a9384}
*{box-sizing:border-box}body{margin:0;font:15px/1.55 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:var(--ink);background:#f7f5f0}
.wrap{max-width:1080px;margin:0 auto;padding:28px 20px 70px}
h1{font-size:24px;margin:0 0 2px}.stamp{color:var(--muted);font-size:13px;margin-bottom:18px}
.stamp code{background:#efeae1;padding:1px 6px;border-radius:5px}
.cards{display:flex;gap:14px;flex-wrap:wrap;margin:16px 0 22px}
.card{flex:1;min-width:150px;background:#fff;border:1px solid var(--line);border-radius:12px;padding:14px 16px}
.card b{display:block;font-size:28px;line-height:1.1}.card span{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.05em}
table{width:100%;border-collapse:collapse;background:#fff;border:1px solid var(--line);border-radius:12px;overflow:hidden}
th,td{text-align:left;padding:11px 13px;border-bottom:1px solid var(--line);vertical-align:top;font-size:13px}
th{background:#efeae1;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:#6b6b6b}
tr:last-child td{border-bottom:none}
.proj{font-weight:600}.sub{color:var(--muted);font-size:12px;margin-top:2px}.sub a{color:#1d7a36}
.pill{display:inline-block;padding:3px 9px;border-radius:999px;font-size:12px;font-weight:600;white-space:nowrap}
code{background:#f2efe8;padding:1px 5px;border-radius:4px;font-size:12px}
a{color:#1d7a36}.muted{color:var(--muted)}
.legend{margin-top:22px;background:#fff;border:1px solid var(--line);border-radius:12px;padding:16px 18px;font-size:13px}
.legend h2{font-size:13px;text-transform:uppercase;letter-spacing:.06em;color:#6b6b6b;margin:0 0 8px}
.legend li{margin:4px 0}.warn{color:#9a2323}
.acct{background:#fff;border:1px solid var(--line);border-radius:12px;padding:16px 18px;margin:0 0 20px}
.acct h2{font-size:13px;text-transform:uppercase;letter-spacing:.06em;color:#6b6b6b;margin:0 0 12px}
.acct-row{display:flex;gap:22px;flex-wrap:wrap}
.acct-row>div{display:flex;flex-direction:column;gap:5px}
.acct-row span{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.05em}
.acct-foot{margin-top:12px;font-size:12px;color:#6b6b6b}
</style></head><body><div class="wrap">
<h1>AdSense Fleet Status</h1>
<div class="stamp">🕓 Live-probed <code>${stamp}</code> · re-run <code>node build.mjs</code> to refresh</div>

<div class="cards">
  <div class="card"><b>${readyCount}/${sitesInAccount}</b><span>Sites approved (Ready) in AdSense</span></div>
  <div class="card"><b>${gettingReady}</b><span>Getting ready (in review)</span></div>
  <div class="card"><b>${liveCount}</b><span>Loader served live on-site</span></div>
  <div class="card"><b>0</b><span>Manual ad units live</span></div>
</div>

${acctCard}

<table>
<thead><tr><th>Project / Domain</th><th>Policy</th><th>AdSense review</th><th>ads.txt (Google vs live)</th><th>Live loader status</th><th>Loader served</th><th>Manual units</th></tr></thead>
<tbody>
${rows}
</tbody></table>

<div class="legend">
<h2>How to read this</h2>
<ul>
<li><b>Loader live</b> — the <code>adsbygoogle.js</code> Auto-ads script is being served on the live domain (covers mobile + desktop from one responsive tag).</li>
<li><b>Manual units</b> — named <code>&lt;ins&gt;</code> ad slots. <b>Zero are live fleet-wide</b> — every <code>__ADSLOTS</code> is empty, so only Auto ads is in play.</li>
<li class="warn"><b>Not shown here: real ad serving.</b> Paid creative only paints if the AdSense account is <b>approved for that domain</b> AND <b>Auto ads is toggled ON</b> in the dashboard. That is account-side and can't be verified by probing — check <a href="https://adsense.google.com/" target="_blank" rel="noopener">adsense.google.com</a>.</li>
</ul>
</div>
</div></body></html>`;

await writeFile(new URL('./public/index.html', import.meta.url), html);
await writeFile(new URL('./public/status.json', import.meta.url), JSON.stringify({ probedAt: now.toISOString(), results }, null, 2));
console.log(`Wrote public/index.html — ${sitesInAccount} sites in account, ${readyCount} Ready, ${liveCount} loader-live at ${stamp}`);