Skip to content

Regenerate the Launchpad Page

The Launchpad is a single static HTML page listing every managed project with its dev / docs / prod / GitHub links and port band — built to be the browser’s default localhost site. The page is derived state with a single owner (the links index): it is never hand-edited, only regenerated.

Schema and derivation rules live in the Launchpad Links Index reference. Always-on serving is #1245; liveness badges are #1246. How the page looks — the colour tokens for each theme, type, motion, and what each liveness colour means — is set in the Launchpad design brief (#2144).

Terminal window
node ~/.claude/scripts/launchpad/render-page.js --out ~/.claude/launchpad/index.html

That one command derives the links index and renders the page. The project list is this machine’s projects in the fleet feed — the document the one dashboard server, scripts/dashboard/server.js, serves at /fleet.json (#2081) — and each project’s links come from declared state (ports ledger band, projects.json, its dev.json / wrangler config / git remote). With no fleet feed, every ledger project is listed; --ledger on links-index.js forces that. Re-running is always safe: unchanged inputs produce a byte-identical page.

Whenever declared state changes — a project gains a port band, a dev.json, a wrangler route, or a new repo remote:

  • after bob init provisions a new project (port band allocated)
  • after adding or editing a project’s dev.json
  • after a wrangler route / custom domain change
  • after registering a project in projects.json (the operator repo’s derived registry, #3432)

Two-step variant (inspect the index first)

Section titled “Two-step variant (inspect the index first)”
Terminal window
node ~/.claude/scripts/launchpad/links-index.js --out /tmp/links.json # 1. generate
jq '.projects[] | select(.name=="tct")' /tmp/links.json # 2. inspect
node ~/.claude/scripts/launchpad/render-page.js --index /tmp/links.json --out ~/.claude/launchpad/index.html

render-page.js with --index renders a prebuilt index verbatim — useful for diffing what changed before republishing, or for rendering a snapshot from another machine.

Terminal window
open ~/.claude/launchpad/index.html # macOS; or use the always-on server (runbooks/launchpad-serving.md)
make test-launchpad-links test-launchpad-page

In the page: / focuses the filter, typing narrows by project name or stack term, Enter opens the first match’s dev server, Escape clears. Light/dark follows the browser theme.

Dev/docs pills carry a liveness dot — filled = up, red = down, hollow = unknown — painted per page load from the server’s /liveness.json probe. Opened as a plain file (no server), the dots simply stay hollow; see the probe semantics.

The page also applies your layout after it paints — the view and each project’s order and span, from ~/.claude/launchpad/layout.json via the server’s /layout.json (#2139). The rendered file never changes with the layout; a project you have not arranged appends in default order, and as a plain file the page uses the layout the session last saw in localStorage, never erroring. Contract, routes and merge rules: the layout contract.

The header has a view switcher (#2141). Every view shows the same projects from the same page data: switching never reloads anything, and the filter you typed stays applied.

View Shortcut Shows
Tiles 1 The default: a grid of project tiles you can reorder and resize (below).
List 2 One dense row per project — the original Launchpad layout, with the same filter and Enter behaviour.
Board 3 Cards in columns: what is up, what you can start, what is down (#2142). See Board below.
Ports 4 The ports ledger as a band strip: every project’s band, the free ports between them, and any collision (#2143). See Port map below.
Graph 5 Projects as nodes, joined by what they share: stack, mirror host, neighbouring port bands (#2153). See Graph below.
  • Switching. Click a view in the switcher, or press 1–5 anywhere except in the filter box (there the digits are part of your search).
  • Saved. The view you pick is saved as view in ~/.claude/launchpad/layout.json, with your tile arrangement untouched, and the page opens in it next time. As a plain file it is kept in the browser’s localStorage.
  • Force a view for one load. Add ?view=tiles, ?view=list, ?view=board, ?view=portmap or ?view=graph to the URL — for a bookmark or a test. The page opens in that view and saves nothing; your saved view is unchanged until you pick another one. An unknown value is ignored.

Board: what is running, and what can I start

Section titled “Board: what is running, and what can I start”

The board (#2142) shows each project as a card in a column. Each card shows the project name (a link to its dev server), its port band and, where the project can be started from here, a start button. Each column has a count badge. A column with nothing in it stays on the board and says nothing here, so an empty column never looks like missing data.

Choose how the cards are grouped with group by above the board:

Group by Columns
liveness (default) Up — the dev server answers. Startable — it is down, and the project declares a dev start path (dev.json). Down — it is down and cannot be started from here. Unknown — not probed yet, or no dev server declared.
stack One column per stack (the project’s first stack term), then unassigned for projects that declare none.
  • Saved. The choice is saved as board.groupBy in ~/.claude/launchpad/layout.json, next to your view and tile arrangement, and neither one resets the other.
  • Starting a project. Press start on a card in Startable. It is the same start as the red dot in the list: the button pulses starting… while bob dev up runs. A failure shows start failed: … in the card, and the page then checks liveness again, so a project that came up moves to Up.
  • Cards move themselves. Cards are not dragged: the grouping is worked out from live state. When liveness changes, cards slide to their new column (or just land, with reduce motion on).
  • Filter. / filters the board too. The counts show the matching cards.
  • There is no area grouping: no project declares an area yet, and the board does not guess one.

The port map (#2143) draws the ports ledger (ports.json in the operator repo) as one strip, lowest port on the left. You see which bands are taken, where the free ports are and who sits next to whom, without reading JSON. It only shows the ledger: nothing on it can be edited.

On the strip Means
A filled segment One project’s band, labelled with its name and start–end.
The bar at a band’s left edge The band’s base port: the dev server. It turns green while the dev server answers.
A dashed, hollow segment Free ports: no project’s band covers them. A long free run is drawn shorter than its real size, but its label gives the true range.
A striped segment A collision: two or more projects declare bands that share these ports.
  • Links. Hover over a band, or move to it with Tab, to see its dev, docs and dash links with their liveness dots. Press Enter to open its dev server.
  • Collisions. Every collision is also listed in a warning line above the strip, naming both bands and the ports they share. The page never fixes one. Change the band in ports.json (see Port allocation), then regenerate the page.
  • Small screens. Below 600px wide the strip becomes a list: one row per band, with its links shown in the row.
  • Where the data comes from. Only the links index: each project’s base and the index’s band_width, the ledger’s ports per band. The page and the server read no other file.

The graph (#2153) draws every project as a node. A line joins two projects when the declared state says they have something in common. Nothing else is inferred, so a sparse graph is accurate.

Edge Drawn as Joins two projects when
stack solid, blue They share stack terms (node, cloudflare-workers…). More shared terms make a thicker line.
mirror dashed, amber Both mirror to the same host in the operator’s mirrors.json. A project that is not mirrored has none.
ports dotted, grey Their port bands are next to each other in the ledger, touching or overlapping. A free gap between bands breaks the line.
  • Legend. The buttons above the graph show how many edges of each kind there are. Click one to hide that kind, and click it again to show it. Mirror edges are the ones to hide when every project mirrors to one host: they then join everything to everything.
  • Nodes. A node with more links is bigger. A node whose dev server answers is green, slightly larger, and glows. Down and unknown nodes stay plain.
  • Links. Hover over a node, or move to it with Tab, to see its dev, docs and dash links with their liveness dots. Press Enter to open its dev server. Tab goes through the nodes in the index’s order.
  • Pinning. Drag a node to place it. It stays there (a dashed ring marks it), and the others settle around it. The position is saved as graph.pins in ~/.claude/launchpad/layout.json, next to your view, tiles and board choice, and none of them resets the others. Double-click a pinned node to unpin it.
  • Movement. The page loads with the graph already laid out. Pins move their nodes into place and the rest settle around them within two seconds, then nothing moves. With reduce motion on, the graph is drawn already settled.
  • Filter. / dims the nodes that do not match, and every edge that does not touch a match. Nothing is hidden, so the graph keeps its shape. Escape clears it.
  • Screen readers. A hidden list under the graph names each project and what it is joined to, edge kind by edge kind.
  • Where the data comes from. Only the links index: each project’s stack, mirror_host and base, and the index’s band_width. The layout is computed in the page from a fixed seed, so a given index always draws the same graph.

The page opens as a grid of project tiles (#2140). Each tile shows the project name, its port band, the link pills with their liveness dots (the start dot where the project is startable) and its stack terms. Arrange them to suit you; the arrangement is saved and comes back on the next load.

To Do
Move a tile Press its grab handle ⠿ and drag. A dashed placeholder shows where it will land; release to drop it there.
Move a tile from the keyboard Focus the tile (Tab), then press Alt+← or Alt+→.
Widen or narrow a tile Click its resize handle ⇲ to cycle 1 → 2 → 3 columns, or drag the handle sideways by the columns you want. A three-column tile puts all its links on one row.
Find a project Press / and type. Non-matching tiles hide; the saved order is untouched. Enter opens the first visible tile’s dev server, Escape clears.
  • Where it is saved. Served by the Launchpad server, every move or resize is saved to ~/.claude/launchpad/layout.json through POST /action/layout. Opened as a plain file, it is kept in the browser’s localStorage for that browser only. To start over, delete the file (or clear the site’s storage): every tile goes back to default order at one column.
  • Filtered moves. With a filter on, Alt+←/→ steps past the next visible tile; a hidden tile keeps its place.
  • Touch. Drag the grab handle with a finger; the handles never scroll the page. Under 480px wide the grid is one column and dragging still works.
  • Motion. Moves and resizes slide into place in under 200 ms. With the system’s reduce motion setting on, nothing animates; tiles just land.

Verify it with make test-launchpad-page. It pins the tile and switcher markup and the grid rules, and drives the page’s own script for drag, keyboard, resize, filter, reduced motion, the view switcher and the ?view= override; make test-launchpad-board does the same for the board — grouping, columns, the group key, card moves and the start button.

Every view answers the same keys. Outside a text box:

Key Does
⌘K (macOS) / Ctrl+K Open the command palette (below). Works from any view and from inside the filter box.
/ Focus the filter. Type to narrow the projects; Enter opens the first match’s dev server, Escape clears.
1–5 Switch view: Tiles, List, Board, Ports, Graph.
Tab Walk the tiles (or the graph’s nodes).
Alt+← / Alt+→ Move the focused tile.

⌘K / Ctrl+K opens a search box over everything the page can act on (#2155). Type part of a name and act on a match without leaving the keyboard.

Type To find Enter does
A project name: alp The project, then its links Opens its dev server (its first link when it has none)
A name and a link kind: bravo docs That one link (dev, docs, dash, prod, repo, issues, ci, board) Opens the link
A port number: 6313 The link served on that port; a project by its band’s base port Opens the link
A stack term: workers The projects with that term Opens the dev server
start bravo Start a project that declares a dev start path (a dev.json) Starts its dev server, as clicking the red dev dot does; the outcome shows on the project’s tile
A view name: graph The view Switches to it, and saves it as your view
  • Moving. The best match is selected as you type. ↓ / ↑ move the selection, Enter acts on it, and ⌘Enter / Ctrl+Enter opens a link in a new tab. Clicking a match acts on it too.
  • Matching. A match at the start of a name ranks first, then one at the start of a word, then one inside a word, then the letters typed in order anywhere in the name. Equal matches follow your tile order.
  • Dots. Projects and their dev, docs and dash links carry the same liveness dots as the page: green is up, red is down.
  • Closing. Escape (or a click outside the box) closes the palette and puts focus back where it was. Opening and closing never scroll the page or change the view.
  • The filter is separate. The palette searches every project, whatever the filter hides, and leaves the filter as it was. Inside the palette, / and the digits are search text.

Verify it with make test-launchpad-palette (the items and the ranking) and make test-launchpad-palette-page (the palette in a real browser: keys, focus, dots, start, views).

  • Never edit the generated HTML. Fix the declared source (dev.json, wrangler config, ports ledger, projects.json) and regenerate.
  • Nothing is guessed. A project missing a prod URL or docs link means no route / docs block is declared — declare it, then regenerate.
  • The renderer adds no state of its own: no timestamps, no counters. Diffs of the output always reflect a diff of declared inputs.