← back to Purelymail Fleet Viewer
purelymail fleet remediation live viewer on :9779
8e1cb84d2d1aa2a183e138fa511b71d754a3e0bf · 2026-05-18 15:30:53 -0700 · SteveStudio2
Files touched
A .gitignoreA data/remediation-status.jsonA public/index.htmlA server.js
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 →