← back to Purelymail Fleet Viewer
viewer: grid view + density slider + collapsible per-domain iframe
76964603c104a2742d0bf093bfad9451d34e71e9 · 2026-05-18 16:41:42 -0700 · SteveStudio2
Files touched
Diff
commit 76964603c104a2742d0bf093bfad9451d34e71e9
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Mon May 18 16:41:42 2026 -0700
viewer: grid view + density slider + collapsible per-domain iframe
---
public/index.html | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 105 insertions(+), 8 deletions(-)
diff --git a/public/index.html b/public/index.html
index 8fc64b6..bee4134 100644
--- a/public/index.html
+++ b/public/index.html
@@ -47,6 +47,21 @@
td a{color:var(--gold);text-decoration:none}
td a:hover{text-decoration:underline}
.y{color:var(--ok)} .x{color:var(--bad)} .muted{color:var(--dim)}
+ /* collapsible iframe row */
+ tr.drow{cursor:pointer}
+ .caret{display:inline-block;width:11px;color:var(--gold);font-size:10px}
+ tr.detail td{padding:0;background:#0d0c0a}
+ .frame-wrap iframe{width:100%;height:540px;border:0;background:#fff;display:block}
+ .frame-link{padding:6px 10px;font-size:11px}
+ /* grid view */
+ input[type=range]{accent-color:var(--gold);vertical-align:middle}
+ .dgrid{display:grid;grid-template-columns:repeat(var(--cols,3),1fr);gap:14px;margin-top:6px}
+ .dcard{background:var(--panel);border:1px solid var(--line);border-radius:8px;overflow:hidden}
+ .dcard .thumb{width:100%;height:230px;border:0;background:#fff;display:block}
+ .dcard .meta{padding:8px 10px}
+ .dcard .meta .dn{font-size:12px;font-weight:600}
+ .dcard .meta .dn a{color:var(--gold);text-decoration:none}
+ .dcard .meta .st{margin-top:6px;display:flex;gap:4px;flex-wrap:wrap}
.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>
@@ -85,6 +100,15 @@
<option value="deliverable">Deliverable first</option>
</select>
</label>
+ <label>View
+ <select id="view">
+ <option value="table">Table</option>
+ <option value="grid">Grid</option>
+ </select>
+ </label>
+ <label id="densitywrap" style="display:none">Density
+ <input type="range" id="density" min="1" max="6" value="3">
+ </label>
<span class="sub" id="rowcount"></span>
</div>
<table id="domains">
@@ -94,6 +118,7 @@
</tr></thead>
<tbody></tbody>
</table>
+ <div id="grid" class="dgrid" style="display:none"></div>
<div class="foot" id="foot"></div>
</div>
@@ -101,6 +126,7 @@
<script>
const $ = s => document.querySelector(s);
let DATA = null;
+let lastSig = 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>'; }
@@ -148,12 +174,14 @@ function render(d){
$('#stages').innerHTML = [r.fixA, r.fixB, r.receiveTest, r.sendTest]
.filter(Boolean).map(stageCard).join('');
- drawTable();
+ // only rebuild the domain table/grid when the domain data actually changed —
+ // avoids reloading every iframe on the 15s auto-refresh
+ const sig = JSON.stringify(d.health.domains || []);
+ if (sig !== lastSig){ lastSig = sig; draw(); }
$('#foot').textContent = 'purelymail-fleet-viewer · 127.0.0.1:9779 · health source: email-deliverability-agent/output/domain-health.json';
}
-function drawTable(){
- if (!DATA) return;
+function getRows(){
let rows = (DATA.health.domains || []).slice();
const f = $('#filter').value, sortBy = $('#sort').value;
if (f !== 'all') rows = rows.filter(d => classify(d).k === f);
@@ -162,26 +190,95 @@ function drawTable(){
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);
});
+ return rows;
+}
+
+function draw(){
+ if (!DATA) return;
+ const v = $('#view').value;
+ $('#domains').style.display = v==='table' ? '' : 'none';
+ $('#grid').style.display = v==='grid' ? '' : 'none';
+ $('#densitywrap').style.display = v==='grid' ? '' : 'none';
+ if (v==='grid') drawGrid(); else drawTable();
+}
+
+function toggleRow(safe, domain){
+ const det = $('#detail-'+safe), caret = $('#caret-'+safe);
+ if (!det) return;
+ const open = det.style.display === 'none';
+ det.style.display = open ? '' : 'none';
+ if (caret) caret.textContent = open ? '▾' : '▸';
+ if (open){
+ const fw = $('#fw-'+safe);
+ if (fw && !fw.dataset.loaded){
+ fw.dataset.loaded = '1';
+ fw.innerHTML = '<iframe src="https://'+domain+'" loading="lazy" referrerpolicy="no-referrer"></iframe>'
+ + '<div class="frame-link"><a href="https://'+domain+'" target="_blank" rel="noopener">open https://'
+ + domain+' ↗ (blank = site blocks embedding)</a></div>';
+ }
+ }
+}
+
+function drawGrid(){
+ const rows = getRows();
+ $('#rowcount').textContent = rows.length + ' shown';
+ $('#grid').innerHTML = rows.map(d=>{
+ const deliv = d.deliverable ? pill('ok','yes') : pill('bad','no');
+ const iss = (d.issues||[]).length ? pill('warn',d.issues.join(', ')) : pill('ok','clean');
+ return '<div class="dcard">'
+ + '<iframe class="thumb" src="https://'+d.domain+'" loading="lazy" referrerpolicy="no-referrer"></iframe>'
+ + '<div class="meta"><div class="dn"><a href="https://'+d.domain
+ + '" target="_blank" rel="noopener">'+d.domain+'</a></div>'
+ + '<div class="st">'+deliv+' '+iss+'</div></div></div>';
+ }).join('') || '<div class="muted">no domains match</div>';
+}
+
+function drawTable(){
+ const rows = getRows();
$('#rowcount').textContent = rows.length + ' shown';
$('#domains tbody').innerHTML = rows.map(d=>{
const deliv = d.deliverable ? pill('ok','yes') : pill('bad','no');
- const link = '<a href="https://'+d.domain+'" target="_blank" rel="noopener">'+d.domain+'</a>';
- return '<tr><td>'+link+'</td>'
+ const safe = d.domain.replace(/[^a-z0-9]/gi,'-');
+ const link = '<a href="https://'+d.domain+'" target="_blank" rel="noopener" '
+ + 'onclick="event.stopPropagation()">'+d.domain+'</a>';
+ return '<tr class="drow" onclick="toggleRow(\''+safe+'\',\''+d.domain+'\')">'
+ + '<td><span class="caret" id="caret-'+safe+'">▸</span> '+link+'</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>';
+ + '<td class="muted">'+((d.issues||[]).join(', ')||'—')+'</td></tr>'
+ + '<tr class="detail" id="detail-'+safe+'" style="display:none">'
+ + '<td colspan="8"><div class="frame-wrap" id="fw-'+safe+'"></div></td></tr>';
}).join('') || '<tr><td colspan="8" class="muted">no domains match</td></tr>';
}
+function setDensity(){
+ const n = $('#density').value;
+ $('#grid').style.setProperty('--cols', n);
+ localStorage.setItem('pmfv.density', n);
+}
+
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);
+
+// restore persisted controls (per-namespace localStorage keys)
+['filter','sort','view'].forEach(k=>{
+ const v = localStorage.getItem('pmfv.'+k);
+ if (v!=null && $('#'+k)) $('#'+k).value = v;
+});
+{ const d = localStorage.getItem('pmfv.density'); if (d!=null) $('#density').value = d; }
+
+['filter','sort','view'].forEach(k=>$('#'+k).addEventListener('change',()=>{
+ localStorage.setItem('pmfv.'+k, $('#'+k).value);
+ draw();
+}));
+$('#density').addEventListener('input', setDensity);
+
+setDensity();
load();
setInterval(load, 15000);
</script>
← cdf96c3 receive+send test complete — 202/203 receive, 203/203 send
·
back to Purelymail Fleet Viewer
·
viewer: server-rendered screenshots for every domain + Site d0d1ab6 →