Skip to content

Hub tabs: every instance's dashboard in one page - #99

Merged
hhff merged 9 commits into
mainfrom
hub-tabs
Nov 5, 2026
Merged

hhff merged 9 commits into
mainfrom
hub-tabs

Conversation

@hhff

@hhff hhff commented Nov 5, 2026

Copy link
Copy Markdown
Member

Spec: docs/superpowers/specs/2026-11-05-hub-tabs-design.md. Plan: docs/superpowers/plans/2026-11-05-hub-tabs.md.

What

  • dashboard/hub.html: the hub is now a tab bar.
    • Tabs: one per instance, keyed by root path, labeled with the project name, with a badge when its Inbox count is above 0. The tooltip shows root, kit and fleet state.
    • Frames: selecting a tab shows that instance's own dashboard in a frame at http://127.0.0.1:<port>/. A frame loads on first select and then stays loaded, so it keeps its scroll and notes. A tab follows its instance to a new port.
    • Gone instances: they dim for 30 s, then their tab is removed.
    • URL: the hash (#<name>, or #<name>-<hex6> for twins) keeps the selection. With none, the hub opens the first instance with something in its Inbox.
    • Keyboard: left and right arrows move between tabs.
    • Notify: one "Notify me" for all instances. The hub notifies when an Inbox count rises, and clicking the notification opens that tab.
  • scripts/devteam.mjs: the dashboard page sends frame-ancestors 'self' http://127.0.0.1:<hub> http://localhost:<hub>, and the hub page's CSP gains frame-src listing exactly the 25 instance ports.
  • dashboard/index.html: a framed dashboard (window.top !== window) shows no Notify control and sends no notifications. A cross-origin frame can't ask for permission, and the hub covers it.
  • README and test/kit.test.mjs: tests for both CSPs and the tab bar.

Why

The owner wants one page to switch between every devteam project and see which ones are waiting on them.

Authority

  • The hub page still has no owner powers: GET only, connect-src 'self', its own Host, and frames limited to the instance block.
  • Owner actions in a framed dashboard are the dashboard's own same-origin requests, so their Host, Origin and Sec-Fetch-Site checks are unchanged.
  • This also closes a clickjacking path: before, any site could frame the owner's dashboard, because it sent no frame-ancestors.

Checked

Headless Chrome, two fixture instances on test ports:

  • two tabs, and the instance with an open question shows a badge of 1;
  • the default selection is that instance, with its dashboard rendered in the frame and its own Notify control hidden;
  • /#alpha selects alpha.

Migration

None. Restart the dashboard after the sync. The tabs appear once the dashboard holding 4320 runs this version.

🤖 Generated with Claude Code

hhff and others added 9 commits November 4, 2026 21:37
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ck; a framed dashboard leaves notifications to the hub

The dashboard page sent no frame-ancestors, so any site could frame the owner's dashboard and
steer a click onto an owner action. It now allows only itself and the machine's hub.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…box notification for all

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…boxed frames; duplicate roots; a bad hash

From the PR review:
- A review page's frame-ancestors names the hub too: browsers check every ancestor, so inside a
  tab the review frame was refused.
- The hub has one address, 127.0.0.1: localhost is redirected there, and dashboards let only that
  origin frame them (the hub never binds ::1, which something else could hold).
- Frames are sandboxed (scripts, same-origin, forms, popups, downloads; never top navigation) and
  may write the clipboard for the copy buttons.
- Two answers claiming one root: the tab keeps the port it shows (or the lowest); its tooltip names
  the port. A bad #hash no longer stops selection. One sort order; a tab's frame title follows its
  name; an instance that appears with items already waiting notifies.
- The dashboard checks the kit's version again when main moves and every 10 minutes, so a
  dashboard started before its adoption landed stops showing +local.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kes its first count as the baseline

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… is never closed under it

Node's 5s default keepAliveTimeout closed idle sockets that a client was about to reuse (Node
18's fetch keeps them about 4s), and that request failed with 'fetch failed'. That is the
intermittent dashboard-test failure on macOS + Node 18, and browsers can hit the same race.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hhff

hhff commented Nov 5, 2026

Copy link
Copy Markdown
Member Author

Review (upstream loop). A fresh reviewer agent reviewed it adversarially in two passes.

  • Pass 1, CHANGES: blocker: review pages couldn't load inside the hub, since nested frame-ancestors checks every ancestor. Should-fixes: duplicate roots, a bad hash, an unsandboxed frame, and localhost trusted though the hub never binds ::1. All fixed in be60c17.
  • Pass 2, APPROVE, verified in Chrome: review pages load in a tab and stay locked down nested; owner actions, downloads, popups and copy work in the sandboxed frame; an impostor root is ignored; a bad hash falls back; localhost gets a 302 to 127.0.0.1. A notification nit was fixed in dfeb050.
  • Also on this branch: an old test's exact-CSP assertion was updated (ed28cd1), and Dashboard and hub keep idle connections 65s #101 (keep-alive 65 s) was merged in.
  • Tests at b1a9688: CI 18/20/22/24 green. Local Node 18.20.4, 24.14.1 and 20.19.6: 47/47 each. That's the first clean local Node 18 run since the intermittent dashboard 'fetch failed'; the keep-alive fix ends it.
  • Merging this lands Dashboard and hub keep idle connections 65s #101's commits too.

@hhff
hhff merged commit 74f54ec into main Nov 5, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant