Honest States
Make qdash tell the truth while it loads, when the backend is down, and when a write fails. Apply bible rule R31 ("states are honest, end to end", adopted in c107) to the surfaces that currently show reassuring numbers they haven't measured.
New in this cycle: R31 applied: '—' and skeleton rows while loading, one banner when qdash is unreachable, the all-clear only after the data loads, 'No agents reporting' instead of 100.
Why
The app shows its empty state when it is really loading or failed. On first paint, and whenever the backend is unreachable:
- Findings says "No findings. Fleet is clean. 🛡";
- the posture score reads a green 100;
- Campaigns says "No campaigns yet";
- Cost says "No spend yet.";
- Users says "No attributed users".
Some writes swallow their failures, so the operator never learns a save didn't land. For a security control plane, a false all-clear is the worst kind of wrong. The c99 Opus track found this (SIGNOFF F3, CO2, CA1, C3, S1/S2, Δ13, K2, ND3), and c107 adopted the rule as R31 before the code was changed (opus-harvest.md rows B9–B11).
Objective
Every R31 surface distinguishes loading, failed and legitimately empty, and every write failure is visible. Done when:
- each surface in plan.md §1 renders three distinguishable states on the embedded build: seed loading, backend stopped, and a truly empty roster;
- no write in §1 swallows a rejection;
- specs cover the rollup changes;
- there is a before/after visual record.
Scope
- qcontrol:
crates/qdash/ui/app/(appliances,lib/posture-rollups.ts, the domain composables with swallowed writes), onfeat/new-pages. That branch is parked under PR #713 until Mark says it's ready; this work adds to it. - design: this directory; possibly a layer-candidate row if a library part lacks a loading or error port (R31 says to guard it locally and log the missing port).
- Out of scope: the request layer (typed errors, not-found handling in the transport) and shared requests, which are separate follow-ons. Backend changes.
Key Changes
- kick-off (2026-10-02): plan drafted from the c107 harvest's B9–B11 rows, re-verified against the shipped app on
feat/new-pages. The copy and look for the loading and failed states are a design call (plan §3), and Mark makes it before any code changes. - decisions (2026-10-02): Mark ruled on D1–D5:
- D1: "—" for metrics, skeleton rows for tables.
- D2: one page banner plus "—" when the backend is unreachable.
- D3: inline error and toast when a save fails; the control reverts.
- D4: posture with no agents shows "—" plus "No agents reporting".
- D5: a "last updated" tag after 2+ missed polls.
Work starts: chassis → rollups → the false all-clears, then a review stop with screenshots. - part 1 (2026-10-02) — chassis + the false all-clears, review stop. qcontrol
f76fa091, local onfeat/new-pages, not pushed.- Chassis:
feedState()(error first; a failed poll after a load stays ready); shelluseBackendHealth, fed by the service client, where unreachable means no response, a timeout or 502–504, never a 404; the primitives mark feeds that failed with nothing loaded; layoutBackendBanner(D2 banner, D5 "last updated" tag); libSkeletonRows. - Rollups:
postureBandaverages scored harnesses only, and avg and coverage arenullwhen nothing is measured (the spec covers it). - Surfaces: Findings cards + ledger; Posture tiles, score, factor bars, history and risk table (two more all-clears found there: "All instrumented harnesses are hardened", "Not enough run history"); Compliance attention; Cost tiles + breakdown; Campaigns cards; Users roster.
- 566 specs,
bible-checkclean. - Visual record:
c108-states-{before,after}.json, 19 + 19 shots, six pages × loading / failed / ready plus a stale shot. "Before" is the pre-c108 embedded build on:7882: Findings with qdash answering 503 says "0 HIGH … No findings. Fleet is clean." "After" is:3030: banner + "—", skeleton rows, and the data kept with "Last updated 11s ago · retrying". capture.mtsgainedapi: 'slow' | 'down'(page or action) to simulate the backend. It matches paths starting/api/, becausenuxt devserves source modules like/_nuxt/composables/api/….- Concurrent session: the c106 sticky-title port is working in the same
../qcontroltree (PageHeader, useDetailTitle, 13 detail pages,layouts/default.vue; layer 0.9.26 committed as2dacfeaa). Its mid-edit PageHeader briefly broke two captures; both were retaken. My commit stages only my hunks, and its files are untouched. - Review notes: the stale tag sits a little tight against the floating filter bar. Not yet done: the other ~60 empty-state sites, the 14 silent write catches (D3 toast + inline), and
actor-bandssuccessPct … : 100(tool success with no judged calls).
- Chassis:
- part 2 (2026-10-02) — every empty state waits for its data. Mark: "got it, let's proceed" (group 1, then groups 2–3). qcontrol
e042a5d4(2a),786405aa(2b),26ae31f8(bible §16 wiring), all local onfeat/new-pages.- 2a, the remaining false all-clears: Alerts "nothing would fire ✓", Data Protection "No sensitive file access ✓" ×2 and "No secrets or PII ✓" (plus its tiles), Users "everyone covered", Identity, Posture installs, Policies custom controls. Adds lib
StateEmpty(the one place an empty message decides whether it may speak), aBarListstateport, and LAYER-CANDIDATES rows (SkeletonRows,StateEmpty, and the layer state port that would retire them). - 2b, the rest:
usePageState(): each feed registers with the component that created it, and a page gets one state over its running feeds; closed-gate and on-demand feeds don't count.- Inventory: the seven listings, where the filter-match and no-data copy both wait, and every detail panel.
- Organization / Activity: the listings, the team panels, the six session ledgers, the history bands and their provenance captions.
- Cost: the tables and the guardrail. Performance: the ledgers.
- Detail pages: a skeleton while loading and "—" when down; they used to render nothing. Endpoints no longer says "not found" while loading.
- Tools: the success rate is null with no judged calls.
- The stale tag gets room from the filter bar.
- Verified:
scripts/qdash-r31-check.mjs(new, reusable) loads 34 routes with/apiheld open, answering 503, and up; all clean. The first runs caught four copies the static scan had missed: "No spend or token targets yet", "No model observed", "No harness observed" (a provenance caption) and the Endpoints "not found". 569 specs. - Deliberately left: Settings / Reports "No page yet" (design placeholders), the /live filter popover, the policy filter's "No policy matches", AdjudicationCard "No adjudications yet" (only inside an already-loaded finding), and
policy-statepercent … : 100for a framework with no required controls (a definitional question, not a loading one). - Next: part 3, the writes. A toast (layer candidate) plus inline errors for the 14 silent catches, with the control reverting (D3), each forced to fail on a scratch seed.
- 2a, the remaining false all-clears: Alerts "nothing would fire ✓", Data Protection "No sensitive file access ✓" ×2 and "No secrets or PII ✓" (plus its tiles), Users "everyone covered", Identity, Posture installs, Policies custom controls. Adds lib
- pushed (2026-10-02, from another session): Mark asked for the branch to go to a colleague, so parts 1–2 and the part 3 work in the tree were committed and pushed to origin
feat/new-pages.- Parts 1–2 (
f76fa091,e042a5d4,786405aa,26ae31f8) and the layer 0.9.27 bump (5f24a817) were pushed as they were. - Part 3 is committed as
686cf266("honest states, part 3 — a failed save says so"): the 26 files the tree held at 13:23. That coversuseToast+failReason,ToastStackin the default layout, and the inline save errors across policies, the Govern sheet, controls and the evidence packet, campaigns, findings, integrations and the Users roster. - Checked before committing: 575 specs,
bible-checkclean, and /policies, /integrations, /campaigns and /organization/users all render on:3030. - Not done: no save was forced to fail, so no toast has been seen, and the "each forced to fail on a scratch seed" pass above is still open. If part 3 wasn't finished at 13:23, carry on from
686cf266.
- Parts 1–2 (
- part 3 (2026-10-02) — a failed save says so (D3). Mark: "go ahead". qcontrol
686cf266. That commit was made by a concurrent session (the c109 nav thread) during a commit sweep of the shared tree. It contains exactly this thread's part-3 changes with an accurate message; that session then regenerated the dist with c108 parts 2–3 in it (bf351751).- Shell
useToast+failReason("qdash is unreachable" vs "qdash rejected it") +ToastStackin the layout. Lib candidate:UxToast. - Silent writes that now report: campaign remove; evidence-packet export; egress decisions on Policies and the policy detail (now shown on P10's own save line); integration routing chips, save, disconnect and the setup modal (the last three had no
catchat all, or a barereturn false); team assignment on the Users roster. - Writes that already had an inline error now also toast (D3 "both"): campaign create, finding decisions, the Findings egress modal, control save, threshold save.
- Copy: "save failed — the backend rejected the control" (said even when qdash didn't answer) became "✕ Couldn't save the control — try again".
- Controls revert to server truth on failure.
- Verified:
scripts/qdash-d3-check.mjs(new) forces 12 write paths to fail. Each failing request gets a 503 from Playwright and never reaches the seed. All 12 show inline + toast. Not driven by the script but sharing tested code: the P15/P17 threshold editors and the Policies-list Approve/Block buttons. Live agent focus/reveal are intentional fallbacks and stay as they are. - Twice during the run, the shared dev server served stale transforms of files I'd edited while other sessions were committing in the same tree.
touching the changed files fixed it.
- Shell
- embedded build (2026-10-02). Release binary from HEAD (
bf351751, dist includes c108), on a fresh seed copy:qdash-r31-check34/34 routes clean in slow / down / up;qdash-d3-check12/12. - close-out (2026-10-02). Mark: "make it so, screenshots, history, anything else".
- qcontrol
1154ae0a, four more sites found by the final captures and a wider sweep:- Data Protection "No sensitive access by user";
- Compliance's per-control "No active findings" (green);
- harness detail "No at-rest installs scanned";
- Surface panels, which no longer blame the build ("not served by this backend — restart qdash") when qdash is simply unreachable. The not-served note stays for a real 404.
e30d5220: the stale-data notice. A concurrent session (5e64694f) restyled it to float bottom-right but usedabsolute, which anchored it to the document's bottom, below the fold on any long page (Users: y=1273 on a 900 px window).fixedrestores the intended float, and the toast stack moves tobottom-16so the two never overlap.- Dists:
16c403df,eec2211e. - Final record:
c108-states-final.json, 13 shots in thread "final". Loading and failed states across Findings, Posture, Data Protection, Alerts, Compliance, Cost, Models, a tool detail page and Users; the stale notice; two failed saves with their toasts (evidence export, a routing chip). capture.mtsgainedfail:/api/x(writes only) andfailget:/api/x.qdash-r31-checkgained the four new phrases. Both checks pass on a freshly restarted dev server: 34/34 and 12/12.- Two operational lessons:
- With several sessions committing in one tree, the dev server's watcher drops events and serves stale transforms. A restart is the reliable fix.
make qdash-uirunsnpm ciin the same directory and breaks a livenuxt dev(503) until it restarts.
- qcontrol
Outcome
qdash now tells the truth about its own state (bible R31). Loading, failed and empty are three different things on every page: "—" and skeleton rows while data loads, one banner when qdash is unreachable, and the empty or all-clear copy only after the data has really arrived. A failed save says so beside the control and in a toast, and the control reverts.
- The false all-clears are gone: "Fleet is clean", posture 100, coverage 100%, "all 0 findings mapped", "No sensitive file access ✓", "Nothing would fire ✓", "everyone covered", and "No active findings" per control. In their place: "—", or "No agents reporting" (D4).
- Mechanism:
feedState()/usePageState()(feeds register with the page that created them), libStateEmpty,SkeletonRows,BarListstate, shelluseBackendHealth+BackendBanner(unreachable ≠ rejected),useToast+failReason. Documented in bible §16 "How it's wired". - Scope: about 70 empty-state sites, 13 detail pages, 4 rollups that invented 100, and 15 write paths (8 were silent, 3 had no
catchat all). - Verified by two reusable checks:
scripts/qdash-r31-check.mjs: 34 routes with/apiheld open, answering 503, and up.scripts/qdash-d3-check.mjs: 12 write paths forced to fail.
Both pass on the release binary built from source and on the dev server. - Visual record: 19 + 19 before/after shots (part 1) and 13 final shots, all subject-keyed into
/cycles/evolution. - qcontrol commits on
feat/new-pages, local, parked under PR #713:f76fa091,e042a5d4,786405aa(parts 1–2a/2b);26ae31f8(bible);686cf266(part 3, committed by a concurrent session's sweep);1154ae0a,e30d5220(close-out);- dists
bf351751,16c403df,eec2211e.
- Carries forward:
- layer ports for
DataTable/UxTableList/ lists (astateport),UxToast,DataSkeletonRowsandDataStateEmpty(LAYER-CANDIDATES); - the shared-request engine and typed transport errors (c107 follow-ons);
policy-statepercent … : 100for a framework with no required controls, which is a definitional question.
- layer ports for
Visual record
51 captures — heroes, page slices, and full-page renders. Click to view.
Honest states — final (parts 1–3)
2026-10-02· 13 captureslatest












Honest states — after (R31)
2026-10-02· 19 captures


















Honest states — before (pre-c108 build)
2026-10-02· 19 captures

















