[object Object]

← back to Purelymail Fleet Viewer

purelymail fleet remediation live viewer on :9779

8e1cb84d2d1aa2a183e138fa511b71d754a3e0bf · 2026-05-18 15:30:53 -0700 · SteveStudio2

Files touched

Diff

commit 8e1cb84d2d1aa2a183e138fa511b71d754a3e0bf
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Mon May 18 15:30:53 2026 -0700

    purelymail fleet remediation live viewer on :9779
---
 .gitignore                   |   8 ++
 data/remediation-status.json |  26 ++++++
 public/index.html            | 186 +++++++++++++++++++++++++++++++++++++++++++
 server.js                    |  50 ++++++++++++
 4 files changed, 270 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1924158
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/data/remediation-status.json b/data/remediation-status.json
new file mode 100644
index 0000000..5a7e7a8
--- /dev/null
+++ b/data/remediation-status.json
@@ -0,0 +1,26 @@
+{
+  "updatedAt": "2026-05-18T15:30:00Z",
+  "stage": "fix-running",
+  "stageLabel": "DNS-fix agent running — Fix A + Fix B",
+  "fixA": {
+    "status": "running",
+    "label": "Remove stale dual-SPF record on 32 GoDaddy domains",
+    "done": 0,
+    "total": 32,
+    "note": "Canary on roomsettings.com first, dig-verify, then batch 31"
+  },
+  "fixB": {
+    "status": "running",
+    "label": "Add Purelymail catch-all routing rules — venturaclaw.com + venturaclaw.agentabrams.com",
+    "done": 0,
+    "total": 2
+  },
+  "receiveTest": {
+    "status": "pending",
+    "label": "211-domain receive probe — UUID-tagged, gated behind Fix A+B",
+    "passed": 0,
+    "failed": 0,
+    "total": 211,
+    "results": []
+  }
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..1c267b5
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,186 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Purelymail Fleet — Live Remediation</title>
+<style>
+  :root{
+    --bg:#0e0d0b; --panel:#1a1815; --line:#2e2a24; --ink:#ece7dc; --dim:#8c857a;
+    --gold:#c9a24b; --ok:#3fae6b; --warn:#d6a23a; --bad:#cf5b4e; --run:#5b87c4;
+  }
+  *{box-sizing:border-box}
+  body{margin:0;background:var(--bg);color:var(--ink);
+    font:14px/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
+  header{padding:22px 28px 14px;border-bottom:1px solid var(--line)}
+  h1{margin:0;font-size:18px;font-weight:600;letter-spacing:.04em}
+  h1 span{color:var(--gold)}
+  .sub{color:var(--dim);font-size:12px;margin-top:4px}
+  .wrap{padding:20px 28px;max-width:1280px}
+  h2{font-size:13px;letter-spacing:.08em;text-transform:uppercase;color:var(--gold);
+    margin:28px 0 10px;font-weight:600}
+  .cards{display:flex;gap:12px;flex-wrap:wrap;margin:14px 0 4px}
+  .card{background:var(--panel);border:1px solid var(--line);border-radius:8px;
+    padding:12px 16px;min-width:108px}
+  .card .n{font-size:24px;font-weight:700}
+  .card .l{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.05em}
+  .card.ok .n{color:var(--ok)} .card.warn .n{color:var(--warn)}
+  .card.bad .n{color:var(--bad)} .card.run .n{color:var(--run)}
+  .stage{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:14px 16px;margin:8px 0}
+  .stage h3{margin:0 0 8px;font-size:13px;font-weight:600}
+  .bar{height:9px;background:#26221b;border-radius:5px;overflow:hidden;margin:8px 0 4px}
+  .bar i{display:block;height:100%;background:var(--gold);transition:width .4s}
+  .pill{display:inline-block;padding:1px 8px;border-radius:10px;font-size:11px;font-weight:600}
+  .pill.ok{background:rgba(63,174,107,.16);color:var(--ok)}
+  .pill.run{background:rgba(91,135,196,.16);color:var(--run)}
+  .pill.warn{background:rgba(214,162,58,.16);color:var(--warn)}
+  .pill.bad{background:rgba(207,91,78,.16);color:var(--bad)}
+  .pill.pend{background:#26221b;color:var(--dim)}
+  .ctl{margin:10px 0;display:flex;gap:10px;align-items:center;flex-wrap:wrap}
+  select{background:var(--panel);color:var(--ink);border:1px solid var(--line);
+    border-radius:6px;padding:5px 9px;font:inherit;font-size:12px}
+  table{width:100%;border-collapse:collapse;margin-top:6px}
+  th,td{text-align:left;padding:5px 9px;border-bottom:1px solid var(--line);font-size:12px}
+  th{color:var(--dim);font-weight:500;font-size:10px;text-transform:uppercase;letter-spacing:.05em;
+    cursor:pointer;user-select:none}
+  tr:hover td{background:#171511}
+  .y{color:var(--ok)} .x{color:var(--bad)} .muted{color:var(--dim)}
+  .foot{color:var(--dim);font-size:11px;margin-top:24px;padding-top:12px;border-top:1px solid var(--line)}
+  #err{color:var(--bad);font-size:12px}
+</style>
+</head>
+<body>
+<header>
+  <h1><span>◆</span> Purelymail Fleet — Live Remediation</h1>
+  <div class="sub" id="stamp">loading…</div>
+</header>
+<div class="wrap">
+  <div id="err"></div>
+
+  <h2>Fleet Health — 211 domains</h2>
+  <div class="cards" id="cards"></div>
+  <div class="sub" id="healthnote"></div>
+
+  <h2>Remediation Progress</h2>
+  <div id="stages"></div>
+
+  <h2>Per-Domain Status</h2>
+  <div class="ctl">
+    <label>Filter
+      <select id="filter">
+        <option value="all">All 211</option>
+        <option value="dualspf">Dual-SPF (Fix A)</option>
+        <option value="norouting">No routing (Fix B)</option>
+        <option value="dead">Dead</option>
+        <option value="healthy">Fully healthy</option>
+        <option value="undeliverable">Undeliverable</option>
+      </select>
+    </label>
+    <label>Sort
+      <select id="sort">
+        <option value="domain">Domain A→Z</option>
+        <option value="status">Status (worst first)</option>
+        <option value="deliverable">Deliverable first</option>
+      </select>
+    </label>
+    <span class="sub" id="rowcount"></span>
+  </div>
+  <table id="domains">
+    <thead><tr>
+      <th>Domain</th><th>MX</th><th>SPF</th><th>DKIM</th><th>DMARC</th>
+      <th>Routing</th><th>Deliverable</th><th>Issues</th>
+    </tr></thead>
+    <tbody></tbody>
+  </table>
+
+  <div class="foot" id="foot"></div>
+</div>
+
+<script>
+const $ = s => document.querySelector(s);
+let DATA = null;
+
+function pill(cls,txt){ return '<span class="pill '+cls+'">'+txt+'</span>'; }
+function yn(v){ return v ? '<span class="y">✓</span>' : '<span class="x">✗</span>'; }
+
+function stageCard(s){
+  const st = s.status || 'pending';
+  const cls = st==='done'?'ok':st==='running'?'run':st==='failed'?'bad':'pend';
+  const pct = s.total ? Math.round((s.done||0)/s.total*100) : (st==='done'?100:0);
+  let counts = '';
+  if (s.total != null) counts = (s.done||0)+' / '+s.total;
+  if (s.passed != null) counts = (s.passed||0)+' passed · '+(s.failed||0)+' failed / '+s.total;
+  return '<div class="stage"><h3>'+(s.label||'')+'  '+pill(cls,st)+'</h3>'
+    + '<div class="bar"><i style="width:'+pct+'%"></i></div>'
+    + '<div class="sub">'+counts+(s.note?'  ·  '+s.note:'')+'</div></div>';
+}
+
+function classify(d){
+  const i = d.issues || [];
+  if (i.includes('DEAD')||i.some(x=>/EXPIRED|CANCEL|PARK/i.test(x))) return {k:'dead',rank:0};
+  if (!d.deliverable) return {k:'undeliverable',rank:1};
+  if (i.includes('NO_ROUTING')||d.hasRouting===false) return {k:'norouting',rank:2};
+  if (i.some(x=>/SPF/i.test(x))) return {k:'dualspf',rank:3};
+  return {k:'healthy',rank:5};
+}
+
+function render(d){
+  DATA = d;
+  $('#err').textContent = d.healthFound ? '' : 'domain-health.json not found yet — showing remediation status only';
+  $('#stamp').textContent = 'refreshed ' + new Date(d.generatedAt).toLocaleTimeString()
+    + '  ·  auto-refresh 15s  ·  stage: ' + (d.remediation.stageLabel || d.remediation.stage || '—');
+
+  const t = d.health.totals || {};
+  $('#cards').innerHTML = [
+    ['Total', t.domains ?? '—', ''],
+    ['Healthy', t.fullyHealthy ?? '—', 'ok'],
+    ['Dual-SPF', t.dualSpfRfcViolation ?? '—', 'warn'],
+    ['No routing', t.noRouting ?? '—', 'warn'],
+    ['Dead', t.dead ?? '—', 'bad'],
+    ['Undeliverable', t.undeliverable ?? '—', 'bad'],
+  ].map(c=>'<div class="card '+c[2]+'"><div class="n">'+c[1]+'</div>'
+    +'<div class="l">'+c[0]+'</div></div>').join('');
+  $('#healthnote').textContent = d.health.note || '';
+
+  const r = d.remediation;
+  $('#stages').innerHTML = [r.fixA, r.fixB, r.receiveTest]
+    .filter(Boolean).map(stageCard).join('');
+
+  drawTable();
+  $('#foot').textContent = 'purelymail-fleet-viewer · 127.0.0.1:9779 · health source: email-deliverability-agent/output/domain-health.json';
+}
+
+function drawTable(){
+  if (!DATA) return;
+  let rows = (DATA.health.domains || []).slice();
+  const f = $('#filter').value, sortBy = $('#sort').value;
+  if (f !== 'all') rows = rows.filter(d => classify(d).k === f);
+  rows.sort((a,b)=>{
+    if (sortBy==='domain') return (a.domain||'').localeCompare(b.domain||'');
+    if (sortBy==='deliverable') return (b.deliverable?1:0)-(a.deliverable?1:0) || (a.domain||'').localeCompare(b.domain);
+    return classify(a).rank - classify(b).rank || (a.domain||'').localeCompare(b.domain);
+  });
+  $('#rowcount').textContent = rows.length + ' shown';
+  $('#domains tbody').innerHTML = rows.map(d=>{
+    const deliv = d.deliverable ? pill('ok','yes') : pill('bad','no');
+    return '<tr><td>'+d.domain+'</td>'
+      + '<td>'+yn(d.passesMx)+'</td><td>'+yn(d.passesSpf)+'</td>'
+      + '<td>'+yn(d.passesDkim)+'</td><td>'+yn(d.passesDmarc)+'</td>'
+      + '<td>'+yn(d.hasRouting)+'</td><td>'+deliv+'</td>'
+      + '<td class="muted">'+((d.issues||[]).join(', ')||'—')+'</td></tr>';
+  }).join('') || '<tr><td colspan="8" class="muted">no domains match</td></tr>';
+}
+
+async function load(){
+  try{
+    const r = await fetch('/api/status');
+    render(await r.json());
+  }catch(e){ $('#err').textContent = 'load failed: '+e.message; }
+}
+$('#filter').addEventListener('change', drawTable);
+$('#sort').addEventListener('change', drawTable);
+load();
+setInterval(load, 15000);
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..a2f120c
--- /dev/null
+++ b/server.js
@@ -0,0 +1,50 @@
+#!/usr/bin/env node
+/* Purelymail fleet remediation — live web viewer (zero dependencies).
+ *
+ *   node server.js          # http://127.0.0.1:9779
+ *
+ * /api/status merges:
+ *   - email-deliverability-agent/output/domain-health.json  (live fleet state, updated by the DNS agent)
+ *   - data/remediation-status.json                          (Fix A/B + receive-test progress)
+ */
+'use strict';
+const http = require('http');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const PORT = 9779;
+const HEALTH = path.join(os.homedir(), 'Projects/email-deliverability-agent/output/domain-health.json');
+const STATUS = path.join(__dirname, 'data/remediation-status.json');
+
+const readJSON = (p, fb) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return fb; } };
+
+function buildStatus() {
+  const health = readJSON(HEALTH, null);
+  const remediation = readJSON(STATUS, null);
+  return {
+    generatedAt: new Date().toISOString(),
+    healthFound: !!health,
+    health: health || { totals: {}, domains: [], dualSpfDomains: [], deadDomains: [], noRoutingDomains: [] },
+    remediation: remediation || { stage: 'unknown', fixA: {}, fixB: {}, receiveTest: {} },
+  };
+}
+
+const server = http.createServer((req, res) => {
+  if (req.url === '/api/status') {
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    res.end(JSON.stringify(buildStatus()));
+    return;
+  }
+  try {
+    const html = fs.readFileSync(path.join(__dirname, 'public/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(`purelymail fleet viewer -> http://127.0.0.1:${PORT}`);
+});

(oldest)  ·  back to Purelymail Fleet Viewer  ·  bind viewer to 0.0.0.0 for tailnet access c7b4ed0 →