DRAFT ADR: Reporting and dashboards¶
- Status: Draft — not ratified, and deliberately not numbered. Gated on OI-001, OI-002, OI-003.
- Date: 2026-08-17
- Deciders: El, Lachlan
Three inputs are unresolved and each could change the decision: AroFlo data completeness, Metabase behaviour under a memory limit, and the AGPL position on embedding. Ratify — and assign a number — after the spikes, not before.
Context¶
Two distinct things get called "reporting" and they must not share a build:
- Octopodia's reporting — proof of retainer value, for renewal conversations.
- The client's operational dashboard — their live numbers, on their data. A product they would want whether or not we automate anything.
Greenhills has asked for the second: scheduling, payments and approvals, plus average duration by job type. AroFlo's own reporting is reportedly not comprehensive, and AroFlo↔Xero is disconnected, so the defensible value is the cross-system join rather than better charts over one source (OI-006).
Proposed decision¶
Boundary. Aggregate metrics leave a tenant; rows never do. Each tenant stack pushes counters — action counts, approval latency, exception rate — to the control-plane registry on a schedule. Cross-tenant reporting queries the registry, not N databases. ADR-0006 and ADR-0008 survive intact and no warehouse is built.
Data architecture — layers in the tenant's own Postgres, no warehouse:
| Layer | Contents | Reuse |
|---|---|---|
| Landing | Raw as-pulled, immutable, timestamped. Kept permanently. | Universal shape |
| Mapping | Source-specific translation — the only place AroFlo-isms may exist | Per source |
| Modelled | Canonical: job, job_event, invoice, payment, approval, customer, technician | Universal |
| Serving | Materialised views, one per dashboard metric | Metric library universal; definitions per client |
Keeping landing permanently is the decision that pays for itself: when a client redefines "completed" in month four, the rebuild is local and free rather than a re-pull against AroFlo's 2,000 calls/day cap. Backup posture for each layer is set by ADR-0026, which is a different question from retention on the box.
Reproducibility. Connectors are built per source, not per client. The canonical model is universal — every field-service SMB has those entities, so Simpro or ServiceM8 means a different connector and the same model. Taxonomy, thresholds and metric definitions are config rows, not code (ADR-0003). Each tenant is version-pinned to a model + metric-library version; upgrades are an Ansible run.
Test to apply: could this tenant be rebuilt from the repo and its config rows alone? If not, that is drift, and drift turns ten clients into ten products.
Presentation — two layers.
- Headline metrics (~six numbers) rendered natively in the client review app, in Octopodia brand. Small, fixed, decision-grade. This is the felt-bespoke surface.
- Metabase embedded beneath it for depth and self-serve exploration, at /insights under the tenant's app hostname (ADR-0022 §3). One instance per paying dashboard client (ADR-0023 §5–7).
- Static embedding (free tier, signed JWT). Interactive embedding is Pro-and-above and bills per signed-in viewer, which is fatal across N tenants. The free tier carries a "Powered by Metabase" badge — acceptable because it sits on the depth layer, not the daily view.
- Access control is enforced in our app: the backend decides which dashboards to sign a JWT for, based on the user's role in the review app. Metabase manages no users and the client never sees a Metabase login. Per-tenant instances mean no data sandboxing or row-level security is needed — the instance is the boundary.
Bespoke, in three tiers: - Base pack — every field-service client. Versioned, upgraded centrally, provisioned via the Metabase REST API from definitions committed in the repo. - Catalogue — proven cards, switched on per tenant by config. Built once, reused. - Bespoke — one client only, in their config. Priced as build plus maintenance (OI-024).
Rule: the second time a client asks for something, it moves from bespoke to catalogue. Bespoke logic goes in the serving views; bespoke presentation is not sold, because Metabase theming is shallow on the free tier.
Fact vs model. "412 invoices processed, 9 exceptions" is fact. "38 hours returned" is a model using a minutes-saved figure agreed with the client at Blueprint (OI-009). Present them differently.
Sequencing. Read-only dashboard first, on real data, for a few weeks before any write-back to Xero. Reading their data wrong is embarrassing; writing to their accounting system wrong is expensive. A visibly wrong dashboard also proves the sync on day one instead of month three.
Open before ratification¶
See OI-001 to OI-009 and OI-029 to OI-031 in docs/OPEN-ITEMS.md.
Watch item — not now¶
DuckDB-WASM + Parquet (OI-033): browser-embedded analytical database over static Parquet exports. No backend, no JVM, no AGPL question, no per-tenant CVE patching. Costs: the whole file lands in the browser so row-level access control is impossible; snapshot not stream; the charting layer becomes our build. Strong candidate for the native headline layer after Greenhills ships — the serving views export to Parquet either way, so nothing is wasted by waiting.