← back to Agent Cabinet
calmer overlay defaults (Shared+Suggested off) with localStorage persistence + UI smoke harness
183cbbfccb87fbaecbcc7c050cd40626feab8e8e · 2026-06-18 18:56:43 -0700 · SteveStudio2
Files touched
M pyramid.htmlA scripts/check-pyramid-syntax.jsA scripts/smoke-pyramid-ui.py
Diff
commit 183cbbfccb87fbaecbcc7c050cd40626feab8e8e
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Thu Jun 18 18:56:43 2026 -0700
calmer overlay defaults (Shared+Suggested off) with localStorage persistence + UI smoke harness
---
pyramid.html | 17 +++--
scripts/check-pyramid-syntax.js | 14 ++++
scripts/smoke-pyramid-ui.py | 156 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 182 insertions(+), 5 deletions(-)
diff --git a/pyramid.html b/pyramid.html
index cccd5c3..86aa66a 100644
--- a/pyramid.html
+++ b/pyramid.html
@@ -673,17 +673,24 @@ function applyToggles(){
const gx=document.getElementById('g-gaps'); if(gx) gx.style.display = on('tg-gap')?'':'none';
const ghc=document.getElementById('gap-handles'); if(ghc) ghc.style.display = on('tg-gap')?'':'none';
}
+// Calmer default — Hierarchy + Gaps on, Shared + Suggested overlays off until the
+// user opts in. Persisted to localStorage so the choice survives reload.
+const TOGGLE_DEFAULTS={'tg-hier':true,'tg-shared':false,'tg-sug':false,'tg-gap':true};
+const TOGGLE_LS='cabinet.toggles';
+function loadToggles(){ try{ const s=JSON.parse(localStorage.getItem(TOGGLE_LS)||'{}'); return {...TOGGLE_DEFAULTS,...s}; }catch(e){ return {...TOGGLE_DEFAULTS}; } }
+function saveToggles(){ const o={}; ['tg-hier','tg-shared','tg-sug','tg-gap'].forEach(id=>{ const e=document.getElementById(id); if(e) o[id]=e.checked; }); try{ localStorage.setItem(TOGGLE_LS,JSON.stringify(o)); }catch(e){} }
function buildToolbar(){
SHARED=computeShared();
+ const t=loadToggles(), ck=id=>t[id]?' checked':'';
document.getElementById('toolbar').innerHTML =
- `<label><input type="checkbox" id="tg-hier" checked> Hierarchy</label>`+
- `<label><input type="checkbox" id="tg-shared" checked><span class="tog-line" style="border-top:2px dashed #cbd5e1"></span>Shared now (${SHARED.length})</label>`+
- `<label><input type="checkbox" id="tg-sug" checked><span class="tog-line" style="border-top:2px dotted #fbbf24"></span>Suggested moves (${SUGGEST.length})</label>`+
- `<label><input type="checkbox" id="tg-gap" checked><span class="tog-line" style="border-top:2px dotted #f87171"></span>Gaps (${GAPS.length})</label>`+
+ `<label><input type="checkbox" id="tg-hier"${ck('tg-hier')}> Hierarchy</label>`+
+ `<label><input type="checkbox" id="tg-shared"${ck('tg-shared')}><span class="tog-line" style="border-top:2px dashed #cbd5e1"></span>Shared now (${SHARED.length})</label>`+
+ `<label><input type="checkbox" id="tg-sug"${ck('tg-sug')}><span class="tog-line" style="border-top:2px dotted #fbbf24"></span>Suggested moves (${SUGGEST.length})</label>`+
+ `<label><input type="checkbox" id="tg-gap"${ck('tg-gap')}><span class="tog-line" style="border-top:2px dotted #f87171"></span>Gaps (${GAPS.length})</label>`+
`<button class="tb-btn" onclick="openIdeasGallery()">💡 Ideas</button>`+
`<button class="tb-btn" style="margin-left:0" onclick="location.href='/activity'">🕓 Activity</button>`+
`<button class="tb-btn" style="margin-left:0" onclick="showAnalysis()">▤ List</button>`;
- ['tg-hier','tg-shared','tg-sug','tg-gap'].forEach(id=>document.getElementById(id).addEventListener('change',applyToggles));
+ ['tg-hier','tg-shared','tg-sug','tg-gap'].forEach(id=>document.getElementById(id).addEventListener('change',()=>{ saveToggles(); applyToggles(); }));
}
function offName(id){ const o=OFF.find(x=>x.id===id); return o?o.nm.replace('VP ',''):id; }
function showAnalysis(){
diff --git a/scripts/check-pyramid-syntax.js b/scripts/check-pyramid-syntax.js
new file mode 100644
index 0000000..aacfe1c
--- /dev/null
+++ b/scripts/check-pyramid-syntax.js
@@ -0,0 +1,14 @@
+// Extracts the inline <script> from pyramid.html and validates it parses.
+// Exits non-zero on a syntax error. Run: node scripts/check-pyramid-syntax.js
+const fs = require('fs');
+const path = require('path');
+const html = fs.readFileSync(path.join(__dirname, '..', 'pyramid.html'), 'utf8');
+const m = html.match(/<script>([\s\S]*?)<\/script>/);
+if (!m) { console.error('no <script> block found'); process.exit(1); }
+try {
+ new Function(m[1]);
+ console.log('pyramid.html inline script: SYNTAX OK');
+} catch (e) {
+ console.error('SYNTAX ERROR in pyramid.html inline script:', e.message);
+ process.exit(1);
+}
diff --git a/scripts/smoke-pyramid-ui.py b/scripts/smoke-pyramid-ui.py
new file mode 100644
index 0000000..d8b3538
--- /dev/null
+++ b/scripts/smoke-pyramid-ui.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python3
+"""Persistent UI smoke test for the Agent Cabinet pyramid viewer.
+
+Drives a real headless Chromium against the running server and ASSERTS that each
+hardened control actually does its job (DOM re-renders, drawer focus, toggle
+visibility, persistence) — not merely that the page loads.
+
+Usage: python3 scripts/smoke-pyramid-ui.py [BASE_URL]
+ BASE_URL defaults to http://127.0.0.1:9766
+Exits non-zero on the first failed assertion.
+"""
+import sys
+import urllib.request
+from playwright.sync_api import sync_playwright, expect
+
+BASE = (sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:9766").rstrip("/")
+PY = BASE + "/pyramid"
+passed = []
+failed = []
+
+
+def ok(name):
+ passed.append(name)
+ print(f" PASS {name}")
+
+
+def bad(name, detail=""):
+ failed.append(name)
+ print(f" FAIL {name} {detail}")
+
+
+def check_redirects():
+ # P1-0: /pyramid.html and /pyramid/ must 301 -> /pyramid (no auto-follow).
+ for path in ("/pyramid.html", "/pyramid/"):
+ req = urllib.request.Request(BASE + path, method="GET")
+ opener = urllib.request.build_opener(NoRedirect())
+ try:
+ opener.open(req)
+ bad(f"301 {path}", "expected redirect, got 2xx")
+ except urllib.error.HTTPError as e:
+ if e.code == 301 and e.headers.get("Location") == "/pyramid":
+ ok(f"301 {path} -> /pyramid")
+ else:
+ bad(f"301 {path}", f"code={e.code} loc={e.headers.get('Location')}")
+
+
+class NoRedirect(urllib.request.HTTPRedirectHandler):
+ def redirect_request(self, *a, **k):
+ return None
+
+
+def run_browser():
+ with sync_playwright() as p:
+ browser = p.chromium.launch(headless=True)
+ ctx = browser.new_context(viewport={"width": 1400, "height": 900})
+ page = ctx.new_page()
+ page.goto(PY)
+ page.wait_for_load_state("networkidle")
+ page.wait_for_timeout(400) # let the double-rAF redraw settle
+
+ # ---- P1: overlay default state (calmer) ----
+ # Hierarchy + Gaps default ON; Shared + Suggested default OFF.
+ assert page.locator("#tg-hier").is_checked(), "hierarchy should default on"
+ ok("default tg-hier ON")
+ assert page.locator("#tg-gap").is_checked(), "gaps should default on"
+ ok("default tg-gap ON")
+ assert not page.locator("#tg-shared").is_checked(), "shared should default off"
+ ok("default tg-shared OFF")
+ assert not page.locator("#tg-sug").is_checked(), "suggested should default off"
+ ok("default tg-sug OFF")
+
+ # The suggested overlay group must actually be hidden when its toggle is off.
+ gsug = page.locator("#g-suggest")
+ if gsug.count():
+ disp = gsug.evaluate("e => getComputedStyle(e).display")
+ assert disp == "none", f"g-suggest display={disp}, expected none"
+ ok("g-suggest hidden by default")
+ # Gap lines visible by default.
+ ggaps = page.locator("#g-gaps")
+ if ggaps.count():
+ disp = ggaps.evaluate("e => getComputedStyle(e).display")
+ assert disp != "none", "g-gaps should be visible by default"
+ ok("g-gaps visible by default")
+
+ # Toggling Suggested ON must reveal the group (DOM re-render assertion).
+ page.locator("#tg-sug").check()
+ page.wait_for_timeout(150)
+ disp = page.locator("#g-suggest").evaluate("e => getComputedStyle(e).display")
+ assert disp != "none", "g-suggest should show after toggle on"
+ ok("toggle tg-sug ON reveals g-suggest")
+
+ # ---- P1: persistence across reload ----
+ page.reload()
+ page.wait_for_load_state("networkidle")
+ page.wait_for_timeout(300)
+ assert page.locator("#tg-sug").is_checked(), "tg-sug should persist ON after reload"
+ ok("tg-sug persists ON after reload")
+ # restore default for a clean state
+ page.locator("#tg-sug").uncheck()
+
+ # ---- P1: gap handle size + actionable ghost node ----
+ gh = page.locator(".gap-h").first
+ gh.wait_for(state="attached", timeout=5000)
+ box = gh.bounding_box()
+ assert box and box["width"] >= 27, f"gap handle too small: {box['width'] if box else None}px"
+ ok(f"gap handle >=28px ({round(box['width'])}px)")
+
+ # ---- P1: drawer focus management on gap open ----
+ gh.scroll_into_view_if_needed()
+ gh.click()
+ page.wait_for_timeout(350)
+ assert "open" in (page.locator("#drawer").get_attribute("class") or ""), "drawer should open"
+ ok("gap handle opens drawer")
+ focused = page.evaluate("document.activeElement && document.activeElement.id")
+ assert focused in ("dr-run", "dr-open"), f"focus on primary action expected, got {focused}"
+ ok(f"drawer focuses primary action ({focused})")
+
+ # ---- P1: Approve dominant / Dismiss secondary + renamed ----
+ dismiss_txt = page.locator("#dr-open").inner_text()
+ assert "Dismiss" in dismiss_txt, f"dismiss button should say Dismiss, got '{dismiss_txt}'"
+ ok(f"dismiss renamed ('{dismiss_txt.strip()}')")
+
+ # ---- P0 regression: Escape closes + focus restore ----
+ page.keyboard.press("Escape")
+ page.wait_for_timeout(250)
+ assert "open" not in (page.locator("#drawer").get_attribute("class") or ""), "Escape should close drawer"
+ ok("Escape closes drawer")
+
+ # ---- P2: zoom-to-fit control exists & redraws ----
+ if page.locator("#zoom-fit").count():
+ page.locator("#zoom-fit").click()
+ page.wait_for_timeout(200)
+ ok("zoom-fit control present & clickable")
+
+ # ---- P2: gaps-state-unavailable indicator wiring exists ----
+ if page.locator("#state-warn").count():
+ ok("state-warn indicator element present")
+
+ browser.close()
+
+
+def main():
+ print(f"Smoke test: {PY}")
+ check_redirects()
+ try:
+ run_browser()
+ except AssertionError as e:
+ bad("browser-assertion", str(e))
+ except Exception as e:
+ bad("browser-exception", str(e))
+ print(f"\n{len(passed)} passed, {len(failed)} failed")
+ sys.exit(1 if failed else 0)
+
+
+if __name__ == "__main__":
+ main()
← 6f9f1ff redirect /pyramid.html and /pyramid/ to canonical /pyramid (
·
back to Agent Cabinet
·
enlarge gap/suggestion handles to 30px with hover/focus labe 6fb7695 →