← back to Agentabrams Viewer
domain viewer: add sort + list/grid toggle + density + per-field on/off menu (viewer control standard)
096b817e576297048544a2fbc55aa06791f2d794 · 2026-08-11 14:21:21 -0700 · Steve
Files touched
Diff
commit 096b817e576297048544a2fbc55aa06791f2d794
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Aug 11 14:21:21 2026 -0700
domain viewer: add sort + list/grid toggle + density + per-field on/off menu (viewer control standard)
---
public/index.html | 293 ++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 283 insertions(+), 10 deletions(-)
diff --git a/public/index.html b/public/index.html
index 9ad6f20..7f8ec3a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -6,18 +6,76 @@
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%2328412f'/%3E%3Ctext x='8' y='12' text-anchor='middle' font-family='Georgia,serif' font-size='11' fill='%23f7f1e6'%3Ea%3C/text%3E%3C/svg%3E">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
- :root { --rail: 300px; --bg:#0e1116; --panel:#161b22; --line:#2b313b; --txt:#dfe6ee; --dim:#8b95a3; --accent:#3fb6a8; }
+ :root {
+ --rail: 300px;
+ --bg:#0e1116; --panel:#161b22; --line:#2b313b; --txt:#dfe6ee; --dim:#8b95a3; --accent:#3fb6a8;
+ --row-py: 7px; --row-px: 9px; --row-fs: 14px;
+ --rail-tile-min: 120px;
+ }
* { box-sizing: border-box; }
body { margin:0; height:100vh; display:flex; font:14px/1.45 -apple-system, "SF Pro Text", Helvetica, sans-serif; background:var(--bg); color:var(--txt); }
+ /* ── Rail ── */
#rail { width:var(--rail); min-width:230px; max-width:480px; display:flex; flex-direction:column; border-right:1px solid var(--line); background:var(--panel); }
#rail header { padding:14px 14px 10px; border-bottom:1px solid var(--line); }
#rail h1 { margin:0 0 8px; font-size:15px; font-weight:700; letter-spacing:.02em; }
#rail h1 small { color:var(--dim); font-weight:400; }
#q { width:100%; padding:7px 10px; border-radius:7px; border:1px solid var(--line); background:var(--bg); color:var(--txt); outline:none; }
#q:focus { border-color:var(--accent); }
+
+ /* ── Controls bar (lives inside rail header, below search) ── */
+ #ctrlbar {
+ display:flex; align-items:center; gap:6px; flex-wrap:wrap;
+ margin-top:8px; padding-top:8px; border-top:1px solid var(--line);
+ }
+ /* Sort select */
+ #sortSel {
+ flex:1 1 auto; min-width:0;
+ padding:5px 6px; border-radius:6px; border:1px solid var(--line);
+ background:var(--bg); color:var(--txt); font-size:11.5px; cursor:pointer;
+ outline:none;
+ }
+ #sortSel:focus { border-color:var(--accent); }
+ /* List / Grid seg */
+ .seg { display:flex; border:1px solid var(--line); border-radius:6px; overflow:hidden; flex:none; }
+ .seg button {
+ padding:5px 9px; background:var(--bg); border:none; font-size:11px;
+ letter-spacing:.06em; text-transform:uppercase; cursor:pointer; color:var(--dim);
+ }
+ .seg button.on { background:var(--accent); color:#0e1116; font-weight:600; }
+ .seg button:hover:not(.on) { color:var(--accent); }
+ /* Density slider */
+ .dens-wrap { display:flex; align-items:center; gap:5px; flex:none; }
+ .dens-wrap label { font-size:10px; letter-spacing:.1em; text-transform:uppercase; color:var(--dim); white-space:nowrap; }
+ #densSlider { accent-color:var(--accent); width:60px; }
+ /* Fields menu */
+ .fieldmenu { position:relative; flex:none; }
+ #fieldsBtn {
+ padding:5px 9px; border-radius:6px; border:1px solid var(--line);
+ background:var(--bg); color:var(--dim); font-size:11px;
+ letter-spacing:.06em; text-transform:uppercase; cursor:pointer;
+ }
+ #fieldsBtn:hover, #fieldsBtn[aria-expanded="true"] { border-color:var(--accent); color:var(--accent); }
+ .fpop {
+ position:absolute; top:calc(100% + 6px); right:0;
+ background:var(--panel); border:1px solid var(--line); border-radius:8px;
+ box-shadow:0 14px 34px rgba(0,0,0,.45); padding:6px 4px; z-index:50; min-width:160px;
+ }
+ .fpop[hidden] { display:none; }
+ .fpop label {
+ display:flex; align-items:center; gap:8px; padding:6px 12px;
+ font-size:11.5px; color:var(--txt); cursor:pointer; white-space:nowrap; border-radius:5px;
+ }
+ .fpop label:hover { background:#1e2530; }
+ .fpop input[type=checkbox] { accent-color:var(--accent); }
+
+ /* ── List (default) ── */
#list { flex:1; overflow-y:auto; padding:6px; }
- .item { display:flex; align-items:center; gap:8px; padding:7px 9px; border-radius:7px; cursor:pointer; color:var(--txt); user-select:none; }
+ .item {
+ display:flex; align-items:center; gap:8px;
+ padding:var(--row-py) var(--row-px); border-radius:7px; cursor:pointer;
+ color:var(--txt); user-select:none; font-size:var(--row-fs);
+ }
.item:hover { background:#1e2530; }
.item.active { background:#213132; outline:1px solid var(--accent); }
.dot { width:8px; height:8px; border-radius:50%; flex:none; background:#555; }
@@ -28,8 +86,31 @@
.name { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.name b { color:var(--accent); font-weight:600; }
.ms { color:var(--dim); font-size:11px; flex:none; }
+
+ /* ── Grid (tile) mode on #list ── */
+ #list.grid {
+ display:flex; flex-wrap:wrap; gap:6px; padding:8px 6px; align-content:flex-start;
+ }
+ #list.grid .item {
+ flex-direction:column; align-items:center; justify-content:center;
+ min-width:var(--rail-tile-min); flex:1 1 var(--rail-tile-min);
+ padding:10px 8px; text-align:center; gap:4px; border:1px solid var(--line);
+ border-radius:8px; background:#1a1f27;
+ }
+ #list.grid .item.active { background:#213132; }
+ #list.grid .item:hover { background:#1e2530; border-color:var(--accent); }
+ #list.grid .name { font-size:11px; white-space:normal; word-break:break-all; line-height:1.3; }
+ #list.grid .ms { font-size:10px; }
+
+ /* ── Per-field body-class hide switches ── */
+ body.hf-dot [data-f="dot"] { display:none; }
+ body.hf-name [data-f="name"] { display:none; }
+ body.hf-ms [data-f="ms"] { display:none; }
+
+ /* ── Foot / status legend ── */
#foot { padding:8px 14px; border-top:1px solid var(--line); color:var(--dim); font-size:11.5px; }
+ /* ── Right main pane ── */
#main { flex:1; display:flex; flex-direction:column; min-width:0; }
#bar { display:flex; align-items:center; gap:10px; padding:8px 12px; border-bottom:1px solid var(--line); background:var(--panel); }
#cur { font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
@@ -47,6 +128,31 @@
<header>
<h1>agentabrams.com <small id="count"></small></h1>
<input id="q" type="search" placeholder="Filter domains…" autocomplete="off">
+
+ <!-- Controls bar: sort + view toggle + density + fields -->
+ <div id="ctrlbar">
+ <select id="sortSel" aria-label="Sort domains">
+ <option value="az">Name A→Z</option>
+ <option value="za">Name Z→A</option>
+ <option value="up_first">Status: Up first</option>
+ <option value="down_first">Status: Down first</option>
+ <option value="auth_type">Auth type (grouped)</option>
+ <option value="ms_asc">Response time ↑ (fastest)</option>
+ <option value="ms_desc">Response time ↓ (slowest)</option>
+ </select>
+ <div class="seg" role="tablist" aria-label="View mode">
+ <button id="vList" class="on" title="List view">☰</button>
+ <button id="vGrid" title="Grid / tile view">⊞</button>
+ </div>
+ <div class="dens-wrap" title="Adjust density">
+ <label for="densSlider">▤</label>
+ <input type="range" id="densSlider" min="1" max="10" step="1" value="5" aria-label="Density">
+ </div>
+ <div class="fieldmenu">
+ <button id="fieldsBtn" aria-haspopup="true" aria-expanded="false">Fields ▾</button>
+ <div class="fpop" id="fpop" hidden role="menu"></div>
+ </div>
+ </div>
</header>
<div id="list"></div>
<div id="foot">click a domain → loads in the right panel (unified admin pw auto-applied) · dot: <span style="color:#d9a53a">Basic un/pw</span> / <span style="color:#4a90e2">login·SSO</span> / <span style="color:#3fbf6f">open (no auth)</span> / <span style="color:#d95555">down or pw rejected</span></div>
@@ -63,23 +169,133 @@
</div>
<script>
+// ── Escape untrusted text before it touches innerHTML ──
+const esc = s => String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
+
+// ── State ──────────────────────────────────────────────
+// Every control persists under the existing aav- prefix.
let domains = [], status = {}, current = localStorage.getItem('aav-current') || '';
-const list = document.getElementById('list'), q = document.getElementById('q');
-const frame = document.getElementById('frame'), empty = document.getElementById('empty');
-const cur = document.getElementById('cur'), count = document.getElementById('count');
-// Escape untrusted text before it touches innerHTML (search filter + domain labels).
-const esc = s => String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
+const FIELDS = [
+ ['dot', 'Status dot', true],
+ ['name', 'Domain', true],
+ ['ms', 'Response ms', true],
+];
+
+function loadFields() {
+ const def = {}; FIELDS.forEach(([k,,d]) => def[k] = d);
+ try { return Object.assign(def, JSON.parse(localStorage.getItem('aav-fields') || '{}')); }
+ catch(e) { return def; }
+}
+
+const ctrlState = {
+ sort: localStorage.getItem('aav-sort') || 'az',
+ view: localStorage.getItem('aav-view') || 'list',
+ density: parseInt(localStorage.getItem('aav-density') || '5', 10),
+ fields: loadFields(),
+};
+
+// ── Element refs ───────────────────────────────────────
+const list = document.getElementById('list');
+const q = document.getElementById('q');
+const frame = document.getElementById('frame');
+const empty = document.getElementById('empty');
+const cur = document.getElementById('cur');
+const count = document.getElementById('count');
+const sortSel = document.getElementById('sortSel');
+const densSlider = document.getElementById('densSlider');
+const vListBtn = document.getElementById('vList');
+const vGridBtn = document.getElementById('vGrid');
+const fieldsBtn = document.getElementById('fieldsBtn');
+const fpop = document.getElementById('fpop');
+// ── Sorters ────────────────────────────────────────────
+// authType priority for grouping: open → basic → login → down → unknown
+const authOrder = t => ({ open:0, basic:1, login:2, down:3 }[t] ?? 4);
+
+function statusScore(s) {
+ // up-first: 0=up, 1=down; for down_first flip
+ if (!s) return 2;
+ if (s.authFail || s.authType === 'down' || !s.up) return 1;
+ return 0;
+}
+
+const SORTERS = {
+ az: (a, b) => a.localeCompare(b),
+ za: (a, b) => b.localeCompare(a),
+ up_first: (a, b) => statusScore(status[a]) - statusScore(status[b]) || a.localeCompare(b),
+ down_first:(a, b) => statusScore(status[b]) - statusScore(status[a]) || a.localeCompare(b),
+ auth_type: (a, b) => {
+ const sa = status[a], sb = status[b];
+ const oa = authOrder(sa ? sa.authType : null);
+ const ob = authOrder(sb ? sb.authType : null);
+ return oa - ob || a.localeCompare(b);
+ },
+ ms_asc: (a, b) => {
+ const ma = status[a] && status[a].up ? status[a].ms : Infinity;
+ const mb = status[b] && status[b].up ? status[b].ms : Infinity;
+ return ma - mb || a.localeCompare(b);
+ },
+ ms_desc: (a, b) => {
+ const ma = status[a] && status[a].up ? status[a].ms : -Infinity;
+ const mb = status[b] && status[b].up ? status[b].ms : -Infinity;
+ return mb - ma || a.localeCompare(b);
+ },
+};
+
+// ── View + density helpers ─────────────────────────────
+function applyDensity(v) {
+ // List mode: remap 1-10 → generous padding (1=compact, 10=loose)
+ // Density 5 = baseline (7px/9px padding, 14px font — the original values)
+ const py = Math.round(3 + (v - 1) * 1.2) + 'px'; // 3px … 14px
+ const px = Math.round(5 + (v - 1) * 1.4) + 'px'; // 5px … 18px
+ const fs = Math.round(11 + (v - 1) * 0.5) + 'px'; // 11px … 16px
+ document.documentElement.style.setProperty('--row-py', py);
+ document.documentElement.style.setProperty('--row-px', px);
+ document.documentElement.style.setProperty('--row-fs', fs);
+ // Grid mode: tile min-width 80px (compact) → 180px (large)
+ const tileMin = Math.round(80 + (v - 1) * 11) + 'px'; // 80px … 179px
+ document.documentElement.style.setProperty('--rail-tile-min', tileMin);
+}
+
+function setView() {
+ const isGrid = ctrlState.view === 'grid';
+ list.classList.toggle('grid', isGrid);
+ vListBtn.classList.toggle('on', !isGrid);
+ vGridBtn.classList.toggle('on', isGrid);
+}
+
+function applyFields() {
+ FIELDS.forEach(([k]) => document.body.classList.toggle('hf-' + k, !ctrlState.fields[k]));
+}
+
+function buildFieldMenu() {
+ fpop.innerHTML = FIELDS.map(([k, label]) =>
+ `<label><input type="checkbox" data-field="${esc(k)}"${ctrlState.fields[k] ? ' checked' : ''}> ${esc(label)}</label>`
+ ).join('');
+ fpop.querySelectorAll('input').forEach(cb => cb.addEventListener('change', () => {
+ ctrlState.fields[cb.dataset.field] = cb.checked;
+ localStorage.setItem('aav-fields', JSON.stringify(ctrlState.fields));
+ applyFields();
+ }));
+}
+
+// ── Render ─────────────────────────────────────────────
function render() {
const f = q.value.trim().toLowerCase();
- const shown = domains.filter(d => d.includes(f));
+ let shown = domains.filter(d => d.includes(f));
+
+ // Sort
+ const sorter = SORTERS[ctrlState.sort] || SORTERS.az;
+ shown = [...shown].sort(sorter);
+
count.textContent = `(${shown.length}/${domains.length})`;
list.innerHTML = '';
for (const d of shown) {
const s = status[d];
const row = document.createElement('div');
row.className = 'item' + (d === current ? ' active' : '');
+
// authType: basic (amber) | login (blue) | open (green) | down/authFail (red).
// Falls back to the old gated/up logic if authType is absent.
const dotCls = !s ? '' : s.authFail ? 'down'
@@ -95,13 +311,20 @@ function render() {
: s.authType === 'down' ? 'unreachable' : '';
const ed = esc(d);
const label = f ? ed.replace(esc(f), m => `<b>${m}</b>`) : ed;
- row.innerHTML = `<span class="dot ${dotCls}" title="${tip}"></span><span class="name">${label}</span>` +
- (s && s.up ? `<span class="ms">${s.ms}ms</span>` : s ? `<span class="ms">—</span>` : '');
+
+ // Each sub-element carries data-f so field-hide CSS rules apply.
+ row.innerHTML =
+ `<span class="dot ${esc(dotCls)}" data-f="dot" title="${esc(tip)}"></span>` +
+ `<span class="name" data-f="name">${label}</span>` +
+ (s && s.up
+ ? `<span class="ms" data-f="ms">${s.ms}ms</span>`
+ : s ? `<span class="ms" data-f="ms">—</span>` : '');
row.onclick = () => select(d);
list.appendChild(row);
}
}
+// ── Select (iframe load) — core behavior preserved ─────
function select(d) {
current = d;
localStorage.setItem('aav-current', d);
@@ -114,10 +337,60 @@ function select(d) {
render();
}
+// ── Wire controls ──────────────────────────────────────
document.getElementById('reload').onclick = () => { if (current) frame.src = '/site/' + current + '/'; };
document.getElementById('newtab').onclick = () => { if (current) window.open('https://' + current, '_blank', 'noopener'); };
q.oninput = render;
+// Sort
+sortSel.value = ctrlState.sort;
+sortSel.addEventListener('change', () => {
+ ctrlState.sort = sortSel.value;
+ localStorage.setItem('aav-sort', ctrlState.sort);
+ render();
+});
+
+// View toggle
+vListBtn.onclick = () => {
+ ctrlState.view = 'list';
+ localStorage.setItem('aav-view', 'list');
+ setView();
+};
+vGridBtn.onclick = () => {
+ ctrlState.view = 'grid';
+ localStorage.setItem('aav-view', 'grid');
+ setView();
+};
+
+// Density
+densSlider.value = ctrlState.density;
+applyDensity(ctrlState.density);
+densSlider.addEventListener('input', () => {
+ ctrlState.density = parseInt(densSlider.value, 10);
+ localStorage.setItem('aav-density', String(ctrlState.density));
+ applyDensity(ctrlState.density);
+});
+
+// Fields menu
+buildFieldMenu();
+applyFields();
+fieldsBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const open = fpop.hidden;
+ fpop.hidden = !open;
+ fieldsBtn.setAttribute('aria-expanded', String(open));
+});
+document.addEventListener('click', (e) => {
+ if (!fpop.hidden && !e.target.closest('.fieldmenu')) {
+ fpop.hidden = true;
+ fieldsBtn.setAttribute('aria-expanded', 'false');
+ }
+});
+
+// Apply initial view + density
+setView();
+
+// ── Data fetch ─────────────────────────────────────────
(async () => {
try {
const dres = await fetch('/api/domains');
← 6f754cb retire rentv-website: drop stale domain reference (TK-10123,
·
back to Agentabrams Viewer
·
creds-safe fetch guard: resolve relative fetch vs credential 9a406b8 →