← back to Visual Factory
visual-factory: adopt href-to-deeper-data primitives (TK-10093)
14fabff13e60154d399b0f84b917e8d22267a9b5 · 2026-08-01 20:38:06 -0700 · Steve Abrams
Files touched
A public/drill.jsM public/index.html
Diff
commit 14fabff13e60154d399b0f84b917e8d22267a9b5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Aug 1 20:38:06 2026 -0700
visual-factory: adopt href-to-deeper-data primitives (TK-10093)
---
public/drill.js | 36 +++++++++++++++++++
public/index.html | 105 +++++++++++++++++++++++++++++++++++++++++++++++-------
2 files changed, 128 insertions(+), 13 deletions(-)
diff --git a/public/drill.js b/public/drill.js
new file mode 100644
index 0000000..e63cdd3
--- /dev/null
+++ b/public/drill.js
@@ -0,0 +1,36 @@
+// ── href-to-deeper-data primitives — SINGLE SOURCE OF TRUTH (HARD rule, 2026-07-31) ──
+// Every displayed data point must be an href to its deeper (filtered) data. These are the
+// reusable atoms; a viewer's index.html loads this file, then defines FIELDS, FIL, $, esc,
+// and applyFilters (which these reference at call time via the shared classic-script global
+// scope). Canonical copy: ~/Projects/_shared/web/drill.js → each viewer's public/drill.js.
+// Memory: all-data-points-href-to-deeper-data. Reference viewers: astek-landing, schumacher-internal.
+
+// Build the URL for a given filter set — any count/atom becomes a real, shareable link.
+function qstr(fil, q, pat) {
+ const u = new URLSearchParams();
+ for (const [k] of FIELDS) { if (fil[k]) u.set(k, fil[k]); }
+ if (q) u.set('q', q); if (pat) u.set('pat', pat);
+ const s = u.toString(); return s ? ('?' + s) : location.pathname;
+}
+// Read the URL's filter state back into FIL + the search inputs (deep-link / back-button).
+function readURL() {
+ const u = new URLSearchParams(location.search);
+ for (const [k] of FIELDS) FIL[k] = u.get(k) || '';
+ $('#q').value = u.get('q') || ''; $('#pattern').value = u.get('pat') || '';
+}
+// Reflect the current filter state into the URL (push = new history entry, else replace).
+function writeURL(push) {
+ const url = qstr(FIL, $('#q').value.trim(), $('#pattern').value.trim());
+ history[push ? 'pushState' : 'replaceState']({}, '', url);
+}
+// drill(field,value,label,cls) → an <a> whose href IS the filtered view for that value.
+// cmd/ctrl/middle-click opens it in a new tab; plain click filters in place (delegated listener).
+function drill(field, val, label, cls) {
+ if (val == null || val === '') return '';
+ const href = qstr({ ...FIL, [field]: String(val) }, $('#q').value.trim(), $('#pattern').value.trim());
+ return `<a class="drill ${cls || ''}" href="${href}" data-filter="${field}" data-v="${esc(val)}" title="Filter to ${esc(val)}">${label != null ? esc(label) : esc(val)}</a>`;
+}
+// Atom drill target: SET the filter to this value (drill in), vs pickF which TOGGLES.
+function setF(k, v) { if (FIL[k] === String(v)) return; FIL[k] = String(v); applyFilters('push'); }
+
+window.qstr = qstr; window.readURL = readURL; window.writeURL = writeURL; window.drill = drill; window.setF = setF;
diff --git a/public/index.html b/public/index.html
index 670c12f..2cd42e0 100644
--- a/public/index.html
+++ b/public/index.html
@@ -36,6 +36,12 @@
.tab:hover { color: var(--text); }
.tab.active { background: var(--accent); color: #1b1b1b; border-color: var(--accent); font-weight: 600; }
.tab .count { opacity: 0.65; margin-left: 6px; font-size: 10px; }
+ a.tab { text-decoration: none; }
+ /* drill atom — every on-card data point is an href to its filtered view */
+ .drill { color: inherit; text-decoration: none; cursor: pointer; border-bottom: 1px dotted transparent; }
+ .drill:hover { color: var(--accent); border-bottom-color: var(--accent); }
+ .meta .status.drill:hover, .meta .drill:hover { color: var(--accent); }
+ .drill.dombadge { color: var(--accent); }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; cursor: pointer; transition: border-color 0.15s; }
@@ -127,13 +133,69 @@
</div>
</div>
+<!-- href-to-deeper-data primitives (shared atoms; app-specific adapter is inline below) -->
+<script src="/drill.js"></script>
<script>
// Same-origin: the orchestrator now serves this page, so paths are relative.
const ORCH = '';
const STAGE_NAMES = ['queued','intake','compose','render','vision_check','critic','iterate','catalog','activate'];
let currentFilter = localStorage.getItem('vf:filter') || 'all';
+let domainFilter = ''; // '' = all domains
let lastRuns = [];
+// ── href-to-deeper-data (TK-10093) ───────────────────────────────────────────
+// This is a run/pipeline board: the FILTER MODEL is the status-bucket tabs, and
+// each card carries two drillable data points — its `domain` and its `status`.
+// The shared /drill.js atoms want classic-script globals (FIELDS/FIL/$/esc/
+// applyFilters + qstr/readURL/writeURL); we provide a THIN adapter mapping them
+// onto this board's currentFilter (tab bucket, url param `f`) + domainFilter
+// (url param `domain`), so tabs/status/domain are real, shareable <a href>
+// deep-links and the URL always reflects the active filter (deep-link + back
+// button restore it).
+const $ = s => document.querySelector(s);
+const esc = escapeHtml; // defined later; hoisted fn
+const FIELDS = [['f','Status'],['domain','Domain']];
+const FIL = { f: '', domain: '' };
+function qstr(fil) {
+ const u = new URLSearchParams();
+ if (fil.f && fil.f !== 'all') u.set('f', fil.f);
+ if (fil.domain) u.set('domain', fil.domain);
+ const s = u.toString();
+ return s ? ('?' + s) : location.pathname;
+}
+// drill(field,val,label,cls) → an <a> whose href IS the filtered view for that value.
+function drill(field, val, label, cls) {
+ if (val == null || val === '') return '';
+ const href = qstr({ ...FIL, [field]: String(val) });
+ return `<a class="drill ${cls || ''}" href="${href}" data-filter="${field}" data-v="${escapeHtml(String(val))}" title="Filter to ${escapeHtml(String(val))}">${label != null ? escapeHtml(String(label)) : escapeHtml(String(val))}</a>`;
+}
+function readURL() {
+ const u = new URLSearchParams(location.search);
+ currentFilter = u.get('f') || 'all';
+ domainFilter = u.get('domain') || '';
+ FIL.f = currentFilter; FIL.domain = domainFilter;
+}
+function writeURL(push) {
+ FIL.f = currentFilter; FIL.domain = domainFilter;
+ history[push ? 'pushState' : 'replaceState']({}, '', qstr(FIL));
+}
+// applyFilters(nav) — reflect state → URL + repaint tabs/grid.
+function applyFilters(nav) {
+ FIL.f = currentFilter; FIL.domain = domainFilter;
+ if (nav !== 'none') writeURL(nav === 'push');
+ renderTabs(lastRuns);
+ renderGrid(lastRuns);
+}
+// setF drill target used by delegated atom clicks.
+function setF(k, v) {
+ if (k === 'f') { currentFilter = String(v); localStorage.setItem('vf:filter', currentFilter); }
+ else if (k === 'domain') { domainFilter = domainFilter === String(v) ? '' : String(v); }
+ applyFilters('push');
+}
+window.qstr = qstr; window.drill = drill; window.readURL = readURL; window.writeURL = writeURL;
+window.setF = setF; window.esc = esc; window.applyFilters = applyFilters;
+window.FIELDS = FIELDS; window.FIL = FIL;
+
// Bucket a run into a UI tab. Server statuses are messier than the user
// thinks about them — collapse here so the tabs stay simple.
function bucket(status) {
@@ -160,31 +222,32 @@ function renderTabs(runs) {
const b = bucket(r.status);
if (b in counts) counts[b]++;
}
+ // Each tab is a real <a href> to its filtered view (drill atom on field `f`).
document.getElementById('tabs').innerHTML = TABS.map(t =>
- `<span class="tab ${currentFilter===t.id?'active':''}" data-filter="${t.id}">${t.label}<span class="count">${counts[t.id]||0}</span></span>`
+ `<a class="tab drill ${currentFilter===t.id?'active':''}" data-filter="f" data-v="${t.id}" href="${qstr({ ...FIL, f: t.id })}">${t.label}<span class="count">${counts[t.id]||0}</span></a>`
).join('');
- document.querySelectorAll('.tab').forEach(el => {
- el.addEventListener('click', () => {
- currentFilter = el.dataset.filter;
- localStorage.setItem('vf:filter', currentFilter);
- renderTabs(lastRuns);
- renderGrid(lastRuns);
- });
- });
+ // domain-filter indicator: a clearable pill when a domain drill is active.
+ if (domainFilter) {
+ document.getElementById('tabs').insertAdjacentHTML('beforeend',
+ `<a class="tab drill active" data-filter="domain" data-v="${escapeHtml(domainFilter)}" href="${qstr({ ...FIL, domain: '' })}" title="Clear domain filter">domain: ${escapeHtml(domainFilter)} ✕</a>`);
+ }
}
function renderGrid(runs) {
const grid = document.getElementById('grid');
- const filtered = currentFilter === 'all' ? runs : runs.filter(r => bucket(r.status) === currentFilter);
- if (!filtered.length) { grid.innerHTML = `<div class="empty">No runs in “${currentFilter}”.</div>`; return; }
+ const filtered = runs.filter(r =>
+ (currentFilter === 'all' || bucket(r.status) === currentFilter) &&
+ (!domainFilter || r.domain === domainFilter));
+ if (!filtered.length) { grid.innerHTML = `<div class="empty">No runs in “${escapeHtml(currentFilter)}”${domainFilter ? ' · ' + escapeHtml(domainFilter) : ''}.</div>`; return; }
grid.innerHTML = filtered.map(r => {
const status = String(r.status || '');
const canActivate = status.startsWith('awaiting_activation');
const canRetry = /^(failed|completed_unapproved|unapproved)/.test(status);
const isRunning = status === 'running' || status === 'pending';
const dimText = `${r.width||'?'}×${r.height||'?'}`;
+ // domain badge is a drill atom — click filters the board to that domain.
const domainBadge = r.domain
- ? `<span style="color:var(--accent);font-size:10px;font-weight:600;letter-spacing:0.06em;text-transform:uppercase">${escapeHtml(r.domain)}</span>`
+ ? `<span style="font-size:10px;font-weight:600;letter-spacing:0.06em;text-transform:uppercase">${drill('domain', r.domain, r.domain, 'dombadge')}</span>`
: '';
const subline = r.purpose ? escapeHtml(r.purpose) : escapeHtml(r.brief || '');
const vMatch = status.match(/^awaiting_activation_v(\d+)$/);
@@ -215,7 +278,7 @@ function renderGrid(runs) {
<div class="brief">${subline}</div>
${badges.length ? `<div class="badges">${badges.join('')}</div>` : ''}
${errsnip}
- <div class="row"><span class="status ${status}">${escapeHtml(status)}</span></div>
+ <div class="row"><span class="status ${status}">${drill('f', bucket(status), status)}</span></div>
${actions.length ? `<div class="actions-row">${actions.join('')}</div>` : ''}
</div>
</div>`;
@@ -276,6 +339,21 @@ function renderGrid(runs) {
});
}
+// Delegated drill atom clicks (bound ONCE). Capture phase + stopPropagation so a
+// drill on `domain`/`status` inside a card filters the board WITHOUT also opening
+// the card's modal. Modifier / middle-click keeps the real href (open in new tab).
+function wireDrill(container) {
+ container.addEventListener('click', e => {
+ const a = e.target.closest('a.drill'); if (!a || !container.contains(a)) return;
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button === 1) return;
+ e.preventDefault(); e.stopPropagation();
+ setF(a.dataset.filter, a.dataset.v);
+ }, true);
+}
+wireDrill(document.getElementById('grid'));
+wireDrill(document.getElementById('tabs'));
+addEventListener('popstate', () => { readURL(); applyFilters('none'); });
+
// Codex P2 #14 fix: cancel an in-flight loadRuns / openModal before issuing a
// new one, so an older slow response can't overwrite newer state when the user
// switches filters fast or the 2s modal poll laps the previous request.
@@ -415,6 +493,7 @@ async function refreshModal() {
}
setInterval(refreshModal, 2000);
+readURL(); // deep-link / back-button: hydrate currentFilter + domainFilter before first paint
loadRuns();
loadHealth();
setInterval(loadHealth, 30_000);
← 26fb67d refactor: add .bak/.pre-/.orig 404 guard + broaden .gitignor
·
back to Visual Factory
·
auto-data-snapshot: 2026-09-23T14:48:42 (2 data files) — .en b533446 →