← back to Coming Soon Template
viewer/server.js
128 lines
#!/usr/bin/env node
/* Coming-Soon fleet review viewer — single page, zero dependencies.
* Consolidates: the 21 abrams/butler domains (Pages/Worker/DNS/NS state),
* the dead-domain purge, and George account status.
*
* node viewer/server.js # serves http://127.0.0.1:9778
*
* /api/status aggregates live GoDaddy NS state + the project's data/*.json.
*/
'use strict';
const http = require('http');
const fs = require('fs');
const os = require('os');
const path = require('path');
const PORT = 9778;
const ENV = path.join(os.homedir(), 'Projects/secrets-manager/.env');
const ROOT = path.join(os.homedir(), 'Projects/coming-soon-template');
const WORKER_DOMAINS = ['bhbutler.com', 'boulevardbutler.com'];
function envVal(key) {
const line = fs.readFileSync(ENV, 'utf8').split('\n').find(l => l.startsWith(key + '='));
return line ? line.slice(key.length + 1).trim() : '';
}
const GD_KEY = envVal('GODADDY_API_KEY');
const GD_SECRET = envVal('GODADDY_API_SECRET');
const GD_PAT = envVal('GODADDY_PAT');
// Prefer a Bearer PAT when present (TK-10 key rotation); else legacy sso-key.
const GD_AUTH = GD_PAT ? `Bearer ${GD_PAT}` : `sso-key ${GD_KEY}:${GD_SECRET}`;
const readJSON = (p, fb) => { try { return JSON.parse(fs.readFileSync(path.join(ROOT, p), 'utf8')); } catch { return fb; } };
const isCfNs = ns => Array.isArray(ns) && ns.length > 0 && ns.every(h => /\.ns\.cloudflare\.com$/i.test(h));
async function godaddyNs(domain) {
try {
const res = await fetch(`https://api.godaddy.com/v1/domains/${domain}`, {
headers: { 'Authorization': GD_AUTH },
});
const j = await res.json();
return { ns: j.nameServers || [], status: j.status || 'UNKNOWN' };
} catch (e) {
return { ns: [], status: 'ERROR', error: String(e.message || e) };
}
}
async function georgeStatus() {
try {
const ctl = AbortSignal.timeout(5000);
const res = await fetch('http://100.107.67.67:9850/health', { signal: ctl });
const j = await res.json();
return Object.entries(j.accounts || {}).map(([slot, a]) => ({
slot, email: a.email, reachable: true,
}));
} catch {
return [
{ slot: 'steve-office', email: 'steve@designerwallcoverings.com', reachable: false },
{ slot: 'info', email: 'info@designerwallcoverings.com', reachable: false },
{ slot: 'steve-personal', email: 'steveabramsdesigns@gmail.com', reachable: false },
];
}
}
async function buildStatus() {
const zones = readJSON('data/zone-setup.json', []);
const dns = readJSON('data/dns-setup.json', []);
const dnsByDomain = Object.fromEntries(dns.map(d => [d.domain, d]));
const domains = await Promise.all(zones.map(async z => {
const gd = await godaddyNs(z.domain);
const project = z.domain.replace(/\.com$/, '');
const isWorker = WORKER_DOMAINS.includes(z.domain);
const d = dnsByDomain[z.domain];
return {
domain: z.domain,
host: isWorker ? 'Worker' : 'Pages',
zone_status: z.status,
dns_ready: isWorker ? true : !!(d && /already_correct|created|updated/.test(d.status || '')),
ns_on_cloudflare: isCfNs(gd.ns),
registrar_status: gd.status,
current_ns: gd.ns,
pages_dev: isWorker ? null : `${project}.pages.dev`,
live_url: `https://${z.domain}`,
};
}));
return {
generatedAt: new Date().toISOString(),
domains,
summary: {
total: domains.length,
pages: domains.filter(d => d.host === 'Pages').length,
workers: domains.filter(d => d.host === 'Worker').length,
flipped: domains.filter(d => d.ns_on_cloudflare).length,
},
dead: [
{ domain: 'brazilliancewallpaper.com', note: 'Expired 2026-05-04 (PENDING_HOLD_REDEMPTION). Purged from 5 active-config files + memory.' },
{ domain: 'dohngia.com', note: 'Expired 2026-05-04 (PENDING_HOLD_REDEMPTION). Purged from godaddy-active.json + decisions.json + memory.' },
],
george: await georgeStatus(),
};
}
const server = http.createServer(async (req, res) => {
if (req.url === '/api/status') {
try {
const data = await buildStatus();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
} catch (e) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: String(e.message || e) }));
}
return;
}
// serve the single page
try {
const html = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf8');
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
} catch {
res.writeHead(404); res.end('index.html missing');
}
});
server.listen(PORT, '127.0.0.1', () => {
console.log(`coming-soon review viewer -> http://127.0.0.1:${PORT}`);
});