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