← Design · c99

Full Nuxt / Vue Conversion

c992026-09-28 · written by the Claude Code · Fable 5.1 track

Two 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.

Outcome · 2026-10-02

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.

  1. 1
    Pull and run5 min

    git fetch && git checkout c99/compare, then cd 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, then QDASH_API_BASE=http://127.0.0.1:7882 npm run dev (Fable: :3032; the layer resolves from a sibling q-nuxt-layer checkout or the package).

  2. 2
    Read 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.

  3. 3
    Read 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.

  4. 4
    Run the tests and the checker2 min

    npm test runs vitest and scripts/bible-check.mjs: no endpoint path outside composables, no lib function in an appliance, no page script over 60 lines, no any in the typed homes.

  5. 5
    Skim 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.

  6. 6
    Decide10 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

TrackWhatWhere
Claude Code · Fable 5.1Audit (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.5Its 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
BothOne 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.

Colors in both charts served — from qcontrol (an endpoint or the SSE stream) derived — a client rollup over served rows synthetic — the UI standing in for data qcontrol or a shared store should serve UI state — filters, toggles, pickers, labels, copy; not data design in progress — a placeholder page that reads nothing
Two very different kinds of "real"

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.

Time is the structural lie

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.

The 200-row session cap leaks

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.

Seven fetches per people page

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.

The only invented numbers are few and known

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.

The good bones were already there

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.

Sort
RouteAreaPieces of model data · served / derived / syntheticserved · derived · synthetic

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.

ShowSort
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.

#ContractPages it fixesToday's stand-in
1?period= honoured by every detail endpoint and by /api/surface8 detail pages + every Surface sectionall-time numbers under a windowed pill
2/api/sessions?user=&team=&host= with a window summary and totalUser / Team / Endpoint detail, Sessions listing tilesclient filter over the newest 200
3team on agents_detail rows and on /api/endpoints; owner on endpoints9 pages' team joins, the endpoint owner heuristic/api/teams joined by email
4entity_id on /api/costs and /api/performance rowsCost, Performance drill-downslabel → entity through the agents roster
5/api/agent?id=: os, team, skills[], identity-wide cost, sessions_total, ?period=Harness install detailrun-scoped spend, two joins, "OS —"
6sla_due written by the serverFindings, Alertsclient Mark M rubric
7Served framework and policy recordsControls, Policy detail, Compliance, Frameworkcatalogs + localStorage
8Served connector registry with delivery stateIntegrationsstatic catalog + canned previews
9git on workspace rows; skills axis on /api/surface?workspace=Workspaces listing + detailgit: null, three-way join
10Identity (SSO/SCIM) and MDM denominators; account seatsUsers Coverage, Endpoints coveragePROJECTED 3 / 2 / 1
11Per-tool / per-server cost share; label on modelsMCP server, Tool, Model detailwhole-run spend under per-entity headers
12summary and total_events on session detailSession 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 at crates/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.

One is perfect already

It becomes ui/ through a cutover cycle (regenerate the embedded dist, retire the old app). The bible is its documentation.

The bible is right, the app needs changes

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.

A different route to Vue/Nuxt is preferred

The audit, the contract-gap list and the ledger of staged values stand regardless; the bible becomes an input, not the standard.

Qpoint Brand Style Guide