A local browser view of the Portfolio: registered projects, sourced progress, usage and the fact sources still to connect. It also shows what needs attention, a resolution guide for each blocker, and each project's editable core values. Behaviour and acceptance: human interface. Visual specification: design system.
npm --prefix apps/dashboard run build # builds apps/dashboard/dist (ignored by Git)
npm --prefix apps/dashboard run check # build, then the dashboard's own tests
node src/dashboard-server.ts --journal <local-portfolio-journal> [--port 4317] [--no-inspect] [--no-guidance-edits]
The server binds 127.0.0.1 only. It never creates, migrates or seeds a journal.
Modules
| Module | Owns | May import |
|---|---|---|
src/dashboard-contract.ts | Neutral, browser-safe DTO types, DASHBOARD_PROTOCOL, period and token-counter IDs | Nothing |
src/attention.ts | Pure attention projection and blocker resolution detail | Contract, Portfolio types |
src/dashboard-view.ts | Pure projection: field-by-field DTO copies, cross-project totals, source descriptors and their standing, provider names | Contract, attention, Portfolio error and types, guidance types |
src/project-guidance.ts | Core values domain (guidance) | Journal, Portfolio registry |
src/dashboard-server.ts | HTTP composition root: loopback binding, Host/Origin/token guards, read-only journal access, the two commands, static manifest | Contract, projections, Portfolio, guidance, Journal |
apps/dashboard/src | Presentation: routing, React components, words for typed codes, number/date/currency formatting, layout | React; the contract through contract.ts only |
test/dashboard-boundaries.test.ts checks these import rules against the actual source. There is no dependency-injection framework or second registry: the server constructs Portfolio directly and passes its views to the projection.
Protocol
Every JSON body, including errors, carries protocol: "factory-dashboard/3". The browser refuses a body with any other value and asks for a reload, so a page built for another contract never misreads data. Change the constant whenever a served shape or closed code set changes incompatibly.
Where decisions live
Projection (server). Everything that interprets domain facts:
- Sums across projects: exact only when every part is exact. Otherwise the known parts give a lower bound that keeps every reason. A corrupt project adds
corrupt-project. - Source standing from every Count a source supplies, never one convenient outcome. For example, an undated failed Run keeps verification incomplete even when the verified count is exact.
factsInView: whether the selected view shows a fact of that kind (dated in the period, undated, or a cumulative usage session). It makes no claim about facts outside the period.- Collection capability for each source.
trusted-importmeans only a trusted local import records such facts; nothing collects automatically and the dashboard cannot connect a source.no-contractmeans nothing can hold the facts yet. - Measured value stays unknown (
no-outcome-assessment). Raw measurements never qualify.
Browser. Words for closed codes (states, reasons, availability, capabilities), number, date and currency formatting, table layout and navigation. The browser computes no metric and decides no coverage.
Data, not code. Source IDs and names, provider IDs and names, collectors and Product IDs arrive as data. To add or replace a fact source, change SOURCES in the projection. If it has the same fact contract and an existing capability, the dashboard needs no change. A new capability kind is a contract change and needs a protocol bump.
Current sources
| Source | Counts it supplies | Capability |
|---|---|---|
| Verification runs | Every verification outcome and both validity counts | trusted-import |
| Work items and proposals | Every work transition and rejected proposals | trusted-import |
| Pull requests and issues | Every pull-request and issue transition | trusted-import |
| Releases | Alpha and stable availability and withdrawal | trusted-import |
| Model usage | Dated invocations and their tokens; undated invocations and sessions count as facts in view | trusted-import |
| Customer outcomes | None | no-contract |
The local repository check is separate: it is an explicit command (POST /api/products/:id/inspect) that records Git metadata for one registered Product. It is not a period source.
API
| Route | Does |
|---|---|
GET /api/session | Command token, periods, capabilities (inspect, guidance) |
GET /api/portfolio?period= | The overview, with attention counts |
GET /api/products/:id?period= | One project, with its attention items |
GET /api/attention?period= | Attention across every registered Product |
GET /api/products/:id/blockers/:blockerId | A blocker's governing report, history and recheck capability. The ID is URL-encoded. |
GET /api/products/:id/guidance, …/guidance/history, …/guidance/revisions/:n | Current, recent and one exact guidance revision |
POST /api/products/:id/inspect | Body {}: checks repository availability only |
POST /api/products/:id/guidance | Body { commandId, expectedRevision, values }, at most 24 KiB. Recorded as direct entry. |
Commands. Both need this dashboard's Origin, Sec-Fetch-Site: same-origin when sent, JSON, and the session token. Each is for a Product already in the registry, and each writes through CommandJournal.openExisting.
Reads. GET and HEAD open the journal read-only. They record nothing, run no Git and start no process.
Not exposed. There is no route that marks a blocker resolved, imports observations or runs an arbitrary check.
Attention
src/attention.ts projects Portfolio.currentConditions, which reads each Product stream and the usage stream separately. An unreadable stream becomes one facts-unreadable action, and the others are still read. The list has no state of its own: no read or dismiss flags, no queue and no approval step.
| Category | Items |
|---|---|
| Action | An open blocker; a repository observed unavailable; a source that declared itself unavailable, unauthenticated or corrupt; stored facts that fail validation |
| Notice | A repository never checked, or checked more than a day ago; declared partial coverage |
- Not items. Empty guidance, missing coverage, dirty files, failed Runs and unknown metrics are never items or gates.
- Needs you.
ownerRequiredneeds a source that names the owner and says why further remedies cannot progress. Otherwise the stated responsibility (factory, external, owner per source, or not stated) is shown. - Identity. An item's ID is Product, subject and reason. A correction updates its evidence rather than adding an item, and repeated declarations of one source condition are grouped with their own windows.
- Periods. Blockers and repository state are current and ignore the period. Source notices are those overlapping it.
- Order. Actions, then notices; within each by Product and subject. No business priority is implied.
- Counts. Counts describe reported items, never the absence of blockers.
Blocker resolution guide
/projects/:id/blockers/:blockerId shows:
- the prerequisite, affected work, who can act and why remedies cannot progress;
- the steps to resolve, from the source's optional details or its
nextAction; - the remedies tried and their evidence;
- the report history.
Evidence is text; it is never opened or fetched. Three outcomes are kept apart:
- Reload facts reads recorded facts again and says so. It tests nothing.
- Recheck is
unsupported: no trusted collector checks blocker prerequisites. The repository check checks availability only and never resolves a blocker. - Resolved is only a source recording it. That shows as
reported, orreported-with-evidencewhen the source cites evidence; neither is presented as verified.
Presentation rules
- Overview. Each row shows the project, its stated focus, verified runs with their current validity, and repository availability with check time. Branch and revision are on the project page.
- Current focus. Lists stated focus and open blockers; a blocker row opens its resolution guide. No source ranks next work yet, so the list is not presented as an ordered queue.
- Actions. A central list across projects, with concise counts, required actions separated from notices, and a link from each item to its guide or project. Reload is labelled read-only.
- Core values. The project page shows the current values, revision and provenance. The editor can add, reword, reorder, retire and restore values. Its behaviour:
- Feedback is per field and concise.
- A conflict keeps the losing draft and shows both sides. Choosing to keep the draft is explicit; it is never silently rebased.
- A failed or uncertain save keeps its operation ID, so a retry cannot record twice.
- Connections to finish. Each incomplete source shows its name, a short state and a caption that acknowledges imported facts. Reasons and capability are in a details disclosure. No control implies that a source can be connected from the browser.
- Usage. Invocations are allocated by date. Undated invocations and cumulative sessions are shown separately and never assigned to a period. Costs are provider-reported estimates: not billing, and not spending in a period.
- Brand. The Factory mark sits beside the wordmark. Plumb appears only in the empty Portfolio, below the factual message, when that read succeeded and reports nothing needing attention. Both are decorative, byte-identical copies of the reviewed files in
apps/dashboard/public/brand, served from the static manifest. Sources and hashes: design system.
Tests
| File | Covers |
|---|---|
test/dashboard-view.test.ts | Completeness from every outcome; imported, incomplete, problem and unmeasured sources; period-scoped facts; corrupt projects; replaced and added descriptors; sums |
test/dashboard-server.test.ts | Loopback and Host checks; period views; reads never write or run Git; command guards; static manifest; escaping; missing and corrupt stores; protocol on every body |
test/dashboard-format.test.ts | Unknown versus zero; lower bounds; branch text; remedies; generic source wording; historical validity; refusal of other protocols |
test/dashboard-boundaries.test.ts | The import rules above; guidance has no transport or filesystem access and only the dashboard consumes it; the browser never injects HTML or evaluates code |
test/attention.test.ts | Responsibility and owner classification; deduplication and correction; current blockers versus period notices; repository items; no inferred gates; corrupt Product and usage streams isolated; old blocker facts and their digests; blocker details bounds; resolution standing |
test/dashboard-guidance-attention.test.ts | Guidance command guards, bounds, restart, replay, stale and conflict; attention and blocker routes; corrupt usage isolation; read-only GET and HEAD; no resolve route |
test/dashboard-editor.test.ts | Editor operation reuse, failure classes, change summaries, attention words and encoded targets |
test/dashboard-brand.test.ts | Brand copies match reviewed hashes; mark size and favicon; Plumb placement, alt text and copy; PNG serving |
apps/dashboard/test/brand-render.test.ts | Dashboard package, not the root suite: the real Portfolio view rendered (react-dom/server, via the dashboard's Vite) for each read state. Plumb only in the calm empty case, after the message, with empty alt and no interactive, table or metric ancestor |
Browser behaviour, layout and screenshots are verified separately in a real browser.
