← back to All Dw Landing

server.js

138 lines

/* all.designerwallcoverings.com — internal Basic-Auth directory of every
   *.designerwallcoverings.com brand-microsite line (rollout tracker).
   Serves data/sites.json merged with periodic live HTTPS probes of each
   subdomain. No Cloudflare API dependency. READ-ONLY against the world. */
const express = require('express');
const https = require('https');
const fs = require('fs');
const path = require('path');

const PORT = Number(process.env.PORT || 9946);
const DATA = path.join(__dirname, 'data', 'sites.json');
const PROBE_CACHE = path.join(__dirname, 'data', 'probe-results.json');
const PROBE_INTERVAL_SEC = Number(process.env.PROBE_INTERVAL_SEC || 900); // 15 min
const PROBE_TIMEOUT_MS = Number(process.env.PROBE_TIMEOUT_MS || 5000);
const PROBE_CONCURRENCY = Number(process.env.PROBE_CONCURRENCY || 10);

const app = express();

// HTTP Basic Auth — internal-only gate. Override via BASIC_AUTH="user:pass".
const [AUTH_USER, AUTH_PASS] = (process.env.BASIC_AUTH || 'admin:DW2024!').split(':');
app.use((req, res, next) => {
  const hdr = req.headers.authorization || '';
  const [scheme, b64] = hdr.split(' ');
  if (scheme === 'Basic' && b64) {
    const [u, p] = Buffer.from(b64, 'base64').toString('utf8').split(':');
    if (u === AUTH_USER && p === AUTH_PASS) return next();
  }
  res.set('WWW-Authenticate', 'Basic realm="All DW Microsites (internal)"');
  return res.status(401).send('Authentication required.');
});

app.use(express.json({ limit: '64kb' }));

// ── manifest ────────────────────────────────────────────────────────────────
let SNAP = { counts: {}, sites: [] };
function load() {
  SNAP = JSON.parse(fs.readFileSync(DATA, 'utf8'));
  console.log(`[all-dw] loaded ${SNAP.sites.length} sites (built ${SNAP.built_at})`);
}
load();
fs.watchFile(DATA, { interval: 5000 }, (cur, prev) => {
  if (cur.mtimeMs !== prev.mtimeMs) { try { load(); } catch (e) { console.error('[all-dw] reload failed:', e.message); } }
});

// ── probe loop ──────────────────────────────────────────────────────────────
// status per slug: PROBING (not yet probed) | LIVE-GATED (401) | LIVE-OPEN (2xx/3xx)
// | BROKEN (404/other 4xx/5xx) | MISSING (DNS / SSL / timeout / conn failure)
let PROBES = {};             // slug -> { status, httpStatus, detail, lastProbe, ms }
try { PROBES = JSON.parse(fs.readFileSync(PROBE_CACHE, 'utf8')); } catch {}
let LAST_CYCLE = null;       // ISO of last completed full cycle
let CYCLE_RUNNING = false;

function probeOne(slug) {
  return new Promise((resolve) => {
    const host = `${slug}.designerwallcoverings.com`;
    const t0 = Date.now();
    const done = (status, httpStatus, detail) => resolve({
      status, httpStatus: httpStatus || null, detail: detail || null,
      lastProbe: new Date().toISOString(), ms: Date.now() - t0,
    });
    const req = https.get({ host, path: '/', timeout: PROBE_TIMEOUT_MS, headers: { 'User-Agent': 'all-dw-landing-probe/1.0 (internal rollout tracker)' } }, (res) => {
      res.resume(); // discard body
      const c = res.statusCode;
      if (c === 401) return done('LIVE-GATED', c, 'exists behind auth');
      if (c >= 200 && c < 400) return done('LIVE-OPEN', c);
      return done('BROKEN', c, `HTTP ${c}`);
    });
    req.on('timeout', () => { req.destroy(new Error('timeout')); });
    req.on('error', (e) => done('MISSING', null, e.code || e.message));
  });
}

async function runProbeCycle(reason) {
  if (CYCLE_RUNNING) return { skipped: true, reason: 'cycle already running' };
  CYCLE_RUNNING = true;
  const slugs = SNAP.sites.map(s => s.slug);
  console.log(`[all-dw] probe cycle start (${slugs.length} subdomains, reason=${reason})`);
  const queue = [...slugs];
  const workers = Array.from({ length: Math.min(PROBE_CONCURRENCY, queue.length) }, async () => {
    while (queue.length) {
      const slug = queue.shift();
      PROBES[slug] = await probeOne(slug);
    }
  });
  await Promise.all(workers);
  LAST_CYCLE = new Date().toISOString();
  try { fs.writeFileSync(PROBE_CACHE, JSON.stringify(PROBES)); } catch {}
  CYCLE_RUNNING = false;
  const tally = {};
  for (const p of Object.values(PROBES)) tally[p.status] = (tally[p.status] || 0) + 1;
  console.log(`[all-dw] probe cycle done: ${JSON.stringify(tally)}`);
  return { done: true, tally };
}
runProbeCycle('boot');                                   // serve immediately; results fill in
setInterval(() => runProbeCycle('interval'), PROBE_INTERVAL_SEC * 1000).unref();

// ── API ─────────────────────────────────────────────────────────────────────
function merged() {
  return SNAP.sites.map(s => {
    const pr = PROBES[s.slug] || { status: 'PROBING', httpStatus: null, detail: null, lastProbe: null, ms: null };
    return { ...s, url: `https://${s.slug}.designerwallcoverings.com/`, probe: pr };
  });
}

app.get('/api/sites', (_req, res) => {
  const sites = merged();
  const brands = sites.filter(s => s.type === 'brand' && s.activeProducts > 0);
  const live = new Set(['LIVE-GATED', 'LIVE-OPEN']);
  const withSite = brands.filter(s => s.hasMicrosite || live.has(s.probe.status));
  res.json({
    built_at: SNAP.built_at,
    counts: { ...SNAP.counts, brandsWithMicrosite: withSite.length },
    rollout: {
      withMicrosite: withSite.length,
      activeBrands: brands.length,
      pct: brands.length ? +(100 * withSite.length / brands.length).toFixed(1) : 0,
    },
    sites,
  });
});

app.get('/api/meta', (_req, res) => res.json({
  lastRefresh: LAST_CYCLE || SNAP.built_at,
  intervalSec: PROBE_INTERVAL_SEC,
  probing: CYCLE_RUNNING,
  count: SNAP.sites.length,
  now: new Date().toISOString(),
}));

app.post('/api/probe', async (_req, res) => {
  const r = await runProbeCycle('manual');
  res.json(r);
});

app.use(express.static(path.join(__dirname, 'public')));

app.listen(PORT, () => console.log(`all-dw-landing → http://127.0.0.1:${PORT}  (Basic Auth ${AUTH_USER}/***)`));