Repository navigation
The Dashboard
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.