Start here

ADR-0003: The dashboard is a private Claude artifact fed each morning

Status: accepted · 2026-10-18

Context

ADR-0001 put the operator dashboard on TanStack Start behind Cloudflare Access, built from @botgumi/dashboard-ui. Nothing of that exists yet: dashboard-ui holds types only, the vendor read adapters throw, and templates/app/dashboard/ is empty.

The first app built this way already runs a dashboard that its owner opens every day, in a different form:

  • The page is a private Claude Dashboard artifact, visible to the owner only. No hosting, no domain, no Access policy, no login code.
  • Live sources are read through the viewer's own Claude connectors each time the page opens: analytics (events, funnels, retention, grouped counts) and crash reporting (open issues, errors by level).
  • Morning sources are the ones no connector reaches: revenue, the App Store, the waitlist (D1), server counts (Analytics Engine, D1, Workers analytics), website visits (Web Analytics) and cloud spend. A scheduled task in the Claude app runs a read-only collector each morning, after the app's own morning jobs. It writes one table per source and points the page's sources at the new files.
  • Every number says where it comes from. A click shows the query, the file or the calculation.
  • Failure is local. A source that fails is named on its panel and the others still load. A key that is not set shows "not connected" with the step that fixes it, not an error.
  • Freshness is visible. A morning source older than 48 hours shows as stale. The scheduled task runs only while the Claude app is open; a missed morning runs when it opens next, so one missed day is normal.
  • Counts only. No email, user ID, device ID, position or trace is on the page or in its files.

It took an afternoon to build, needs no deploy, and has run without upkeep beyond adding sources. Its sources outgrew AppSources (3 readers for about 9 sources) and PanelState (no stale, no not-connected, no provenance).

Decision

D1. The default form is a private Claude Dashboard artifact plus a scheduled morning collector. The template's dashboard/ track holds three files: collect.mjs (reads the morning sources, writes one table per source, only reads), page.html (the page: panels, charts, the loaders for live sources, the calculations) and build.mjs (fills the loaders into the page and writes the live source definitions). The artifact is private to the owner; sharing it is the owner's choice in Claude.

D2. TanStack Start behind Cloudflare Access stays as an option, not the default. An app opts in when the dashboard must reach people without Claude (a team, an investor), when it must be up while nobody has the Claude app open, or for a portfolio view across apps. The web form uses the same readers, the same panel states and the same review rules. It is scaffolded when the first app asks for it, not before.

D3. One read layer for both forms. @botgumi/dashboard-ui defines AppSources: the provider readers (metrics, issues, revenue) plus readers for the fixed-core sources (store, waitlist, server, website, spend). The collector and the web form call these readers; the page never calls a vendor. In the artifact form a source with a Claude connector is read live through the connector instead; it moves from morning to live when a connector for it exists.

D4. Panel states. Every panel handles loading, empty, error, offline, not_connected, stale and ready. error names the failing source. not_connected names the missing key and the step that sets it. ready and stale carry the data and its provenance: the source, how the number was got (a query, a file or a calculation over other sources), when it was read, and a link to the source's own view when it has one.

D5. Freshness. A source is live or morning. A morning source read more than 48 hours ago is stale: the panel still draws its data, with its age. Live sources are never stale.

D6. Failure is per source. The collector catches each source on its own, writes the others, and names the ones that failed. The page loads each source on its own. One source failing never blanks another panel.

D7. Counts only. The dashboard holds counts, sums, versions, states and public store facts (rating counts and averages). No email, user ID, device ID, position, trace or review text; the panel links to the source for those.

D8. No number typed into the page. Every number on the page comes from a source or a named calculation over sources. Goals drawn on the page (for example a share of lost sessions under a bar) come from the "Quality bar" section of the app's PRD (docs/templates/prd-review.md), cited by line; a goal with no PRD line is not drawn.

D9. Keys. The collector reads its keys from the app's local .env and the asc profile, each listed in the secrets inventory, each read-only and scoped to the app's own project where the vendor allows it. No key reaches the page or the tables. A missing key gives not_connected, never a failed run.

D10. Cloud spend is an estimate and says so. Usage from the provider's analytics, priced with its published list prices: so far this month, and month end as so far plus the median day of the last 7 for each day left. Bulk one-off days (for example data uploads) count on their day and are left out of the pace. AI spend from @botgumi/spend-guard's ledger is one line of it.

This amends ADR-0001, decision 5: TanStack Start + Cloudflare Pages remains the web stack for the landing page and for the optional web dashboard, not the dashboard's default form.

Consequences

  • An app's dashboard needs no domain, no deploy and no Access policy by default. The go-live checklist asks for Access only when the app chose the web form.
  • The dashboard depends on the owner's Claude plan, its connectors and the Claude app being open in the morning. The stale state makes a missed morning visible instead of silent.
  • The dashboard track's CI is pnpm test (the collector's table shaping and the page's calculations, against fixtures), not pnpm build, unless the app chose the web form.
  • AppSources grows from 3 readers to 8; each new reader ships a no-op and a contract suite. MetricsReader gains funnels, retention and grouped counts; RevenueReader gains a daily series and trial conversion. The stub adapters must implement them before their contract suites can be enabled.
  • Follow-up: scaffold templates/app/dashboard/ (collect.mjs, page.html, build.mjs and their tests) from the readers, and a dashboard.form choice ("artifact" or "web") in botgumi.config.ts for the generator.