← 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><ins></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}`);