Full Nuxt / Vue Conversion
c992026-09-28 · written by the Claude Code · Fable 5.1 trackTwo experiments, one decision: where every number in the qdash UI comes from, the written data interface that came out of it, the two parallel apps built on it, and what the front end needs decided now.
The Fable app became qdash's UI: crates/qdash/ui on feat/new-pages, embedded in the binary (c107 · qdash Cutover). The previous app and the Opus app are archived as tags. Six of Opus's fixes and its honest-states rule came across. This page is kept as the record of the bake-off.
Why this work
Qpoint is moving the qdash front end fully to Nuxt / Vue. The current UI was built page by page while the product was being found; it works, but nothing about how a page gets its data was written down, and every page does it its own way.
This cycle was the first step: before any more pages are built, codify the structure — how a page takes data from the backend, how it moves into components, how decisions go back — and prove the codified structure by rebuilding every existing page on it. Two agents did this in parallel from the same brief so the method could be judged, not just the result.
What follows is what was found, what was written, what was built, and the decisions that would let the conversion proceed.
Executive summary
- 1Most of what the UI shows is real. Every listing and the newer detail pages read from qcontrol; the gaps are concentrated in the governance surfaces (Controls, Compliance, Framework, Integrations), where 44 pieces of data are still stand-ins for things the backend does not serve yet. The conversion does not wait on those: they are named, one per line, in a ledger with the backend contract that retires each.
- 2The bible codifies app.qpoint.io's composable grammar as thirty rules with a test each: pages own scope, appliances are fed by props and speak in emits, primitives are domain-blind and typed, every staged value lives in one ledger.
- 3The Fable track's app: all 43 routes as bills of materials over 44 domain modules and ~150 fed appliances; 66 spec files / 539 specs; a checker that turns "did we follow the documents" into a number. Verified page by page against the seed.
- 4What is needed now is a decision on the path to Vue/Nuxt: adopt one app, adopt the bible and iterate, take pieces of both, or take a different route. The evaluation takes about 45 minutes and is laid out below.
How to evaluate — about 45 minutes
The two criteria: did the work follow the documents, and is it clean, simple and consistent — easy to follow, logical, the same noun meaning the same thing in every file.
- 1Pull and run5 min
git fetch && git checkout c99/compare, thencd crates/qdash/apps/<track> && npm install. Copy the seed db and serve it:cp scripts/qdash-seed/meridian.db /tmp/<track>.db && ./target/release/qdash --db /tmp/<track>.db serve --port 7882, thenQDASH_API_BASE=http://127.0.0.1:7882 npm run dev(Fable::3032; the layer resolves from a siblingq-nuxt-layercheckout or the package). - 2Read the bible's spine10 min
02-data-interface-bible.md§2 (the shape), §3 (the three tiers and their tests), §6 (where business logic lives). Skip the prototypes unless a rule surprises you. - 3Read one page top to bottom10 min
/team/users:pages/team/users/index.vue(the bill of materials) →composables/team/people.ts(the seven feeds, the rollup) →components/team/UsersRoster.vue(fed) →lib/people-rollups.ts(the math). Then one detail:/team/sessions/detail→useSession→components/team/SessionReading.vue. - 4Run the tests and the checker2 min
npm testruns vitest andscripts/bible-check.mjs: no endpoint path outside composables, no lib function in an appliance, no page script over 60 lines, noanyin the typed homes. - 5Skim the judgement calls8 min
04-execution-log.md§Close and bible §14–15 (the addenda): every place the document had to be decided by the code.phase4/*.md§Judgement calls per tranche if one matters to you. - 6Decide10 min
D1 through D6 above. D7–D9 can wait.
Decisions needed, and how to make each
D1 is the decision. D2–D6 are the rules the bible had to guess at; each answer changes a rule, not the shape. D7–D9 can wait for the cutover. Open a decision for its options, how to decide, and the recommendation.
What was produced
| Track | What | Where |
|---|---|---|
| Claude Code · Fable 5.1 | Audit (2 319 lines, four group reports), bible v0.3, plan, execution log, five executor reports; the app at crates/qdash/apps/claude-code-fable-5.1/: 43 routes, 44 domain modules, ~150 appliances, 12 library candidates, 539 specs, npm test = vitest + bible check. | branch c99/claude-code-fable-5.1; design cycles/c99.nuxt-backend-interface/claude-code-fable-5.1/ |
| Claude Code · Opus 5.5 | Its own audit, bible, plan and app under the same convention — not read by the Fable track, so this page cannot summarise it. Start from its README. | branch c99/claude-code-opus-5.5; design cycles/c99.nuxt-backend-interface/claude-code-opus-5.5/README.md |
| Both | One comparison checkout with the two apps side by side, each on its own port against its own copy of the seed. | branch c99/compare (an octopus merge of the two; applies cleanly) |
What the audit found
Every page read to the field with the components, composables and rollups it imports; every on-screen element classified as served, derived, or one of five kinds of shim; every field claim confirmed against the backend handlers.
Listings and the newest details are 80–95 % served with honest derivations. Controls, Compliance, Framework and Integrations are 20–40 % served: catalogs and localStorage wearing badges. There is no middle, so the conversion treated them as two jobs: re-source the first, wrap the second in stand-ins that speak the served protocol.
The shell's 7-day default applies wherever a page registers and nowhere it does not. Eight detail endpoints take no period, so a row's "sessions 12" on a windowed listing becomes "sessions 40" on its all-time detail with nothing on screen saying why.
Every "this person's / this team's" session number is a client filter over the fleet's newest 200 rows. The captions disclose it; the tiles do not.
Users, Teams, User detail and Team detail each issue the same seven polls every 3 seconds and join them client-side. Honest and tested, and the clearest case for the change-token idea.
A hard-coded 3 / 2 / 1 behind the Users coverage tile, a $25 budget copied from a control (now re-sourced), a 100 % outcome when nothing was judged, a stale factor key. Every one is now in the ledger with the contract that retires it.
lib/ with 372 passing specs, the shell-owned global filter, the presentational Overview components, the apply-then-reload write path, the poll gate. The bible keeps every one and names them.
What each page shows
The pieces of data each page loads from qcontrol or a shared store such as teams: served, derived from served rows on the client, or synthetic — the UI standing in for something that store should serve. UI state (filters, toggles, labels, copy) is not data and is not counted. Click a page for its on-screen elements; the "Data it reads" column is where each value comes from — the endpoint or rollup, with the field or expression where the audit recorded one.
Pieces of data × pages
Every piece of data a page loads from qcontrol or a shared store — an endpoint, a client rollup over served rows, or a stand-in for something that store should serve — against the pages that instantiate it. Red is synthetic: a projected placeholder, a catalog or localStorage record, a hard-coded number or fixture. UI state is left out unless you include it.
| 139 pieces | home | /fleet | /posture | /findings | /controls | /controls/… | /compliance | /framework | /alerts | /campaigns | /dlp | /identity | /performance | /cost | /inventory | inventory/harnesses | inventory/harnesses/… | inventory/mcp-servers | inventory/mcp-servers/… | inventory/models | inventory/models/… | inventory/tools | inventory/tools/… | inventory/providers | inventory/providers/… | inventory/providers/accounts/… | inventory/agents/… | inventory/skills | inventory/skills/…?skill= | inventory/workspaces | inventory/workspaces/…?workspace= | /integrations | /integrations/:id | /live | shell | /team | team/users | team/teams | team/users/… | team/teams/… | team/endpoints | team/endpoints/… | team/sessions | team/sessions/… |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| DAILY_BUDGET = 252 | ||||||||||||||||||||||||||||||||||||||||||||
| FRAMEWORK_CATALOG2 | ||||||||||||||||||||||||||||||||||||||||||||
| useFrameworkState2 | ||||||||||||||||||||||||||||||||||||||||||||
| Workspace kind marker2 | ||||||||||||||||||||||||||||||||||||||||||||
| 'operator' / updated_at as "Last matched"1 | ||||||||||||||||||||||||||||||||||||||||||||
| [expires:YYYY-MM-DD] note prefix1 | ||||||||||||||||||||||||||||||||||||||||||||
| Catalog fixtures1 | ||||||||||||||||||||||||||||||||||||||||||||
| CONNECTOR_GROUPS , CONNECTORS1 | ||||||||||||||||||||||||||||||||||||||||||||
| connectorById1 | ||||||||||||||||||||||||||||||||||||||||||||
| decoded invocation policy1 | ||||||||||||||||||||||||||||||||||||||||||||
| DEFAULT_ROUTE, CHANNEL_LABEL1 | ||||||||||||||||||||||||||||||||||||||||||||
| DEFAULT_ROUTE, COLLAPSED_ITEM_COUNT = 3, copy1 | ||||||||||||||||||||||||||||||||||||||||||||
| DEFAULT_ROUTE, SEVS, SEV_ON, copy1 | ||||||||||||||||||||||||||||||||||||||||||||
| Detector hits: 01 | ||||||||||||||||||||||||||||||||||||||||||||
| EVENT_FAMILIES , familyTone palette, formatPrevi1 | ||||||||||||||||||||||||||||||||||||||||||||
| Exception fixtures1 | ||||||||||||||||||||||||||||||||||||||||||||
| FACTOR_PTS1 | ||||||||||||||||||||||||||||||||||||||||||||
| FRAMEWORK_SHORT1 | ||||||||||||||||||||||||||||||||||||||||||||
| frameworkBySlug1 | ||||||||||||||||||||||||||||||||||||||||||||
| generateSyntheticRunEvents1 | ||||||||||||||||||||||||||||||||||||||||||||
| Git pair + ProjectedBadge1 | ||||||||||||||||||||||||||||||||||||||||||||
| Guardrail card copy: "medium" pill, "Daily spend1 | ||||||||||||||||||||||||||||||||||||||||||||
| Intensity thresholds1 | ||||||||||||||||||||||||||||||||||||||||||||
| isGithubRepo1 | ||||||||||||||||||||||||||||||||||||||||||||
| Level counts1 | ||||||||||||||||||||||||||||||||||||||||||||
| List seed entries1 | ||||||||||||||||||||||||||||||||||||||||||||
| LIVE instrumented-framework list1 | ||||||||||||||||||||||||||||||||||||||||||||
| MARK_M_RUBRIC 14/30/90 days1 | ||||||||||||||||||||||||||||||||||||||||||||
| POLICY_CATALOG / POLICY_CATEGORIES / FRAMEWORK_S1 | ||||||||||||||||||||||||||||||||||||||||||||
| POLICY_CATALOG lookup by ?policyId=1 | ||||||||||||||||||||||||||||||||||||||||||||
| previewComponent1 | ||||||||||||||||||||||||||||||||||||||||||||
| PROJECTED1 | ||||||||||||||||||||||||||||||||||||||||||||
| Projected cards1 | ||||||||||||||||||||||||||||||||||||||||||||
| qdash.compliance.frameworks1 | ||||||||||||||||||||||||||||||||||||||||||||
| qdash.policies.v11 | ||||||||||||||||||||||||||||||||||||||||||||
| SAMPLE_FINDINGS, SAMPLE_DIGEST1 | ||||||||||||||||||||||||||||||||||||||||||||
| score bands 80/50, empty-roster = 1001 | ||||||||||||||||||||||||||||||||||||||||||||
| SETUP_RECIPES1 | ||||||||||||||||||||||||||||||||||||||||||||
| setupRecipe , seedValues, missingFields, fieldVi1 | ||||||||||||||||||||||||||||||||||||||||||||
| sourceOf / viewOf regexes1 | ||||||||||||||||||||||||||||||||||||||||||||
| Threshold observed: 0 + value1 | ||||||||||||||||||||||||||||||||||||||||||||
| tokensOf, "500-line guidance", longBody > 80, LE1 | ||||||||||||||||||||||||||||||||||||||||||||
| usePolicyState1 | ||||||||||||||||||||||||||||||||||||||||||||
| values, dirty, seededFrom, guideOpen, saving1 | ||||||||||||||||||||||||||||||||||||||||||||
| Band16 | ||||||||||||||||||||||||||||||||||||||||||||
| Filtered rows4 | ||||||||||||||||||||||||||||||||||||||||||||
| Ledger rows4 | ||||||||||||||||||||||||||||||||||||||||||||
| lastMs2 | ||||||||||||||||||||||||||||||||||||||||||||
| Rows2 | ||||||||||||||||||||||||||||||||||||||||||||
| Surface panels2 | ||||||||||||||||||||||||||||||||||||||||||||
| Agent presence1 | ||||||||||||||||||||||||||||||||||||||||||||
| Attention1 | ||||||||||||||||||||||||||||||||||||||||||||
| Band + attention1 | ||||||||||||||||||||||||||||||||||||||||||||
| campaignCounts1 | ||||||||||||||||||||||||||||||||||||||||||||
| Concentration sentence1 | ||||||||||||||||||||||||||||||||||||||||||||
| credRisk1 | ||||||||||||||||||||||||||||||||||||||||||||
| Default-on rows1 | ||||||||||||||||||||||||||||||||||||||||||||
| deriveStatus two-axis mapping1 | ||||||||||||||||||||||||||||||||||||||||||||
| Device rows1 | ||||||||||||||||||||||||||||||||||||||||||||
| Donut slices1 | ||||||||||||||||||||||||||||||||||||||||||||
| downloadText, findingsCsv1 | ||||||||||||||||||||||||||||||||||||||||||||
| egressHost1 | ||||||||||||||||||||||||||||||||||||||||||||
| Entity-meta index1 | ||||||||||||||||||||||||||||||||||||||||||||
| Event tally1 | ||||||||||||||||||||||||||||||||||||||||||||
| gf.matches over harnesses / source1 | ||||||||||||||||||||||||||||||||||||||||||||
| Harnesses ledger1 | ||||||||||||||||||||||||||||||||||||||||||||
| Header bits1 | ||||||||||||||||||||||||||||||||||||||||||||
| Install bar rows1 | ||||||||||||||||||||||||||||||||||||||||||||
| Install ledger rows1 | ||||||||||||||||||||||||||||||||||||||||||||
| lastMs, provider-id options1 | ||||||||||||||||||||||||||||||||||||||||||||
| lib/download.ts1 | ||||||||||||||||||||||||||||||||||||||||||||
| Live state1 | ||||||||||||||||||||||||||||||||||||||||||||
| nowMs1 | ||||||||||||||||||||||||||||||||||||||||||||
| Outcome donut1 | ||||||||||||||||||||||||||||||||||||||||||||
| Owner1 | ||||||||||||||||||||||||||||||||||||||||||||
| Owner team1 | ||||||||||||||||||||||||||||||||||||||||||||
| Provider options1 | ||||||||||||||||||||||||||||||||||||||||||||
| Quick-filter options1 | ||||||||||||||||||||||||||||||||||||||||||||
| real , volumes, sevCount, isActive, activeStats1 | ||||||||||||||||||||||||||||||||||||||||||||
| renderSkillMarkdown, skillBodyStats1 | ||||||||||||||||||||||||||||||||||||||||||||
| rollingWindowSums1 | ||||||||||||||||||||||||||||||||||||||||||||
| Roster1 | ||||||||||||||||||||||||||||||||||||||||||||
| Row keys1 | ||||||||||||||||||||||||||||||||||||||||||||
| Server options1 | ||||||||||||||||||||||||||||||||||||||||||||
| Session rows1 | ||||||||||||||||||||||||||||||||||||||||||||
| Share column1 | ||||||||||||||||||||||||||||||||||||||||||||
| skillBand, definitionDelta, plural1 | ||||||||||||||||||||||||||||||||||||||||||||
| Skills on this install1 | ||||||||||||||||||||||||||||||||||||||||||||
| Skills panel rows1 | ||||||||||||||||||||||||||||||||||||||||||||
| skillsBand, skillsTeamIndex, peopleOnScannedInst1 | ||||||||||||||||||||||||||||||||||||||||||||
| surfacePanels, scopeCaption, scopeLede1 | ||||||||||||||||||||||||||||||||||||||||||||
| Team1 | ||||||||||||||||||||||||||||||||||||||||||||
| Team rows1 | ||||||||||||||||||||||||||||||||||||||||||||
| Team sessions1 | ||||||||||||||||||||||||||||||||||||||||||||
| Teams panel rows1 | ||||||||||||||||||||||||||||||||||||||||||||
| This person's sessions1 | ||||||||||||||||||||||||||||||||||||||||||||
| Tool / agent / config rows1 | ||||||||||||||||||||||||||||||||||||||||||||
| Transcript / raw / rail derivations1 | ||||||||||||||||||||||||||||||||||||||||||||
| useApi polling + pollGate1 | ||||||||||||||||||||||||||||||||||||||||||||
| useEventTableProjections1 | ||||||||||||||||||||||||||||||||||||||||||||
| userRows1 | ||||||||||||||||||||||||||||||||||||||||||||
| Versions panel rows1 | ||||||||||||||||||||||||||||||||||||||||||||
| workspaceBand1 | ||||||||||||||||||||||||||||||||||||||||||||
| workspaceInstallIds + skillsOnInstalls1 | ||||||||||||||||||||||||||||||||||||||||||||
| workspacesBand1 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/agents8 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/teams8 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/security7 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/surface7 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/installations5 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/skills5 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/stats5 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/campaigns3 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/egress3 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/endpoints3 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/harnesses3 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/sessions3 | ||||||||||||||||||||||||||||||||||||||||||||
| The3 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/accounts2 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/channels2 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/controls2 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/costs2 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/egress-decisions2 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/mcp-servers2 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/models2 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/providers2 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/tools2 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/trends2 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/workspaces2 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/agent1 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/agent/focus1 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/control-test1 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/control-vocab1 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/dlp1 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/events/stream1 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/integrations1 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/inventory1 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/performance1 | ||||||||||||||||||||||||||||||||||||||||||||
| /api/security-packet1 |
Piece names are as the audit wrote them; an endpoint is its path, a shim its identifier. Generated from the four audit files by scripts/audit-to-data.py.
Contract gaps, ranked by pages unblocked
c99 was front-end only. This is the hand-off to the backend: what to serve so the shims retire, in the order that fixes the most.
| # | Contract | Pages it fixes | Today's stand-in |
|---|---|---|---|
| 1 | ?period= honoured by every detail endpoint and by /api/surface | 8 detail pages + every Surface section | all-time numbers under a windowed pill |
| 2 | /api/sessions?user=&team=&host= with a window summary and total | User / Team / Endpoint detail, Sessions listing tiles | client filter over the newest 200 |
| 3 | team on agents_detail rows and on /api/endpoints; owner on endpoints | 9 pages' team joins, the endpoint owner heuristic | /api/teams joined by email |
| 4 | entity_id on /api/costs and /api/performance rows | Cost, Performance drill-downs | label → entity through the agents roster |
| 5 | /api/agent?id=: os, team, skills[], identity-wide cost, sessions_total, ?period= | Harness install detail | run-scoped spend, two joins, "OS —" |
| 6 | sla_due written by the server | Findings, Alerts | client Mark M rubric |
| 7 | Served framework and policy records | Controls, Policy detail, Compliance, Framework | catalogs + localStorage |
| 8 | Served connector registry with delivery state | Integrations | static catalog + canned previews |
| 9 | git on workspace rows; skills axis on /api/surface?workspace= | Workspaces listing + detail | git: null, three-way join |
| 10 | Identity (SSO/SCIM) and MDM denominators; account seats | Users Coverage, Endpoints coverage | PROJECTED 3 / 2 / 1 |
| 11 | Per-tool / per-server cost share; label on models | MCP server, Tool, Model detail | whole-run spend under per-entity headers |
| 12 | summary and total_events on session detail | Session detail | "(AI gen summary)", "nothing collapsed" |
Where everything is
- Branches (GitHub, qpoint-io/qcontrol)
c99/claude-code-fable-5.1·c99/claude-code-opus-5.5·c99/compare(both apps)- The apps
crates/qdash/apps/<track>/; the current UI atcrates/qdash/ui/is untouched- Ports (local convention)
- current UI
:3030· Opus:3031/ seed:7881· Fable:3032/ seed:7882 - Design records
design/cycles/c99.nuxt-backend-interface/—PARALLEL-APPS.md(the convention), one directory per track,EXECUTIVE-OVERVIEW.md(this page as text)- Fable documents
01-data-audit.md·02-data-interface-bible.md·03-conversion-plan.md·04-execution-log.md·questions-and-ideas.md·phase1/(audits) ·phase4/(executor brief + reports)
The experiment, and what it can turn into
These are time-saving experiments, not a commitment. The question under test is whether a written data interface plus a written brief can produce, in a day, an app the engineering lead would accept — the method as much as the artifact.
It becomes ui/ through a cutover cycle (regenerate the embedded dist, retire the old app). The bible is its documentation.
Amend the rules that were guessed at (below), re-run the affected tranches — the executors are cheap and the brief is reusable — and evaluate again.
The audit, the contract-gap list and the ledger of staged values stand regardless; the bible becomes an input, not the standard.