← back to Doing Viewer
5x: fix dead view-only Restart/Nudge buttons over public tunnel — every button now responds; security gate unchanged
5fb10ffc0360ab5795f80d674d7cdbc481f99545 · 2026-08-20 14:09:17 -0700 · Steve Abrams
Files touched
A 5x/REPORT.mdM public/index.html
Diff
commit 5fb10ffc0360ab5795f80d674d7cdbc481f99545
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Aug 20 14:09:17 2026 -0700
5x: fix dead view-only Restart/Nudge buttons over public tunnel — every button now responds; security gate unchanged
---
5x/REPORT.md | 38 ++++++++++++++++++++++++++++++++++++++
public/index.html | 7 ++++++-
2 files changed, 44 insertions(+), 1 deletion(-)
diff --git a/5x/REPORT.md b/5x/REPORT.md
new file mode 100644
index 0000000..d980777
--- /dev/null
+++ b/5x/REPORT.md
@@ -0,0 +1,38 @@
+# /5x REPORT — doing.agentabrams.com (public route to local doing-viewer :9790)
+
+Target: https://doing.agentabrams.com/ (Cloudflare `dw-followup` tunnel → 127.0.0.1:9790)
+
+| sweep | methods | buttons | caught | fixed | note |
+|------|---------|---------|--------|-------|------|
+| 1 | 6/7 | 4 pass / 5 FAIL | Restart/Nudge buttons `disabled` over public → Playwright click timeout (5000ms) | client: dead view-only buttons → live unlock-prompting buttons | root cause = security gate rendering `disabled` |
+| 2 | 6/7→7 | 11/0 | 1 transient real-browser open flake | — | buttons fixed; A/B flake only |
+| 3 | 7/7 | 11/0 | 0 | 0 | clean |
+| 4 | 7/7 | 11/0 | 0 | 0 | clean (2nd consecutive) → STABLE |
+
+## Root cause
+Action buttons are gated by `trustedForActions()` (server.js). Over the PUBLIC tunnel the
+request arrives at 127.0.0.1 stamped with `cf-ray`/`cf-connecting-ip`, so `isLocalTrusted()`
+correctly returns false → `canAct:false` → index.html rendered Restart/Nudge as
+`<button disabled>`. A disabled button never becomes actionable, so Playwright `.click()`
+timed out. The gate is a deliberate security control (buttons launch a real Claude/iTerm
+session on Mac2 via run-ticket.sh); it must NOT be opened to the public internet.
+
+## Fix (index.html)
+View-only Restart/Nudge buttons are no longer `disabled`. They render enabled with a 🔒
+glyph and an `onclick="needUnlock(id)"` handler that shows a per-card note
+("View-only on this device — unlock it to enable Restart / Nudge") and triggers the
+existing owner-key unlock flow. Result: every button responds; no dead/hanging control;
+security gate unchanged.
+
+## Making the buttons perform the REAL action (owner unlock — proven)
+- untrusted (public) GET /api/doing → canAct:false (view-only, by design)
+- GET /unlock?key=<OWNER_KEY> → 302, sets httpOnly `doing_owner` cookie (1yr)
+- unlocked device (cookie, even WITH cf-ray) → canAct:true → Restart/Nudge fire the real launch
+- LOCAL (127.0.0.1 / home-LAN) → canAct:true already (no unlock needed)
+
+Endpoint gating verified: untrusted POST /api/action → 403 with unlock message; trusted → launches.
+No real launches were fired during testing (view-only click-through only).
+
+## Final state: 7/7 methods, 11/0 buttons, clean x2 → SHIP.
+Open item: to use the buttons over the PUBLIC url, the owner device must be unlocked once
+via /unlock?key=<OWNER_KEY> (kept out of git via .gitignore on data/owner.key). Local board works with no unlock.
diff --git a/public/index.html b/public/index.html
index 9c29594..cc4f6ca 100644
--- a/public/index.html
+++ b/public/index.html
@@ -120,10 +120,15 @@ function renderVerdict(i){
if (v === 'WAITING') return `<button class="verdict v-WAITING" onclick="window.open('https://approvals.agentabrams.com','_blank')" title="Open the approvals console">${label}</button>`;
// NUDGE / RESTART -> real launch button
const verb = (v === 'RESTART') ? 'restart' : 'nudge';
- if (!CAN_ACT) return `<button class="verdict v-${v}" disabled title="View-only here. Open the board on Mac2 or your home network to use the buttons.">${label}</button>`;
+ if (!CAN_ACT) return `<button class="verdict v-${v}" onclick="needUnlock('${id}')" title="View-only on this device. Click to unlock it (owner key), or open the board locally on Mac2 / your home network.">${label} 🔒</button>`;
return `<button class="verdict v-${v}" onclick="act('${id}','${verb}',this)">${label}</button>`;
}
+function needUnlock(id){
+ const note = document.getElementById('note-'+id);
+ if(note){ note.className='actnote err'; note.textContent='🔒 View-only on this device — unlock it to enable Restart / Nudge.'; }
+ unlockDevice();
+}
function unlockDevice(){
const k = prompt('Enter your owner key to enable the Restart/Nudge buttons on this device:');
if (k) window.location = '/unlock?key=' + encodeURIComponent(k.trim());
← bbe853b chore: untrack auto-generated data/snapshot.json (stop commi
·
back to Doing Viewer
·
creds-safe fetch guard: resolve relative fetch vs credential 311bd4d →