← back to Adsense Command Post
server.mjs
133 lines
#!/usr/bin/env node
// AdSense Manager command post — Basic-auth GRID of all monetize sites (thumbnail +
// approval status + readiness + ads.txt), plus a table view. Sort + density slider
// (localStorage-persisted). Reads data/audit-latest.json + data/status-latest.json,
// serves screenshots from data/shots/. Port: env PORT or 9971.
import http from 'http';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { execFile } from 'child_process';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SKILL = __dirname; // command post: ./data synced from Mac2
const DATA = path.join(SKILL, 'data');
const SHOTS = path.join(DATA, 'shots');
const PORT = process.env.PORT || 9971;
const USER = 'admin', PASS = 'DW2024!';
const read = f => { try { return JSON.parse(fs.readFileSync(path.join(DATA, f), 'utf8')); } catch { return null; } };
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
function data() {
const audit = read('audit-latest.json') || { results: [], ready: 0, ts: '' };
const status = read('status-latest.json') || { rows: [], tally: {}, ts: '', source: '?' };
const sMap = Object.fromEntries((status.rows || []).map(r => [r.domain, r]));
const rows = (audit.results || []).map(a => ({ ...a, approval: (sMap[a.domain] || {}).approval || 'UNKNOWN', ads_txt: (sMap[a.domain] || {}).ads_txt || '—' }));
return { rows, audit, status };
}
function page() {
const { rows, audit, status } = data();
const tally = status.tally || {};
const shotExists = d => { try { return fs.existsSync(path.join(SHOTS, d + '.png')); } catch { return false; } };
const cards = rows.map(r => {
const thumb = shotExists(r.domain) ? `/shots/${encodeURIComponent(r.domain)}.png` : (r.hero || '');
return { ...r, thumb };
});
const DATAJSON = JSON.stringify(cards.map(r => ({ domain: r.domain, name: r.name, thumb: r.thumb, approval: r.approval, ads_txt: r.ads_txt, readiness: r.readiness, ready: r.ready, blockers: r.blockers || [] })));
const tile = (n, l, cls = '') => `<div class="tile ${cls}"><div class="n">${n}</div><div class="l">${l}</div></div>`;
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>AdSense Command Post</title><style>
:root{--bg:#f5f4f1;--panel:#fff;--ink:#1b1917;--muted:#6b6560;--rule:#e5e1da;--accent:#2f7a4f;--mono:ui-monospace,Menlo,monospace;--cols:5}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 -apple-system,'Segoe UI',Roboto,sans-serif}
.wrap{max-width:1320px;margin:0 auto;padding:26px 20px 80px}
h1{font-size:23px;margin:0 0 3px}.sub{color:var(--muted);font-size:13px;margin-bottom:16px}
.tiles{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-bottom:16px}
@media(max-width:720px){.tiles{grid-template-columns:repeat(2,1fr)}}
.tile{background:var(--panel);border:1px solid var(--rule);border-radius:9px;padding:12px 14px}
.tile .n{font-size:24px;font-weight:600}.tile .l{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin-top:5px}
.tile.ready .n{color:#16794a}.tile.rev .n{color:#5b6470}
.ctl{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:16px}
.ctl label{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
select,button{font:inherit;border:1px solid var(--rule);background:var(--panel);color:var(--ink);border-radius:7px;padding:8px 11px;cursor:pointer}
button.pri{background:var(--accent);color:#fff;border-color:var(--accent)}
input[type=range]{accent-color:var(--accent)}
.grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:16px}
.card{background:var(--panel);border:1px solid var(--rule);border-radius:11px;overflow:hidden;display:flex;flex-direction:column;transition:transform .15s,box-shadow .15s}
.card:hover{transform:translateY(-3px);box-shadow:0 12px 28px rgba(0,0,0,.12)}
.thumb{aspect-ratio:1.4/1;background:#e9e6e0 center/cover no-repeat;position:relative;border-bottom:1px solid var(--rule)}
.thumb .badge{position:absolute;top:8px;left:8px;font-size:10.5px;font-weight:700;padding:3px 8px;border-radius:5px;color:#fff;letter-spacing:.02em}
.thumb .rd{position:absolute;top:8px;right:8px;font-size:10.5px;font-weight:700;padding:3px 7px;border-radius:5px;background:rgba(0,0,0,.62);color:#fff;font-variant-numeric:tabular-nums}
.card .body{padding:10px 12px 12px}
.card .nm{font-weight:600;font-size:13.5px;line-height:1.2}
.card .dm{font-family:var(--mono);font-size:11.5px;color:var(--muted);margin-top:2px;word-break:break-all}
.card .meta{display:flex;gap:8px;align-items:center;margin-top:8px;font-size:11px;color:var(--muted)}
.card a{color:var(--accent);text-decoration:none}
.noshot{display:flex;align-items:center;justify-content:center;height:100%;color:var(--muted);font-size:12px}
.foot{color:var(--muted);font-size:12px;margin-top:22px;line-height:1.7}
</style></head><body><div class="wrap">
<h1>AdSense Command Post</h1>
<div class="sub">pub-5278231299883833 · ${audit.results ? audit.results.length : 0} monetize sites · audit ${esc((audit.ts||'').slice(0,16))}Z · console ${esc((status.ts||'').slice(11,16))}</div>
<div class="tiles">
${tile(audit.results ? audit.results.length : 0, 'Sites')}
${tile(audit.ready ?? 0, 'Audit 100%', 'ready')}
${tile(tally['Ready'] || 0, 'Approved', 'ready')}
${tile(tally['Requires review'] || 0, 'In review', 'rev')}
${tile(tally['Needs attention'] || 0, 'Needs attention')}
</div>
<div class="ctl">
<label>Sort</label>
<select id="sort"><option value="approval">Approval</option><option value="readiness">Readiness</option><option value="domain">Domain A→Z</option><option value="ads">ads.txt</option></select>
<label>Density</label><input type="range" id="dens" min="2" max="7" value="5">
<span style="flex:1"></span>
<button class="pri" onclick="rc(this)">Re-check</button><span class="sub" id="msg"></span>
</div>
<div class="grid" id="grid"></div>
<p class="foot"><b>Grid</b> shows every monetize site with a live screenshot (ads render in the shot once a site is AdSense-approved). Badge = AdSense status, corner = our readiness audit. "Requires review" is normal until Google finishes review; ads.txt "Not found" clears when Google re-crawls (it's live). Sort + density persist. Re-check re-runs the audit + reads the console.</p>
</div>
<script>
const DATA=${DATAJSON};
const BADGE={'Ready':'#16794a','Getting ready':'#a9761a','Requires review':'#5b6470','Needs attention':'#b0413a','UNKNOWN':'#8a8f98'};
const g=document.getElementById('grid'),sortSel=document.getElementById('sort'),dens=document.getElementById('dens');
try{const s=localStorage.getItem('acp_sort');if(s)sortSel.value=s;const d=localStorage.getItem('acp_dens');if(d)dens.value=d;}catch(e){}
document.documentElement.style.setProperty('--cols',dens.value);
const order={'Ready':0,'Needs attention':1,'Getting ready':2,'Requires review':3,'UNKNOWN':4};
function render(){
const k=sortSel.value;
let r=DATA.slice();
r.sort((a,b)=> k==='approval'?(order[a.approval]-order[b.approval]||a.domain.localeCompare(b.domain))
: k==='readiness'?(a.readiness-b.readiness||a.domain.localeCompare(b.domain))
: k==='ads'?String(a.ads_txt).localeCompare(String(b.ads_txt))
: a.domain.localeCompare(b.domain));
g.innerHTML=r.map(x=>{
const th=x.thumb?('background-image:url("'+x.thumb+'")'):'';
const badge=BADGE[x.approval]||'#8a8f98';
return '<div class="card"><a href="https://'+x.domain+'" target="_blank" rel="noopener">'
+'<div class="thumb" style="'+th+'">'+(x.thumb?'':'<div class=noshot>no preview</div>')
+'<span class="badge" style="background:'+badge+'">'+x.approval+'</span>'
+'<span class="rd">'+x.readiness+'%</span></div></a>'
+'<div class="body"><div class="nm">'+x.name+'</div><div class="dm">'+x.domain+'</div>'
+'<div class="meta">ads.txt: '+x.ads_txt+' · <a href="https://'+x.domain+'" target="_blank" rel="noopener">open ↗</a></div></div></div>';
}).join('');
}
sortSel.onchange=()=>{try{localStorage.setItem('acp_sort',sortSel.value)}catch(e){}render()};
dens.oninput=()=>{document.documentElement.style.setProperty('--cols',dens.value);try{localStorage.setItem('acp_dens',dens.value)}catch(e){}};
async function rc(b){b.disabled=true;document.getElementById('msg').textContent='re-checking…';try{await fetch('/api/refresh',{method:'POST'});location.reload();}catch(e){b.disabled=false;document.getElementById('msg').textContent='err';}}
setTimeout(()=>location.reload(),180000);
render();
</script></body></html>`;
}
http.createServer((req, res) => {
const auth = (req.headers.authorization || '').split(' ')[1] || '';
const [u, p] = Buffer.from(auth, 'base64').toString().split(':');
if (u !== USER || p !== PASS) { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="adsense"' }); return res.end('Auth required'); }
if (req.url.startsWith('/shots/')) {
const f = path.join(SHOTS, path.basename(decodeURIComponent(req.url.slice(7))));
if (f.endsWith('.png') && fs.existsSync(f)) { res.writeHead(200, { 'content-type': 'image/png', 'cache-control': 'public,max-age=300' }); return fs.createReadStream(f).pipe(res); }
res.writeHead(404); return res.end();
}
if (req.method === 'POST' && req.url === '/api/refresh'){res.writeHead(200,{'content-type':'application/json'});return res.end('{"ok":true,"note":"refreshed by Mac2 poller"}');}
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); res.end(page());
}).listen(PORT, () => console.log(`AdSense Command Post on http://127.0.0.1:${PORT} (admin/DW2024!)`));