[object Object]

← back to Desktop Dotbar

dotbar: fix ticket-segment clicks — Electron can't window.open() an external URL; POST to /api/open-board which shells to macOS open (same pattern as reveal)

36f503bd09c803dd627fbc65b569f076208c15c8 · 2026-09-23 10:33:50 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VyZZwbLBMfoBmdLGuxBuD

Files touched

Diff

commit 36f503bd09c803dd627fbc65b569f076208c15c8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 23 10:33:50 2026 -0700

    dotbar: fix ticket-segment clicks — Electron can't window.open() an external URL; POST to /api/open-board which shells to macOS open (same pattern as reveal)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_017VyZZwbLBMfoBmdLGuxBuD
---
 public/index.html |  4 +++-
 server.js         | 17 +++++++++++++++++
 2 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/public/index.html b/public/index.html
index c2ce886..c15e3ed 100644
--- a/public/index.html
+++ b/public/index.html
@@ -134,7 +134,9 @@ const ORIENT_LABEL = { top: '▲ Top', left: '◀ Left', right: '▶ Right' };
 async function fetchDots(){ try { const r = await fetch('/api/dots'); return await r.json(); } catch { return null; } }
 async function fetchTickets(){ try { const r = await fetch('/api/tickets'); return await r.json(); } catch { return null; } }
 // Open the Fleet board filtered to one section (blocked/open/stopped/doing/parked/all).
-function openBoard(section){ try { window.open(`http://127.0.0.1:9794/?section=${section}&layout=grid`, 'fleet'); } catch(e){} }
+// Electron renderers can't window.open() an external URL, so POST to the server, which shells
+// out to macOS `open` (same pattern as reveal() for terminals).
+async function openBoard(section){ try { await fetch('/api/open-board', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({section}) }); } catch(e){} }
 
 function resize(open){
   // Electron shell owns the window bounds via IPC; fall back to resizeTo in a plain browser.
diff --git a/server.js b/server.js
index 27330be..a230a25 100755
--- a/server.js
+++ b/server.js
@@ -256,6 +256,23 @@ const server = http.createServer(async (req, res) => {
       });
       return;
     }
+    // Open the Fleet ticket board (filtered to one section) in the DEFAULT BROWSER. The Electron
+    // renderer can't window.open() an external URL, so it POSTs here and the Node server shells out
+    // to macOS `open`. Section is whitelisted (never interpolate user input into a shell/URL blindly).
+    if (url.pathname === '/api/open-board' && req.method === 'POST') {
+      let raw = '';
+      req.on('data', c => (raw += c));
+      req.on('end', async () => {
+        let section = 'all';
+        try { section = JSON.parse(raw).section || 'all'; } catch {}
+        const OK = ['all', 'blocked', 'open', 'stopped', 'doing', 'parked', 'agents', 'skills', 'nondw', 'done'];
+        if (!OK.includes(section)) section = 'all';
+        const url2 = `http://127.0.0.1:9794/?section=${section}&layout=grid`;
+        const { err } = await run('open', [url2]);   // execFile — args are NOT shell-parsed, so no injection
+        send(res, 200, { ok: !err, section, url: url2 });
+      });
+      return;
+    }
     if (url.pathname === '/' || url.pathname === '/index.html') {
       return send(res, 200, fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8'), 'text/html');
     }

← fa6b41e dotbar: each ticket state (blocked/open/idle/doing/parked) i  ·  back to Desktop Dotbar  ·  Harden ram-sniper toggle + ticket-fetch resilience (TK-12071 25e5616 →