qdash Baked Demo
Serve the qdash demo as pre-computed worlds (sizes, scenarios, First run stages) from files, switchable instantly, baked daily, with no containers at serve time
Why
c112 built a demo whose world rolls forward every few minutes, and it works. But on Cloudflare's fractional vCPU each page load recomputes qdash's answers from raw tables, and after every tick the page is cold: about 15 calls queue on half a core, the slowest pass the UI's 8-second abort, and the retries keep the queue full. Mark saw "Can't reach qdash" on the default world on 2026-10-06. Making the running design comfortable would take 2–4 vCPU per size ($70–100 a week) and pages would still go cold after every tick.
Stepping back: the demo's purpose is to show the site populated with real data and how situations appear in it (company sizes, First run, specific exception scenarios), not live updates. Each situation is a fixed world with a fixed set of answers. Those can be computed once and served as files.
Objective
- A baker that turns a world spec (size, scenarios, First run stage) into every API answer the UI can request, as files with a manifest.
- The Worker serving the UI and those files from R2, keyed by a world cookie, with the panel as a picker and writes refused honestly.
- A daily bake, unattended, that replaces the worlds without a visible gap.
demo.qpoint.iocut over, the container applications deleted, and the live path kept deployable from the same tree.- The checks in plan.md passed.
Done = any page of any world loads cold in under a second on the hostname, switching worlds is instant, every scenario shows its story, the bake has run on its own at least once, and c112 is closed with its outcome.
Scope
- qcontrol, branch
c113/baked-demooffc112/demo-container:deploy/qdash-demo/bake.py,bake.sh, the Worker's baked backend and picker,wrangler.baked.jsonc, a daily CI workflow. - Cloudflare: an R2 bucket for worlds, the Worker on
demo.qpoint.io, the four container applications deleted at cutover. - Design: this record, the S5 spike report, captures of the picker and each scenario world.
- Plan: plan.md
Key Changes
kick-off (2026-10-06): the thread that found the c112 overload became this cycle (initial-prompt.md). Grounding read: the UI is a static SPA with a committed dist/; 31 API routes whose server-side parameters are period plus entity selectors, with the global filter's operators applied on the client, so the crawl space is bounded; the Worker's Deps.container(size) is the seam for a baked backend. Plan written with five phases and eight checks; S5 (crawl coverage) running first because it's the one risk.
S5 passed; Phases 1 and 2 started (2026-10-06): (s5-crawl-coverage.md) the server's query grammar plus ids harvested from answers enumerated every URL a 388-visit walk of the UI made (312 normalised URLs, 0 misses after one added rule: the agent filter on six listings). Served from files, a cold Teams page reached its last answer in 247 ms against 1,263 ms live; the index page 310 ms against 2,132 ms. The cost: a complete Mid bake is ~141,000 answers, ~430 MB and ~1.5 h on the laptop, five-sixths of it that one agent-filter affordance over 4,891 agent instances. Consequences in the plan: one pack and one index per route instead of a file per answer (R2 charges per write), and tier 2 by alias (agents with no runs in a period share one answer). Gotchas for the build: APFS case-folding silently overwrote 817 answers during the spike; answers aren't byte-stable (float sums, request-time windows), so equality tests compare parsed JSON. Phase 1 (bake.py) and Phase 2 (the Worker's baked backend, static-asset UI, picker) running in parallel in the qcontrol-c113 worktree. Open for Mark in Phase 3: bake cadence and where the baker runs (plan.md).
Phases 1 and 2 done (2026-10-06): bake.py (phase1-bake.md) bakes a world spec into per-route packs: Mid-sized in 28.5 min, 141,369 answers, 191 MB (S5 projected 1.5 h and 430 MB); Small in under a minute; a scenario world and a First run stage proved the spec paths. The task's tier-2 alias rule was wrong and the live check caught it: the listings' ?agent= filter scopes by the agent's identity (system, email), not by runs, so the rule now groups entities by identity and fetches one representative per identity (verified live on every world, 0 differences). Answers differ between qdash processes beyond float digits (minted ids, request-time stamps, HashMap order), so comparisons skip a cited list of unstable fields. The Worker's baked backend (phase2-worker.md): UI from Static Assets, /api from R2 by a world cookie with ?world= and the ?size= alias, catalog and per-route indexes cached at the edge, misses counted in the log, writes answered 405 with a read-only notice the pill shows for 5 s (the UI's own toast is honest but doesn't say why), the picker on /_admin; 52/52 tests, both configs share router.ts. Phase 2's walk found the one gap, the Teams page's ?team=unassigned, now in the baker. Re-verified by the main session: both suites green; the Worker on local R2 seeded with the real Mid, Small, scenario and First run bakes walked with 0 misses (89/89 hits), cold Teams page 356 ms. Full eleven-world bake at one anchor started on the laptop for the cutover; Phase 3 (bake.sh, check.py baked mode) started.
Phase 3 done; full bake; cutover (2026-10-07): bake.sh and check.py --baked (phase3-upload.md); re-verified 6/6 including the real-bakes test. The full bake of eleven worlds at one anchor (2026-10-06 19:24 UTC) took 3.5 h on the laptop with 0 errors: Small 17 s, Mid-sized 29 min (141k answers, 191 MB), Enterprise 62 min (201k, 261 MB), each Mid scenario world ~29 min, First run stages 1 s to 78 s; 1.2 GB in all. Cutover on Mark's go: bucket qdash-demo-worlds created on the Qpoint account, the worlds uploaded through wrangler (--remote; note wrangler's r2 object get reads local state unless told --remote), catalog last, then wrangler deploy --config wrangler.baked.jsonc over the same Worker name. The first deploy was refused (error 10064: the version stopped exporting the Durable Object classes the live containers depend on), so the baked entry now exports the four classes as inert stubs; a deleted_classes migration removes them once the containers are gone. Version f7155e3a serves demo.qpoint.io as of 18:26 UTC. check.py --baked against the hostname: E1 2,069 API reads over all eleven worlds, 0 misses; E2 every page cold in 400–800 ms (Teams was 3–8 s and timing out before); E3, E5 pass; E4 passes after a check fix (shadow-AI findings carry their time in first_seen). Single calls on the hostname: /api/stats 0.10 s, /api/skills 0.43 s. Held for Mark's review: deleting the four container applications (step 5), which ends the running demo and its spend. Product finding on the way: at Mid every install reads instrumented: true in /api/installations (qdash's type_seen rule marks a tool type instrumented if any instance of it was ever observed) while /api/security lists 24 shadow installs; Inventory's coverage and Findings disagree.
containers deleted, c112 closed, the baker built (2026-10-07): on Mark's "make it so": the four container applications deleted, the v2 deleted_classes migration deployed (version 59ee049e) and the stub classes removed; c112 closed with its outcome and archived. The scheduled baker (phase4-baker.md): Dockerfile.baker (qdash Linux binary, python3, curl, the generator, bake.sh) with an entrypoint that opens a log port, runs bake.sh --world … --upload and exits with its code; in the Worker a QdashBaker container class (standard-4, one instance), scheduled() mapping two crons (Mid-sized daily Mon–Sat 09:00 UTC, all eleven worlds Sunday 06:00 UTC) to world lists, POST /_admin/bake and /_admin/bake/status behind Access, and a "Bake now" section on the picker; v3 migration adds the class. Proved locally in OrbStack at 4 CPUs against a fake S3: Small end to end in 52 s (host: 16 s, so ~1.5× slower at 4 vCPU; nproc inside reports the host's cores, so --workers 8 is a var). Estimated cost: a Mid bake ≈ $0.40, the weekly full bake ≈ $2.40, ≈ $20 a month net. Re-verified: 59/59 tests, tsc clean. Waits on Mark: an R2 API token (object read & write on qdash-demo-worlds) set as two Worker secrets, then the deploy and a first by-hand bake to measure the real standard-4 time.
Outcome
Decisions
- D1 Read-only (Mark, 2026-10-06). No containers at serve time. Writes show a "read-only demo" notice.
- D2 Daily bakes (Mark, 2026-10-06). One scheduled bake a day; "today" is right, newest activity up to a day old.
- D3 Same tree, two configs. One
deploy/qdash-demo/and one Worker source;wrangler.jsoncstays the live (container) config,wrangler.baked.jsoncis the baked one.deploy.sh --livebrings the running demo back. - D4 A new cycle, not more c112 phases. c112's objective and checks are about a world that rolls forward; they were met and measured. c112 closes with its outcome when the cutover lands.
- D5 S1 deleted (2026-10-06). The S1 spike Worker and container application are gone; its findings are in c112's
s1-cloudflare.md.