feat(dashboard): make the live board actionable - #67
Conversation
Records what was verified by building and running the app on 2026-09-05: the dashboard renders and the control plane executes runs, but nothing feeds it, nothing refreshes it, and starting it needs a database. The plan restructures the edge rather than the packages, in five phases with a demonstrable exit criterion each. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seeing the dashboard required Postgres, a migration, a signing secret, and control-plane tokens before the first page could render, so the quickest way to look at it was not to. `dev:solo` is `nuxt dev --dotenv .env.solo`: the same app, the same router, and the same `/api/auth/**` endpoints, with Better Auth on the in-memory store the Playwright preview server already uses. `.env.solo` is checked in because it holds nothing worth keeping out of the repository — the session store dies with the process, and the control-plane token is only accepted by a server started this way. It is loaded only when a command names it with `--dotenv`, so `.env` and every deployment are untouched. Verified: `turbo run dev:solo --filter=@code-zero/dashboard` from a checkout with no database — `/api/v1/health` 200, `/login` 200, `POST /api/auth/sign-up/email` returns a session, and `/` renders Control Plane with that cookie. check:repo, format:check, and typecheck pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The trail had two homes it did not need. Recording went through a hand-rolled `AuditRecorder` injected into the RPC context, while `evlog` — already installed, already wrapping both transports — has an audit pipeline with the same shape. Reading lived in a Nitro route outside the router, written when the router could not authenticate a browser session; since it can, that reason is gone. Recording is now `log.audit()` / `log.audit.deny()`. The persisted record is evlog's own `AuditFields` plus a storage id and timestamp, so there is no second audit vocabulary to keep in step, and `auditEnricher` fills in request context (`requestId`, `traceId`, ip, user agent) no call site had been passing. The identity is evlog's deterministic `idempotencyKey`, so a retried delivery lands on the key it already wrote instead of appending a second copy. `auditLogPlugins` carries the record to the same KV-backed store as before, filtered by `auditOnly` and awaited so an audited mutation cannot answer 200 and lose its record, and installed as `EvlogHandlerPlugin` plugins rather than as its `drain`, so a deployment's own request logging is untouched. Reading is `audit.list`, an authenticated procedure gated on a new explicit `Principal.admin` rather than on a mode grant: what a caller may run and what a caller may see are different questions. Operator tokens are never administrators — the trail records their use, so letting one read it back would let a token audit itself. Verified against the built server: `tasks.create` success and denial each persisted one record carrying `context.requestId` and the user agent; `audit.list` answered FORBIDDEN for an operator token and for a signed-in non-admin over `/rpc/**`. lint:ci, typecheck, and 151 api tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The board showed whatever the last fetch caught. A run records its lifecycle events as it works, so a task appeared and then sat at the state it had when the page loaded until someone pressed refresh — and a stalled page looked exactly like a quiet one. `/api/events` streams the overview over SSE, behind the same session the page needs, pushing whenever a task record is written. The store wrapper that emits lives in the composition root rather than in `packages/api`, so the store contract stays plain persistence: every writer — the router, the webhook route, and the run recording its own events — already goes through that one instance, so a subscriber sees the whole lifecycle rather than the transitions one transport happens to see. Writes are coalesced over 250ms, so a run that records ten events in a burst sends one overview. The client writes each message straight into the query cache the page already reads, rather than invalidating and asking the server for what it just sent. The query stays the loader for the first paint and for a client whose stream never opens. A header indicator says whether the board is actually following, because a stalled stream is otherwise indistinguishable from an idle one. `useLiveOverview` takes the query key rather than reaching for `useNuxtApp()`, which is also what keeps it out of the Nuxt runtime for the unit suite. Verified against the built server: the stream answers 401 unauthenticated; with a session it pushes the current overview on connect and again when a task created through `/api/v1/tasks` reached the store. lint:ci, typecheck, i18n:report, and the dashboard suites pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The router has accepted `tasks.create` and `approvals.decide` since the control plane existed, but nothing in the UI called either: the board could watch a run stop for a human decision and offer no way to give one. The sidebar meanwhile listed nine sections that were inert buttons — a nav that promises surfaces the app does not have teaches an operator that clicking does nothing. The inspector offers Approve and Reject with an optional comment while a task is `needs-human` and undecided, and shows the recorded decision once one exists, because the control plane accepts exactly one. A `details`-based form queues a task from the header. Both emit rather than mutate: the page owns the typed client and the one place a failure is surfaced, so there is no second path to keep in step. Neither writes into the query cache — the decision lands in the store, and the store is what the live stream pushes back, so the board updates from the same source every client sees rather than from a guess about what the server did. The repository is typed rather than picked from the allow-list: the list is server-side checkout paths, which the persisted records deliberately keep out of reach, and `tasks.create` already names the rule it refused on. Verified against the built server, over the same `/rpc/**` the page uses: a session created a task, and got FORBIDDEN for a repository outside the allow-list and for a mode a session may not request. The approval and form logic are covered by 13 new component tests. Browser verification of the rendered result was not possible — this environment has no automation host — so the visual pass against `nuxt-frontend-review` is still owed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Work only reached the control plane when a provider delivered it, so a self-hosted deployment behind no public URL had nothing feeding it: the board stayed empty unless someone posted a task by hand. `server/plugins/poller.ts` lists each watched repository's open pull requests on an interval and starts a review for every head commit it has not started one for. It is the pull-based half of the job the webhook route already does, and it shares that route's durable `DeliveryClaimStore`, so a commit reviewed through one path is never reviewed again through the other. The claim key carries the head sha, which is what makes a new push earn a new review and an unchanged pull request earn nothing. Constraints that are enforced, not documented: it is off unless `CODE_ZERO_POLL_REPOSITORIES` names something; it requests only `observe` or `suggest`, so work nobody asked for cannot write to a checkout; and the checkout comes from the path an operator paired with the repository rather than being derived from the provider's answer, so a run can never target somewhere nobody named. A failed start releases its claim so the next pass retries, and one unreachable provider does not end the pass. `listOpenPullRequests` is new on the GitHub adapter and returns both the base and head commits, because a review reads the diff between them. It skips a record missing either rather than losing the page it arrived in. Verified against the built server: silent and healthy when unconfigured; refuses to start naming the missing variable when configured without a token; and with a token it reports the repository that failed without stopping the process or putting the credential in the log. A pass against real GitHub is still owed — this environment has no credentials, and the tests deliberately reach no network. 27 new tests; lint:ci, typecheck across the graph, and the build pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`accessFromEnvironment` returned `undefined` unless `CODE_ZERO_CONTROL_PLANE_TOKENS` was set, and `mayTargetRepository` fails closed without a policy. So a deployment that authenticates only browser sessions could never create a task: the `CODE_ZERO_CONTROL_PLANE_REPOSITORIES` it had configured did not exist as far as the router was concerned, and every target was refused. The two variables answer different questions. Tokens say who a machine caller is; the allow-list says what any authenticated caller may target, including a person signed into the dashboard. Either one now produces a policy, and only neither returns `undefined`, so an unconfigured deployment still rejects every mutation and a deployment with no tokens still authenticates no machine caller — `principals` is simply empty. Found by running the dashboard with a session and an allow-list and nothing else, which is what `dev:solo` and a self-hosted single-owner install both look like. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A local `zero run` and the dashboard kept separate histories: the CLI executed in the checkout and recorded nothing the board could read, so work started from a terminal was invisible to the surface built to watch it. `--remote` hands the run to a deployment's control plane instead. It presents the session `zero login` stored as a bearer token, so the run is attributed to the person who signed in rather than to a shared operator token, and it goes to `/rpc/**` because that is the only transport that resolves a session — stating the `Sec-Fetch-Mode` header its CSRF guard reads, which a browser sends on its own. The deployment therefore needs `AUTH_ENABLE_DEVICE_AUTHORIZATION=true`, the same flag `zero login` already requires. A flag rather than an inference from `CODE_ZERO_URL`: that variable already selects which deployment `login` and `logout` act on, so treating its presence as "run somewhere else" would silently move an operator's run to another machine and another checkout the first time they set it. The plan called for the implicit form; this is the deliberate departure from it. The exit code comes from the same table a local run uses, so CI reads either the same way, and an answer that is not a result is refused rather than allowed to exit 0. Verified against the built server end to end: from a checkout, `zero run --proactive --remote --json` authenticated with a stored session, executed on the deployment, printed the result, exited 0, and the task appeared in the board's own `dashboard.overview`. 14 new tests; lint:ci and typecheck pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The whole live board rests on one property — every task write announces itself — and nothing tested it. It could not be tested either: the notification was a subclass of the KV-backed store, so reaching it meant reaching the deployment's filesystem driver. `observeWrites` is that subclass turned into a decorator over any `TaskStore`, so a test drives it against an in-memory one. The tests state the parts that matter: it announces every write, only after the write landed, and says nothing when the write failed — a listener re-reading the store on a failed write would find nothing changed and a listener told too early would read the previous state. Forwarding `clear` went with it: `PersistentTaskStore` has none, so it was a capability the wrapper invented for nobody. `docs/architecture.md` gains the live-state paragraph the plan asked for, and `docs/PLAN.md` records what was built, the three places the plan was departed from and why, and the five things still owed — the browser review among them. Verified: 997 tests across every package and app, lint:ci, typecheck, check:repo, i18n:report, and the build all pass. The docs build needs a larger heap than this sandbox allows by default and passes with one; nothing in this branch touches it beyond a one-line table. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Add the `code` executable and VS Code CLI archive
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR is a broad production feature spanning live dashboard behavior, automatic pull-request polling, remote execution, authorization, audit logging, and a database migration. Its authentication-sensitive paths, new automatic workflow, and unexplained root-level executable require human review. Not approved because:
Review your spending limits in Billing settings. You can add or adjust custom eligibility rules. Learn more. |
- Add deployment YAML configuration for control-plane policy - Store repository allow-lists in the database - Add standalone dashboard, docs, and marketing dev commands
| export const repositoryStore: RepositoryStore = | ||
| process.env.AUTH_E2E_MEMORY === 'true' | ||
| ? memoryRepositoryStore(process.env.CODE_ZERO_SOLO_REPOSITORIES) | ||
| : postgresRepositoryStore(); |
There was a problem hiding this comment.
Solo repository setting is ignored
The in-memory repository store reads CODE_ZERO_SOLO_REPOSITORIES, while .env.solo and the README tell users to configure CODE_ZERO_CONTROL_PLANE_REPOSITORIES. As a result, following the documented dev:dashboard setup leaves the repository allow-list empty, so every task creation is refused.
Knowledge Base Used:
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/server/utils/repositories.ts
Line: 113-116
Comment:
**Solo repository setting is ignored**
The in-memory repository store reads `CODE_ZERO_SOLO_REPOSITORIES`, while `.env.solo` and the README tell users to configure `CODE_ZERO_CONTROL_PLANE_REPOSITORIES`. As a result, following the documented `dev:dashboard` setup leaves the repository allow-list empty, so every task creation is refused.
**Knowledge Base Used:**
- [Dashboard application](https://app.greptile.com/wolfstar-project/-/custom-context/knowledge-base/wolfstar-project/code-zero/-/docs/dashboard-application.md)
- [Application service API](https://app.greptile.com/wolfstar-project/-/custom-context/knowledge-base/wolfstar-project/code-zero/-/docs/service-api.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.- Track the latest aube release in mise - Add config and database workspace dependencies to the lockfile
…t the environment The repository and mode grants moved out of the environment in the previous commit but nothing was rewired to the new sources, so the dashboard imported two symbols `@code-zero/api` no longer exports. This closes that. Both oRPC transports now resolve the operator tokens through `controlPlaneAccess()` and answer "may a run target this checkout?" from the `repository` table per request, so a repository configured a moment ago is usable without a restart. `/api/v1/**` takes its CORS allow-list from `control_plane.origins`. The poller reads its watched repositories from the same table on every pass and runs each in the mode that repository is configured with, rather than one mode for the whole deployment. It reschedules itself when a pass finishes instead of holding a fixed interval, which reads `poll.interval_seconds` each time and removes the guard against a pass overrunning its own interval. Five variables are gone: CODE_ZERO_CONTROL_PLANE_REPOSITORIES, _MODES and _ORIGINS, CODE_ZERO_POLL_REPOSITORIES, _INTERVAL_SECONDS and _MODE. What stays in the environment is what a deployment already keeps there: the operator tokens, and CODE_ZERO_CONFIG to find the rest. Also fixes the schema test, which the new `repository` table left failing, and three type assertions the type-aware lint refuses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| "dev": "turbo run dev", | ||
| "dev:docs": "turbo run dev --filter=@code-zero/docs", | ||
| "dev:marketing": "turbo run dev --filter=@code-zero/marketing", | ||
| "dev:solo": "turbo run dev:solo --filter=@code-zero/dashboard", |
There was a problem hiding this comment.
Update stale command references
The standalone dashboard script was renamed to dev:solo, but apps/dashboard/.env.solo and several startup instructions in docs/PLAN.md still tell developers to run dev:dashboard. That command no longer exists and exits with a missing-script error, so developers following the tracked setup cannot start the dashboard. This is non-blocking, but the remaining references should be updated with the rename.
Artifacts
Package-script resolution harness
- The authored harness checks the package script names and runs the documented command.
Package-script resolution output
- The output shows that `dev:dashboard` is absent while `dev:solo` is present.
- The authored command harness invokes the stale dashboard startup command.
- The output records the missing-script failure from the documented command.
Ran code and verified through T-Rex
Prompt To Fix With AI
This is a comment left during a code review.
Path: package.json
Line: 17
Comment:
**Update stale command references**
The standalone dashboard script was renamed to `dev:solo`, but `apps/dashboard/.env.solo` and several startup instructions in `docs/PLAN.md` still tell developers to run `dev:dashboard`. That command no longer exists and exits with a missing-script error, so developers following the tracked setup cannot start the dashboard. This is non-blocking, but the remaining references should be updated with the rename.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.- Prevent stale overview snapshots and unbounded SSE connections - Normalize saved checkout paths and make transport error flags explicit
`newTask.*` and its `modes`/`triggers` children landed in the dashboard locales without a matching `aube run i18n:schema` run, so CI's schema drift check failed on every push regardless of what else changed.
- Keep claims after reviews whose completion write fails - Scan bounded GitHub pull-request pages - Rename the standalone development command to `dev:solo`
…st discovery `listOpenPullRequests` referenced `MAX_PAGES` in its pagination loop without declaring it, so every call threw `ReferenceError: MAX_PAGES is not defined` and broke polling entirely. Adds the constant (20 pages, 2,000 pull requests) and rewrites the tests that covered the old single-page `perPage` parameter to cover walking multiple pages and stopping at the cap instead.
isTaskResult only checked that id/state were strings, so a queued or
in-progress /rpc response (e.g. { id, state: 'queued' }) passed as a
completed TaskResult. JSON mode then printed it and exited 0 before the
review had actually finished, and normal-mode output threw while rendering
a plan that was never populated. Now requires state to be one of the three
terminal states and plan to be present.
Summary
zero run --remotefor dispatching work to a deployment's control plane.dev:solofor running the dashboard with in-memory authentication and no Postgres.Why
This makes the dashboard a usable control surface instead of a read-only snapshot. Operators can see control-plane activity as it happens, create work, and resolve approval requests from the same interface.
The changes preserve the repository boundaries: the dashboard composes the API and authentication layers, runtime execution remains behind the runner, and remote CLI runs are submitted to the deployment control plane rather than executed locally. Polling provides a webhook-independent path for discovering pull requests while sharing durable delivery claims with webhook processing.
Verification
aube run check:repoaube run lint:ciaube run typecheckaube testaube run buildSafety and compatibility
observemode as read-only, or explained the policy change above.Agent context
Reviewer notes
The change spans the dashboard control loop, audit routing, remote CLI execution, pull-request polling, and solo development setup. Particular attention is warranted for authentication and authorization behavior, polling delivery claims, remote-run repository allow-listing, and the restriction of unattended polling to non-writable modes.
Base branch: main
Confidence Score: 5/5
Safe to merge; no blocking issues remain.
No new findings were established. The previously reported issues are fully addressed in the current code: proactive polling and webhook delivery share commit claims, pull-request pagination declares and uses its bounded page limit, remote results require terminal states and plans, repository paths are normalized before storage, stream limits and initial-update ordering are enforced, error flags are explicitly boolean, and the documented solo setup and toolchain version are aligned.
Reviews (14): Last reviewed commit: "fix(cli): reject non-terminal remote tas..." | Re-trigger Greptile
Context used: