Skip to content

The Dashboard

Josh edited this page Nov 5, 2026 · 1 revision

Describes robot-council/core v0.8.3.

A signed-in developer reaches the fleet's state through eight pages, each behind the same access list and framing refusal as the verification page:

path shows
{prefix}/dashboard the fleet's totals, and the way in to the rest
{prefix}/dashboard/agents the agent sessions, each linking to the locks it holds
{prefix}/dashboard/locks the named locks, each linking to the session holding it
{prefix}/dashboard/lanes the lane board: each lane's state and what it is on, pull requests by repository, and backlog meters
{prefix}/dashboard/queue the task board
{prefix}/dashboard/feed the change feed
{prefix}/dashboard/seats the signed-in developer's own seats, assignment hours and days off
{prefix}/dashboard/administration the installations -- admins only

{prefix}/dashboard/presence, where agents and locks shared one page before #308, answers with a permanent redirect to {prefix}/dashboard/agents, so a bookmark to it still lands.

Each page is its own, so each one pays only for what it shows. The administration page refuses a non-admin from the component rather than from the route, so a direct visit answers 403 whether or not it was linked; a host adding its own path-based gate in robot-council.routes.web_middleware still sees every one of these paths.

The pages are Livewire components and refresh by polling every robot-council.dashboard.poll_seconds seconds, defaulting to 5 and bounded to 1..3600. There is no broadcasting: a change an agent commits is visible within one interval and no sooner.

The lane board is rendered from measured state, never typed. A lane's State is one of Working, Idle, Parked, Blocked and not observed, derived each time -- a lane holding no task is never Working, a lane held for nothing_startable is Idle rather than Blocked -- and Parked is the same rule a placement refuses on. Each lane shows its occupancy as held over capacity, such as 2 / 3, and a working lane lists every ticket it holds, each with its sub-label where the lane gave one. Watcher is read from the bridge watcher's own heartbeat, POST {prefix}/api/agent/watcher, which no other request refreshes: absent until it reports, alive within presence.watcher_stale_after_seconds (90), stale with its age after that, and unknown, re-read past fifteen minutes. A gate -- a session in the ci role -- reports the pull request it is validating with POST {prefix}/api/gates/run and { "pull_request": "owner/name#N" } (or gate_start), and finishes with DELETE on the same path (or gate_finish); GitHub reporting that pull request closed or merged ends the run too. The board marks it running and counts each repository's queue: open, not a draft, and no gate on it. A backlog count nobody has measured renders as a dash, never a number. Stamps are shown in robot-council.dashboard.timezone (UTC by default).

{prefix} itself answers a 302 to the dashboard, so the prefix the package is mounted under does not lead nowhere while the site root leads somewhere. Like the stylesheet below it, that route sits outside the web middleware group and outside the access list: it reads nothing and decides nothing, so a visitor being sent elsewhere has no session written for them. It is documented here rather than in the table above because the two sentences around that table -- the access list, the framing refusal, and a host's own gate in robot-council.routes.web_middleware -- are true of those seven pages and not of this redirect.

It is not registered when the prefix resolves to /, whether the host configured an empty string or a bare slash. That path belongs to the host, and a host serving the console at its root has already routed it. Every other path above moves with the prefix; robot-council.routes.api_prefix is a separate key and does not.

A host that has cached its routes keeps the old 404 at {prefix} until it re-runs route:cache, for the same reason mcp:inspector will not list the server in that state: Laravel skips a package's route files when a cached collection exists.

The stylesheet is compiled here and served by the package, at {prefix}/dashboard.css. A consuming application runs no asset build and needs no Node toolchain. That route is deliberately public and deliberately outside the web middleware group, so it starts no session and a page can load its styling before anyone has signed in.

Its one script is served the same way, at {prefix}/dashboard.js (#472). It keeps the row a reader is looking at in place when a poll adds rows above it, through Livewire's documented morph and morphed hooks, and it takes away a developer's GitHub picture that failed to load, so the initial drawn beneath it shows (#410). It does nothing else. It is written by hand and shipped as it is, so there is still nothing to build or publish. It is loaded by src from the page's own origin and no view carries an inline script, so a host's script-src 'self' covers this file. That is a statement about this file alone: Livewire and Alpine have their own needs under a CSP, which Livewire's csp_safe setting governs. Every page in the dashboard layout loads both, the enrollment page included (#521).

Developers' pictures load from GitHub, in the browser (#410). Each is the avatar URL GitHub gave at sign-in, shown only when it is on https://avatars.githubusercontent.com; core itself requests nothing to show one. A host that sends a Content-Security-Policy with an img-src has to allow that origin, or every avatar falls back to the developer's initial. The package's own header, frame-ancestors 'none', restricts no image.

A repository's picture is its owner's avatar (#416), on the same origin and under the same rule. GitHub gives a repository no picture of its own, so every repository of one user or organization shows that account's avatar, ringed so that it does not read as a developer's face. Core learns it from the webhook: every delivery carries its repository's owner, and the avatar is stored when it changes. A repository whose owner has sent no delivery shows the repository's initial until one does.

Both files are served public for a year, and each page names them with ?v= and a hash of the file's bytes, so a release that changes either one is a new URL rather than a stale cache.

Installing this package adds livewire/livewire to a host's dependencies, and Livewire registers its own /livewire/update endpoint and a global middleware. The package registers EnsureAllowlistedDeveloper as Livewire persistent middleware, because Livewire strips from that endpoint every middleware not on its own fixed list -- without which a developer removed from the access list would keep driving components from a page already open.

Clone this wiki locally