← 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
M public/index.htmlM server.js
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 →