@terreno/mcp 57.8.0 → 57.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/docs/upgrades/57.9.0.md +28 -0
  2. package/dist/docs/versioned/next/explanation/charts.md +12 -0
  3. package/dist/docs/versioned/next/explanation/example-coverage.md +1 -1
  4. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +20 -0
  5. package/dist/docs/versioned/next/how-to/README.md +1 -1
  6. package/dist/docs/versioned/next/how-to/admin-import-prebuilt.md +1 -1
  7. package/dist/docs/versioned/next/how-to/charts-and-dashboards.md +5 -0
  8. package/dist/docs/versioned/next/how-to/circleci.md +4 -3
  9. package/dist/docs/versioned/next/how-to/compare-chart-rendered-snapshots.md +8 -0
  10. package/dist/docs/versioned/next/how-to/install-agent-skills.md +1 -1
  11. package/dist/docs/versioned/next/implementationPlans/ai-observability.md +1 -1
  12. package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +1 -1
  13. package/dist/docs/versioned/next/implementationPlans/charts-and-dashboards.md +3 -2
  14. package/dist/docs/versioned/next/implementationPlans/dashboard-chart-parity.md +275 -0
  15. package/dist/docs/versioned/next/reference/ai.md +39 -10
  16. package/dist/docs/versioned/next/reference/environment-variables.md +1 -1
  17. package/dist/docs/versioned/next/reference/feature-flags.md +1 -1
  18. package/dist/docs/versioned/next/reference/ui.md +94 -11
  19. package/dist/docs/versioned/next/tasks/dashboard-chart-parity.md +145 -0
  20. package/package.json +2 -2
  21. package/src/docs/upgrades/57.9.0.md +28 -0
  22. package/src/docs/versioned/next/explanation/charts.md +12 -0
  23. package/src/docs/versioned/next/explanation/example-coverage.md +1 -1
  24. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +20 -0
  25. package/src/docs/versioned/next/how-to/README.md +1 -1
  26. package/src/docs/versioned/next/how-to/admin-import-prebuilt.md +1 -1
  27. package/src/docs/versioned/next/how-to/charts-and-dashboards.md +5 -0
  28. package/src/docs/versioned/next/how-to/circleci.md +4 -3
  29. package/src/docs/versioned/next/how-to/compare-chart-rendered-snapshots.md +8 -0
  30. package/src/docs/versioned/next/how-to/install-agent-skills.md +1 -1
  31. package/src/docs/versioned/next/implementationPlans/ai-observability.md +1 -1
  32. package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +1 -1
  33. package/src/docs/versioned/next/implementationPlans/charts-and-dashboards.md +3 -2
  34. package/src/docs/versioned/next/implementationPlans/dashboard-chart-parity.md +275 -0
  35. package/src/docs/versioned/next/reference/ai.md +39 -10
  36. package/src/docs/versioned/next/reference/environment-variables.md +1 -1
  37. package/src/docs/versioned/next/reference/feature-flags.md +1 -1
  38. package/src/docs/versioned/next/reference/ui.md +94 -11
  39. package/src/docs/versioned/next/tasks/dashboard-chart-parity.md +145 -0
@@ -0,0 +1,28 @@
1
+ # Upgrading to 57.9.0
2
+
3
+ **Action required:** No
4
+ **Affected packages:** @terreno/ai, @terreno/ui
5
+
6
+ ## Summary
7
+
8
+ This release adds durable and resumable GPT chat streams, safer attachment handling, operations-dashboard components, and improved web toast and file-picker behavior. Existing applications continue to work without migration.
9
+
10
+ ## Breaking changes
11
+
12
+ None
13
+
14
+ ## Deprecations
15
+
16
+ None
17
+
18
+ ## New capabilities
19
+
20
+ - Configure `fileStorageService` to persist chat attachments and resume in-flight replies through `GET /gpt/histories/:id/stream`.
21
+ - Compose operations dashboards with scorecards, comparison series, spanning items, and richer chart presentation.
22
+ - Set `maxHeight` on growing `TextField` and `TextArea` components.
23
+
24
+ ## Verification
25
+
26
+ - Run `bun run compile` and `bun run lint` at the app root.
27
+ - Exercise GPT chat attachment upload, reload during a response, and stream resumption when adopting the new AI capabilities.
28
+ - Check web toasts above an open modal and file selection through `FilePickerButton`.
@@ -20,6 +20,14 @@ Height works the same way: `height` is the whole chart, and the tick row, toolti
20
20
  row come out of that budget. A chart therefore fits a fixed-height slot, and reserving the tooltip
21
21
  row even when no tooltip is showing keeps hovering from reflowing the page.
22
22
 
23
+ Cartesian facades use `xTickPolicy="auto"` by default: up to seven categorical labels
24
+ stay horizontal, while denser sets rotate 45 degrees and reserve a taller axis row.
25
+ That taller row comes out of `height`; the plot shrinks so the chart still fits the
26
+ requested slot. `SparklineChart` scales to the data range so a small period-over-period
27
+ change stays visible.
28
+ Callers may force `"truncate"` or `"rotate"`. `title` and `periodLabel` are convenience
29
+ props backed by the same `ChartCard` used for explicit chart/table composition.
30
+
23
31
  `DashboardGrid` is layout only, so it stays on the eager root export. Chart implementations sit behind the same lazy root boundary as `GPTChat`. Cell width subtracts flex `gap` so `md`/`lg` column counts actually fit.
24
32
 
25
33
  Chart paint is proven with **rendered PNG goldens**, not React test-renderer JSON.
@@ -27,3 +35,7 @@ Chart paint is proven with **rendered PNG goldens**, not React test-renderer JSO
27
35
  (`/demo/chart-visual-gallery`), screenshots each fixture from easy to hard, and
28
36
  pixel-diffs against `demo/rendered-snapshots/`. Agents use the `review-chart-visuals`
29
37
  skill to read golden / actual / diff images when a compare fails.
38
+
39
+ The example app admin home (`/admin`) mounts `AdminCharts` so scorecards, a multi-series line,
40
+ bars, an area, and a donut with a center label render together. See
41
+ [Compose charts in a dashboard grid](../how-to/charts-and-dashboards.md).
@@ -21,7 +21,7 @@ that file (and its imports) to a working example. Gaps stay explicit.
21
21
  | Websockets / realtime | yes — `RealtimeApp` in `example-backend/src/server.ts`, `example-backend/src/websockets.ts` | yes — `useSocketConnection` in `example-frontend/app/_layout.tsx` | Sync deltas also use the socket (`SyncApp` + `@terreno/syncdb`). |
22
22
  | Consent forms | yes — `example-backend/src/consentDefinitions.ts`, seed scripts | yes — `example-frontend/app/(tabs)/consents.tsx`, admin `example-frontend/app/admin/consent-forms/` | First-login flow is covered by e2e (`example-frontend/e2e/consents.spec.ts`). |
23
23
  | Product announcements | yes — `AnnouncementsApp` in `example-backend/src/server.ts`, release upload token via `ANNOUNCEMENTS_UPLOAD_TOKEN`, seed `example-backend/src/scripts/seed-announcements.ts`, sample release pack `example-backend/announcements/releases/1.14.0/` with `bun run announcements:pack` | yes — `AnnouncementNavigator` in `example-frontend/app/_layout.tsx` and admin announcement routes | Release packs support multiple Markdown surfaces and idempotent draft or explicit live API import. |
24
- | File upload / GCS | yes — `example-backend/src/api/settings.ts`, AI file routes in `example-backend/src/api/ai.ts` | yes — `example-frontend/app/(tabs)/files.tsx`, `example-frontend/app/gcs-settings.tsx` | |
24
+ | File upload / GCS | yes — `example-backend/src/api/settings.ts`, AI file routes in `example-backend/src/api/ai.ts`, `file-uploads` flag in `example-backend/src/api/fileUploads.ts` | yes — `example-frontend/app/(tabs)/files.tsx`, `example-frontend/app/(tabs)/ai.tsx`, `example-frontend/app/gcs-settings.tsx` | The `file-uploads` feature flag hides upload controls and rejects uploads when off. A missing flag leaves uploads enabled. |
25
25
  | Better Auth | yes — `example-backend/src/utils/betterAuthConfig.ts`, `BetterAuthApp` in `example-backend/src/server.ts` | yes — `example-frontend/app/login.tsx`, `example-frontend/lib/betterAuth.ts` | Default `AUTH_PROVIDER` is `better-auth`. |
26
26
  | syncdb local-first | yes — `SyncApp` in `example-backend/src/server.ts` | yes — `example-frontend/store/syncdb.ts`, `example-frontend/app/(tabs)/index.tsx` | PR #869 closed without merge; `@terreno/syncdb` shipped on the launch line and is the example todos path. |
27
27
  | SyncDB diagnostics | n/a | yes — `debug` registration in `example-frontend/store/syncdb.ts`, debugger UI at `example-frontend/app/syncdb-debug.tsx` | `terreno-mcp-local` can inspect, snapshot/compare/merge, mutate, flush, reconcile/resync, and exercise offline/conflict paths against the running development client. |
@@ -639,6 +639,26 @@ docs are the proof; comms admin stats consume these primitives later.
639
639
 
640
640
  ---
641
641
 
642
+ ## dashboard-chart-parity
643
+
644
+ **Title:** `Ops dashboard chart parity`
645
+
646
+ **Labels:** `area:ui`, `type:feature`
647
+ **Project fields:** Area=`ui`, Target=`Next`, Impact=`Feature`, IP=`dashboard-chart-parity`, Status=`Planned`
648
+
649
+ Follow-up to charts-and-dashboards: scorecards with comparison sparklines, `ChartCard`
650
+ period badges, additive multi-series facades, donut hole labels, spanning `DashboardGrid`
651
+ items, and DataTable period headers / totals / range status so apps can compose a
652
+ “How’s it going?”-class ops dashboard. Terreno theme defaults; no Looker preset. Grammar
653
+ not required.
654
+
655
+ - **Implementation plan:** [dashboard-chart-parity.md](https://github.com/TerrenoLabs/terreno/blob/master/docs/implementationPlans/dashboard-chart-parity.md)
656
+ - **Tasks:** [dashboard-chart-parity.md](https://github.com/TerrenoLabs/terreno/blob/master/docs/tasks/dashboard-chart-parity.md)
657
+ - **RTK flag:** None
658
+ - **Depends on:** charts-and-dashboards
659
+
660
+ ---
661
+
642
662
  ## dark-mode-theme
643
663
 
644
664
  **Title:** `First-class dark mode`
@@ -39,7 +39,7 @@ Problem-oriented, practical steps. Use these when you know what you want to do.
39
39
  - [Upgrade banner](upgrade-banner.md) — Soft warning and hard-block app update UX
40
40
  - [Upgrade Terreno](upgrade-terreno.md) — Lockstep `@terreno/*` bumps, note range, order, rollback
41
41
  - [Track upstream Expo](track-upstream-expo.md) — Morning probe for SDK betas; `release-X.Y.Z` loop log + native fingerprint deps
42
- - [Compose charts in a dashboard grid](charts-and-dashboards.md) — `LineChart` / `BarChart` / `AreaChart` / `DonutChart` inside `Card`s in `DashboardGrid`
42
+ - [Compose charts in a dashboard grid](charts-and-dashboards.md) — `LineChart` / `BarChart` / `AreaChart` / `DonutChart` inside `Card`s in `DashboardGrid` (ops-dashboard scorecards and spans: [dashboard-chart-parity](../implementationPlans/dashboard-chart-parity.md))
43
43
  - [Compare rendered chart snapshots](compare-chart-rendered-snapshots.md) — PNG goldens for the chart visual gallery, not JSON snapshots
44
44
  - [Migrate from @terreno/rtk to @terreno/syncdb](migrate-rtk-to-syncdb.md) — Move data sync to the local-first layer
45
45
  - [Generate a REST CLI from OpenAPI](generate-a-rest-cli.md) — Scaffold `terreno generate rest-cli` for an app API
@@ -18,7 +18,7 @@ const app = new TerrenoApp({userModel: User})
18
18
 
19
19
  - `FeatureFlagsApp`: FeatureFlag model and `feature-flags-overrides` home widget.
20
20
  - `ConsentApp`: ConsentForm and ConsentResponse models plus consent field-widget IDs.
21
- - `DocumentStorageApp`: `documents` custom screen.
21
+ - `DocumentStorageApp`: `documents` custom screen. Pass `fileUploadsEnabled: false` (or a function that returns `false`) to reject uploads while listing and download stay available. The example app wires this to the `file-uploads` feature flag.
22
22
  - `AIAdminApp`: `ai-requests` custom screen and explorer API.
23
23
  - `JobsApp`: `jobs` custom screen and home widget (`dead` / `running` counts). With
24
24
  `accessControl`, requires **both** `admin:access` and `admin:jobs`; without it, legacy
@@ -35,3 +35,8 @@ export const OpsDashboard: FC = () => {
35
35
  Charts have no width prop: each one fills the container you put it in and redraws when that width changes, so size the `Card` or `Box` around it. `height` is the chart's whole height, tick row and tooltip row included, so `height={140}` fits a 140px slot; the tooltip row is always reserved so hovering never reflows the page. Long x labels truncate inside their band rather than widening the chart.
36
36
 
37
37
  Prove paint with `bun run ui:charts:compare` against `demo/rendered-snapshots/`, not with JSON snapshots. See [Compare rendered chart snapshots](compare-chart-rendered-snapshots.md).
38
+
39
+ The example app admin home (`example-frontend/components/AdminCharts.tsx`, mounted
40
+ below the admin dashboard at `/admin`) shows the same pieces together: scorecards with comparison
41
+ sparklines, a multi-series line, a donut with a center label, a bar chart, and an
42
+ area chart inside a spanning `DashboardGrid`.
@@ -242,9 +242,10 @@ restrict `terreno-release` and `terreno-npm` to tag/manual release pipelines.
242
242
  Until contexts exist, e2e jobs may use **project env vars for `E2E_*` secrets only**
243
243
  (or the in-job `ci-e2e-*-secret` fallbacks). **Do not** put `GITHUB_TOKEN` (or any
244
244
  GitHub PAT) in project env vars — those are injected into every job, including
245
- `bun` scripts from the PR. Create `terreno-github-api`, restrict it to this
246
- project, leave fork-PR secret passing off, and attach that context **only** to
247
- `dco` and `architectural-pr-review`. DCO skips if `GITHUB_TOKEN` is unset.
245
+ `bun` scripts from the PR. `terreno-github-api` must hold the PAT as
246
+ `GITHUB_TOKEN`; restrict it to this project and leave fork-PR secret passing off.
247
+ The `always` workflow attaches it **only** to `dco` and `architectural-pr-review`
248
+ (the latter also gets `terreno-agentic`). DCO skips if `GITHUB_TOKEN` is unset.
248
249
  `architectural-pr-review` also skips if `GITHUB_TOKEN` or `CURSOR_API_KEY` is
249
250
  unset, and it skips fork PRs. The job checks out `origin/master` before running
250
251
  the review script so a PR cannot rewrite the reviewer.
@@ -33,4 +33,12 @@ easy-through-hard charts in the demo app and diffs those PNGs against goldens.
33
33
  Agents reviewing a fail should follow the `review-chart-visuals` skill and look at the
34
34
  PNGs, not only the code diff.
35
35
 
36
+ Hard fixture `scorecard-sparkline-comparison` renders five KPI tiles with solid current
37
+ and dotted previous-period sparklines.
38
+ Cartesian parity fixtures are `bar-time-rotated-ticks`, `bar-day-of-week`, and
39
+ `line-three-series`.
40
+ Donut parity fixtures are `donut-center-and-share` and `donut-single-slice`.
41
+ Hard fixture `hows-it-going-dashboard` composes KPI scorecards, time-series charts, donuts,
42
+ and table placeholders in a spanning `DashboardGrid`.
43
+
36
44
  `CHART_VISUAL_BASE_URL` overrides the demo origin when the gallery is already hosted.
@@ -70,7 +70,7 @@ scan stages come from
70
70
  | Docs | `update-docs`, `update-agent-docs`, architecture skills |
71
71
  | GitHub | issues, review, UI verification, release, dependency updates |
72
72
  | Code scans | `terreno-scan-1-aim` … `terreno-scan-5-track`, `terreno-scan-campaign`, `terreno-scan-loop` |
73
- | Expo and native | `track-upstream-expo`, `upgrading-expo`, deployment / EAS skills |
73
+ | Expo and native | `track-upstream-expo`, `upgrading-expo`, `store-assets`, deployment / EAS skills |
74
74
  | Plugin agents | `pre-commit`, `ui-verifier` |
75
75
 
76
76
  `skills.sh.json` at the repo root groups those names on [skills.sh](https://skills.sh).
@@ -274,7 +274,7 @@ Env (example-backend):
274
274
  | `AI_OBS_DATASETS_PRIMARY` | default `local` |
275
275
  | `AI_OBS_EXPERIMENTS_PRIMARY` | must equal datasets |
276
276
  | `AI_OBS_SAMPLE_RATE` | `0`–`1`, default `0` |
277
- | `AI_OBS_PRICE_MAP_JSON` | `{ "gemini-2.5-flash": { "inputPerMTok": 0.1, "outputPerMTok": 0.4 } }` |
277
+ | `AI_OBS_PRICE_MAP_JSON` | `{ "gemini-3.8-flash": { "inputPerMTok": 0.1, "outputPerMTok": 0.4 } }` |
278
278
  | existing Langfuse keys | unchanged |
279
279
  | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | optional OTLP |
280
280
 
@@ -36,7 +36,7 @@ org switcher, dark mode).
36
36
  | C | Communications (pluggable) | `comms-abstraction`, one item **per adapter**: `comms-adapter-expo-push` (first push adapter, per D4), `comms-adapter-twilio-sms`, `comms-adapter-twilio-verify`, `comms-adapter-sendgrid` (first mail adapter, per D2); operations: `comms-admin-dashboard`; consumers: `password-reset-and-email-verification`, `notification-center` |
37
37
  | B | Billing | `inbound-webhooks`, `billing-stripe` (web-first, per D1), `mobile-iap-revenuecat` (Future) |
38
38
  | D | Zero-to-deployed DX | `create-terreno-app`, `mongo-migrations` (deploy items already exist in the OSS launch program) |
39
- | E | B2B UI surfaces | `data-grid-server-filters`, `charts-and-dashboards`, `dark-mode-theme`, `command-palette`, `wizard-stepper`, `wysiwyg-editor` (markdown stays for now, per D3), `global-search` |
39
+ | E | B2B UI surfaces | `data-grid-server-filters`, `charts-and-dashboards`, `dashboard-chart-parity`, `dark-mode-theme`, `command-palette`, `wizard-stepper`, `wysiwyg-editor` (markdown stays for now, per D3), `global-search` |
40
40
  | F | Enterprise & scale | `framework-audit-log` (Next), `rate-limiting` (Next), `job-queues` (Next), `enterprise-sso`, `mfa-step-up-auth` |
41
41
  | — | Native baseline | `native-module-baseline` — all new native modules land in one major release |
42
42
 
@@ -194,8 +194,9 @@ None.
194
194
 
195
195
  ## Not Included / Future Work
196
196
 
197
- - Composition grammar (Phase 6) — recorded Grow decisions; implement on a follow-up Pick.
198
- - Multi-series cartesian beyond `dataKey` / `stackId` once the grammar ships.
197
+ - Ops-dashboard parity (scorecards, comparison sparklines, additive multi-series, donut centers, spanning grid, table chrome) — [`dashboard-chart-parity.md`](dashboard-chart-parity.md).
198
+ - Composition grammar (Phase 6) — recorded Grow decisions; implement on a follow-up Pick. Not required for the How’s it going dashboard.
199
+ - Multi-series cartesian via additive `series` lands in `dashboard-chart-parity`; grammar `dataKey` / `stackId` remains later.
199
200
  - Wiring `CommsDashboardScreen` charts to the stats endpoint.
200
201
  - Example-frontend dashboard.
201
202
  - Zoom/pan.
@@ -0,0 +1,275 @@
1
+ # Implementation Plan: Dashboard chart parity
2
+
3
+ **Status:** Approved — 2026-09-25
4
+ **Branch:** `cursor/dashboard-chart-parity-8a50`
5
+ **Owner:** —
6
+ **Created:** 2026-09-24
7
+ **Roadmap issue:** — (create via `roadmap-item` after this IP is Approved; follow-up to [#1175](https://github.com/TerrenoLabs/terreno/issues/1175))
8
+ **Task list:** [dashboard-chart-parity.md](../tasks/dashboard-chart-parity.md)
9
+ **Depends on:** [charts-and-dashboards.md](charts-and-dashboards.md) (shipped Phases 1–5)
10
+ **RTK deprecation flag:** None
11
+ **Program:** [B2B platform](b2b-platform-program.md) track E
12
+
13
+ ## Goal
14
+
15
+ Make `@terreno/ui` able to compose an ops dashboard like the “How’s it going?”
16
+ reference: KPI scorecards with comparison sparklines, titled chart cards with period
17
+ badges, multi-series lines, donut hole labels, spanning grids, and Looker-like table
18
+ chrome — without copying Looker paint, and without requiring the Chart children grammar.
19
+
20
+ App authors rebuild that page from first-class primitives (`Scorecard`, `SparklineChart`,
21
+ `ChartCard`, spanning `DashboardGrid`, additive chart facades, `DataTable` extras). Proof
22
+ is a demo gallery fixture of the reference layout plus rendered PNG goldens.
23
+
24
+ ## Non-Goals
25
+
26
+ - Pixel-identical Looker colors, fonts, or screenshot matching of the PDF JPEG.
27
+ - Shipping a date-range picker. Period badges may call `onPeriodPress`.
28
+ - Variable `DataTable` row height (would break virtualization).
29
+ - Dual-period numbers inside one table cell (current + previous stacked in the same cell).
30
+ - Replacing `{label, value}[]` facades with composition-only `Chart*` children.
31
+ - Implementing the Phase 6 composition grammar in this slice (still future work on
32
+ `charts-and-dashboards`).
33
+ - Example-frontend screen, comms dashboard wiring, zoom/pan, combo charts, maps, heatmaps.
34
+ - Cross-chart filtering / drill-down.
35
+
36
+ ## Decisions
37
+
38
+ | Question | Decision |
39
+ |----------|----------|
40
+ | Scope | All chart surfaces, headers, period badges, KPI cards, spanning grid, and the table chrome named below. Not the whole PDF pixel-for-pixel. |
41
+ | Easy-build surface | `ChartCard` (title + period badge + child) + `Scorecard` + spanning `DashboardGrid`. |
42
+ | Scorecards | Dedicated `Scorecard` facade plus reusable `SparklineChart`. |
43
+ | Comparison | First-class `comparisonData` with dotted stroke on sparkline and optional cartesian overlay. |
44
+ | Facades vs grammar | Keep `LineChart` / `BarChart` / `AreaChart` / `DonutChart`. Add additive props. This dashboard must not require a composition tree. |
45
+ | Visual language | Capabilities are theme-configurable. Default paint stays Terreno tokens, not a Looker preset. |
46
+ | Responsive grid | Configurable `span` on items. Reference fixture: KPI row 5→2→1; lower chart row 3→1. |
47
+ | Tables | Same IP. After the chart tracer: period header groups, sticky totals row, `1–50 of N` range status. Reuse existing pin/sort/pagination. |
48
+ | Wrapped names | Wrap up to two lines inside the existing `rowHeight`. Keep virtualization. |
49
+ | Period badge | Label plus optional `onPress`. No picker in this slice. |
50
+
51
+ ## Architecture
52
+
53
+ ```
54
+ DashboardGrid
55
+ DashboardGridItem span={{sm,md,lg}}
56
+ ChartCard title periodLabel onPeriodPress?
57
+ Scorecard | LineChart | BarChart | DonutChart | DataTable
58
+ Scorecard
59
+ SparklineChart (plot only; solid + dotted comparison)
60
+ ```
61
+
62
+ Existing facades stay the simple path. `ChartCard` is chrome around any child (chart or
63
+ table). Facade `title` / `periodLabel` / `onPeriodPress` are shortcuts that render the
64
+ same header inside the chart; callers use **either** `ChartCard` **or** those props, not
65
+ both.
66
+
67
+ ### Public props (additive)
68
+
69
+ ```typescript
70
+ interface ChartSeries {
71
+ color?: string;
72
+ data: Array<{color?: string; label: string; value: number}>;
73
+ id: string;
74
+ label: string;
75
+ stroke?: "solid" | "dotted";
76
+ }
77
+
78
+ interface CartesianChartProps {
79
+ comparisonData?: Array<{label: string; value: number}>;
80
+ data: Array<{color?: string; label: string; value: number}>;
81
+ formatValue?: (value: number) => string;
82
+ onPeriodPress?: () => void;
83
+ periodLabel?: string;
84
+ series?: ChartSeries[];
85
+ title?: string;
86
+ xTickPolicy?: "auto" | "truncate" | "rotate";
87
+ // existing: height, legendLabel, emptyText, loading, accessibilityLabel, testID
88
+ }
89
+
90
+ interface DonutChartProps extends CartesianChartProps {
91
+ centerTitle?: string;
92
+ centerValue?: string;
93
+ formatShare?: (value: number, total: number) => string;
94
+ }
95
+
96
+ interface SparklineChartProps {
97
+ comparisonData?: Array<{label: string; value: number}>;
98
+ data: Array<{label: string; value: number}>;
99
+ height?: number;
100
+ testID?: string;
101
+ }
102
+
103
+ interface ScorecardProps {
104
+ comparisonData?: Array<{label: string; value: number}>;
105
+ formatValue?: (value: number) => string;
106
+ onPeriodPress?: () => void;
107
+ periodLabel?: string;
108
+ sparklineData?: Array<{label: string; value: number}>;
109
+ title: string;
110
+ value: number | string;
111
+ testID?: string;
112
+ }
113
+
114
+ interface ChartCardProps {
115
+ children: React.ReactNode;
116
+ filterSummary?: string;
117
+ onPeriodPress?: () => void;
118
+ periodLabel?: string;
119
+ title: string;
120
+ testID?: string;
121
+ }
122
+
123
+ interface DashboardGridItemProps {
124
+ children: React.ReactNode;
125
+ span?: {lg?: number; md?: number; sm?: number};
126
+ testID?: string;
127
+ }
128
+
129
+ interface DataTableProps {
130
+ footerRows?: DataTableCellData[][];
131
+ headerGroups?: Array<{columnCount: number; subtitle?: string; title: string}>;
132
+ rangeStart?: number;
133
+ rangeTotal?: number;
134
+ wrapLines?: 1 | 2; // default 1; 2 = wrap inside rowHeight
135
+ // existing page / setPage / totalPages remain; range copy uses page size math
136
+ }
137
+ ```
138
+
139
+ `series` when present is the cartesian data source (`data` is ignored except as a
140
+ single-series fallback). `comparisonData` aligns on `label` and paints a dotted stroke
141
+ (sparkline/line) or a lighter overlay (bar — if labels match). Default `xTickPolicy` is
142
+ `"auto"`: rotate ~45° when labels would truncate; keep horizontal for ≤7 categorical
143
+ labels.
144
+
145
+ Donut legend rows become `swatch + label + formatShare` (default percent of total).
146
+ `centerTitle` / `centerValue` sit in the hole via overlay `Text`.
147
+
148
+ `DashboardGridItem` span is in column units of the current breakpoint count (span 5 in a
149
+ 5-column KPI row is full width on `sm` when `columns.sm === 1`).
150
+
151
+ `SparklineChart` draws no axes, grid, legend, or reserved tooltip row. Hit targets stay
152
+ ≥24px when press/hover is wired; default is non-interactive (KPI sparkline).
153
+
154
+ `headerGroups` paint a second header row above `columns` (period title + optional
155
+ subtitle such as `(Previous period)`). `footerRows` stick to the bottom of the scroll
156
+ body. Range status is `rangeStart–rangeEnd of rangeTotal` next to existing pagination.
157
+
158
+ ### File layout
159
+
160
+ | Path | Role |
161
+ |------|------|
162
+ | `ui/src/SparklineChart.tsx` | Public sparkline |
163
+ | `ui/src/Scorecard.tsx` | Public KPI tile |
164
+ | `ui/src/ChartCard.tsx` | Public title + period badge chrome |
165
+ | `ui/src/DashboardGridItem.tsx` | Public span wrapper (or named export from `DashboardGrid.tsx` — no barrel) |
166
+ | `ui/src/LineChart.tsx` etc. | Additive `series`, `comparisonData`, ticks, header shortcuts |
167
+ | `ui/src/DonutChart.tsx` | Center stack + share legend |
168
+ | `ui/src/DataTable.tsx` | Header groups, footer rows, range status, wrapLines |
169
+ | `ui/src/Pagination.tsx` | Range copy when totals provided |
170
+ | `demo/chartVisual/fixtureCatalog.ts` | New hard fixtures including `hows-it-going-dashboard` |
171
+ | `docs/how-to/charts-and-dashboards.md` | Rewrite around Scorecard + ChartCard + spans |
172
+
173
+ `DashboardGrid` stays eager. New chart widgets join `heavyOptionalExports`. `ChartCard`
174
+ and `Scorecard` are layout+text heavy enough to sit with charts (lazy). `DashboardGridItem`
175
+ stays eager with the grid.
176
+
177
+ ### Chart visual regression
178
+
179
+ Add fixtures that exercise the reference patterns (not the JPEG pixels):
180
+
181
+ | id | Pattern |
182
+ |----|---------|
183
+ | `scorecard-sparkline-comparison` | Five KPI tiles, solid + dotted sparkline |
184
+ | `bar-time-rotated-ticks` | Vertical bars, rotated dates, y `formatValue` |
185
+ | `line-three-series` | Three lines, markers, per-series legend |
186
+ | `bar-day-of-week` | Seven categorical bars, horizontal ticks |
187
+ | `donut-center-and-share` | Hole label + percent legend |
188
+ | `donut-single-slice` | One 100% slice + center total |
189
+ | `hows-it-going-dashboard` | Spanning grid: 5 KPIs, time bar, 3-line, DOW bar, two donuts, one table card |
190
+
191
+ `bun run ui:charts:compare` must cover these. Agents use `review-chart-visuals`.
192
+
193
+ ## Models / APIs / Notifications
194
+
195
+ None.
196
+
197
+ ## UI
198
+
199
+ Universal React Native + web. Demo stories + visual gallery only. Tables stay `DataTable`
200
+ inside `ChartCard`.
201
+
202
+ ## Phases
203
+
204
+ 1. **Tracer:** `SparklineChart` + `Scorecard` + `ChartCard` + `comparisonData` on the
205
+ sparkline; demo story; docs stub; golden for `scorecard-sparkline-comparison`.
206
+ 2. **Cartesian additive:** `series`, `comparisonData`, `xTickPolicy`, header shortcuts on
207
+ line/bar/area; goldens for time bar and 3-line.
208
+ 3. **Donut:** center stack + share legend; goldens for two-slice and 100% slice.
209
+ 4. **Grid spans:** `DashboardGridItem`; reference KPI wrap; compose `hows-it-going-dashboard`
210
+ charts (table slot placeholder ok until Phase 5).
211
+ 5. **Table chrome:** header groups, footer rows, range status, `wrapLines={2}`; table card
212
+ in the hard fixture.
213
+ 6. **Packaging + docs:** lazy exports, import regression, how-to rewrite, explanation,
214
+ reference, TypeDoc, changelog, ui rule export list.
215
+
216
+ ## Feature Flags & Migrations
217
+
218
+ None. Additive public props. Existing `{label, value}[]` callers stay valid. Not a major.
219
+
220
+ `legendLabel` remains for single-series cartesian. Multi-series uses `series[].label`.
221
+ Donut still ignores `legendLabel`.
222
+
223
+ ## Activity Log & User Updates
224
+
225
+ None.
226
+
227
+ ## Not Included / Future Work
228
+
229
+ - Chart children grammar (`Chart`, `ChartXAxis`, …) — still `charts-and-dashboards` Phase 6.
230
+ - Date-range picker control.
231
+ - Dual-period table cells.
232
+ - Variable row height.
233
+ - Example-frontend / comms wiring.
234
+ - Zoom/pan, combo charts.
235
+
236
+ ## Files to Create / Modify
237
+
238
+ See the task list. Docs in-slice:
239
+
240
+ - `docs/how-to/charts-and-dashboards.md` (rewrite)
241
+ - `docs/explanation/charts.md` (multi-series, scorecard, why not grammar-first)
242
+ - `docs/reference/ui.md` (new exports)
243
+ - `docs/how-to/compare-chart-rendered-snapshots.md` (new fixture ids)
244
+ - `.rulesync/rules/ui/00-ui.mdc`
245
+ - `changelog/unreleased/dashboard-chart-parity.md`
246
+ - TypeDoc props (`bun run website:generate` in the docs task)
247
+
248
+ ## Task List
249
+
250
+ [docs/tasks/dashboard-chart-parity.md](../tasks/dashboard-chart-parity.md)
251
+
252
+ ## Acceptance Criteria
253
+
254
+ - [ ] `Scorecard`, `SparklineChart`, and `ChartCard` are importable from `@terreno/ui` and subpaths.
255
+ - [ ] A scorecard shows title, formatted value, and a sparkline with dotted `comparisonData`.
256
+ - [ ] `LineChart` renders three series from `series` with a legend item each.
257
+ - [ ] Time-series x ticks can rotate; day-of-week ticks stay horizontal under `auto`.
258
+ - [ ] `DonutChart` draws `centerTitle` / `centerValue` and percent legend rows.
259
+ - [ ] `DashboardGridItem` spans reproduce 5→2→1 KPI and 3→1 chart wrapping in the reference fixture.
260
+ - [ ] `DataTable` shows period header groups, a sticky totals row, `1–50 of N`, and two-line wrap inside `rowHeight`.
261
+ - [ ] Existing single-series `{label, value}[]` charts and tests stay green.
262
+ - [ ] `bun run ui:charts:compare` matches goldens including `hows-it-going-dashboard`.
263
+ - [ ] How-to shows the easy-build composition (Scorecard + ChartCard + spans), not only raw `Card` + `LineChart`.
264
+ - [ ] Chart implementations stay lazy on the root; grid item stays eager.
265
+ - [ ] Frontend verification: demo gallery exercised on web; artifacts on the implementation PR.
266
+
267
+ ## Risks
268
+
269
+ | Risk | Mitigation |
270
+ |------|------------|
271
+ | Dual header APIs (`ChartCard` vs facade title) | Document exclusive use; tests assert no double header when only one is set |
272
+ | `DataTable` wrap vs virtualization | Cap at two lines; keep `rowHeight`; screenshot a wrapped first column |
273
+ | Additive `series` vs later grammar | Facades remain the supported easy path; grammar stays a follow-up |
274
+ | Golden flake on dense dashboards | One hard fixture at a fixed gallery width; review-chart-visuals on mismatch |
275
+ | Header groups vs pinned columns | Totals and groups must align with `pinnedColumns` scroll sync; regression tests |
@@ -104,7 +104,7 @@ import {AIService} from "@terreno/ai";
104
104
  import {google} from "@ai-sdk/google";
105
105
 
106
106
  const aiService = new AIService({
107
- model: google("gemini-2.5-flash"),
107
+ model: google("gemini-3.8-flash"),
108
108
  defaultTemperature: 1.0,
109
109
  });
110
110
  ```
@@ -211,7 +211,9 @@ Conversation history with multi-modal prompts.
211
211
  | `userId` | ObjectId | Owner (required) |
212
212
  | `title` | string? | Auto-generated on first `/gpt/prompt` response when empty |
213
213
  | `projectId` | ObjectId? | Optional project association |
214
- | `prompts` | array | Messages: `text`, `type` (`user` \| `assistant` \| `system` \| `tool-call` \| `tool-result`), optional `content` parts, `model`, `rating`, tool fields. `text` is required unless `content` has parts; an image-only assistant response saves `text: ""` |
214
+ | `prompts` | array | Messages: `text`, `type` (`user` \| `assistant` \| `system` \| `tool-call` \| `tool-result`), optional `content` parts, `model`, `rating`, tool fields, `status`, `streamId`. `text` is required unless `content` has parts or `status` is set; an image-only assistant response saves `text: ""` |
215
+
216
+ Assistant replies from `/gpt/prompt` carry `streamId` and `status`: `streaming` while partial text is persisted, then `complete` or `error`. `buildMessages` skips `streaming` replies and `error` replies with no output. Attachment `content` parts carry an optional `gcsKey` when the file was uploaded to durable storage.
215
217
 
216
218
  **Virtual:** `ownerId` aliases `userId` for `Permissions.IsOwner`.
217
219
 
@@ -252,10 +254,36 @@ GPT project with persistent context and memories.
252
254
  | `/gpt/prompt` | POST | `IsAuthenticated` | SSE streaming chat; body: `prompt`, optional `historyId`, `systemPrompt`, `attachments`, `model`, `projectId` |
253
255
  | `/gpt/remix` | POST | `IsAuthenticated` | Non-streaming text remix; body: `{text}` |
254
256
  | `/gpt/histories/:id/rating` | PATCH | `IsAuthenticated` | Rate a prompt; body: `{promptIndex, rating: "up" \| "down" \| null}` |
257
+ | `/gpt/histories/:id/stream` | GET | `IsAuthenticated` (owner) | SSE resume of an in-flight reply; query: optional `streamId`, `offset` |
255
258
  | `/gpt/tools` | GET | `IsAuthenticated` | List builtin + MCP tools |
256
259
 
257
260
  Generated images (image-output models such as `gemini-3-pro-image`) arrive as SSE `image` events: `{image: {mimeType, url}}` with a base64 data URL. Each image is sent once, even when the model reports it both as a stream file part and in the final `result.files`. The saved assistant prompt stores one `image` content part per image. On later turns, `buildMessages` sends an image-only assistant prompt to the model as the text `[Generated image]`, because providers reject empty assistant turns.
258
261
 
262
+ #### Attachments
263
+
264
+ Each `attachments` item is `{type: "image" | "file", url, mimeType, filename?}`. The `url` must be `http(s):` or `data:`. Client-only URLs (`blob:`, `file:`, `content:`, `ph:`) return `400` before streaming starts, because neither the model provider nor a later page load can read them. Upload the file with `POST /files/upload` first, or send it as a `data:` URL.
265
+
266
+ When `fileStorageService` is set, `data:` attachments are uploaded with `FileStorageService.upload`. The saved user prompt then stores the storage `url` plus `gcsKey`, not the base64 payload. The model still receives the original data for that turn. On later turns, parts with a `gcsKey` are sent to the model as 1-hour signed URLs. A failed upload returns `502 Attachment upload failed`. Without storage, attachments are saved as sent.
267
+
268
+ `fileUploadsEnabled` turns uploads off without removing storage. Pass `false`, or a function that returns `false`, and any prompt that includes attachments returns `403 File uploads are disabled` before streaming. Omit it, or pass `true`, to leave uploads enabled. The example app wires this to the `file-uploads` feature flag.
269
+
270
+ #### Stream events and resume
271
+
272
+ `/gpt/prompt` saves the user turn and a `status: "streaming"` assistant placeholder before streaming. While the reply streams, partial text is persisted about every second (`streamPersistIntervalMs`), plus a heartbeat every 10 seconds. The final reply replaces the placeholder with `status: "complete"`. On failure, partial text is kept with `status: "error"`; an empty placeholder is removed.
273
+
274
+ | Event | Sent by | Meaning |
275
+ |-------|---------|---------|
276
+ | `{historyId, started: true, streamId}` | prompt | First event; the turn is saved and resumable |
277
+ | `{historyId, resumed: true, streamId?}` | resume | First event; `streamId` is absent when nothing is streaming |
278
+ | `{text}` | both | Text delta |
279
+ | `{replace: true, text}` | resume | Authoritative whole reply: sent first when the client provides `offset`, and whenever persisted text was rewritten (for example, a step became a tool call) |
280
+ | `{image: {mimeType, url}}` | both | Generated image |
281
+ | `{file}`, `{toolCall}`, `{toolResult}` | prompt | File and tool events |
282
+ | `{error}` | both | Error; resume sends it when the reply ended as `error` or went stale |
283
+ | `{done: true, historyId, title?}` | both | Reply finished |
284
+
285
+ `GET /gpt/histories/:id/stream` re-attaches after a reload or remount. Pass `offset` as the number of characters the client already shows, usually the stored placeholder `text`. The endpoint polls the stored history (`streamResumePollIntervalMs`, default 500 ms), so it works across server instances. A `streaming` reply with no update for `streamStaleAfterMs` (default 60 s) is marked `error`.
286
+
259
287
  AI resolution order: `x-ai-api-key` header + `createModelFn` → `createServerModelFn(modelId)` → configured `aiService` → demo SSE response when `demoMode` and none available.
260
288
 
261
289
  ### addGptHistoryRoutes(router, options?)
@@ -283,7 +311,7 @@ Requires `fileStorageService` and `gcsBucket` (registered by `AiApp` when both a
283
311
 
284
312
  | Endpoint | Method | Auth | Description |
285
313
  |----------|--------|------|-------------|
286
- | `/files/upload` | POST | `IsAuthenticated` | Multipart upload (`file` field); allowed MIME: images, PDF, plain text, CSV, JSON |
314
+ | `/files/upload` | POST | `IsAuthenticated` | Multipart upload (`file` field); allowed MIME: images, PDF, plain text, CSV, JSON. Returns `403` when `fileUploadsEnabled` is off |
287
315
  | `/files/*gcsKey` | GET | None | Returns signed read URL (1 hour) |
288
316
  | `/files/*gcsKey` | DELETE | `IsAuthenticated` (owner) | Soft-delete attachment and remove from GCS |
289
317
 
@@ -311,7 +339,7 @@ Requires `mcpService` (registered by `AiApp` when set).
311
339
  import {AiApp, AIService, FileStorageService, MCPService} from "@terreno/ai";
312
340
  import {google} from "@ai-sdk/google";
313
341
 
314
- const aiService = new AIService({model: google("gemini-2.5-flash")});
342
+ const aiService = new AIService({model: google("gemini-3.8-flash")});
315
343
 
316
344
  new AiApp({
317
345
  aiService,
@@ -320,7 +348,7 @@ new AiApp({
320
348
  mcpService: new MCPService([{name: "tools", transport: {type: "sse", url: "..."}}]),
321
349
  tools: myToolDefinitions,
322
350
  demoMode: false,
323
- createModelFn: (apiKey, modelId) => google(modelId ?? "gemini-2.5-flash", {apiKey}),
351
+ createModelFn: (apiKey, modelId) => google(modelId ?? "gemini-3.8-flash", {apiKey}),
324
352
  openApiOptions: options,
325
353
  }).register(app);
326
354
  ```
@@ -331,7 +359,8 @@ new AiApp({
331
359
  | `createModelFn` | Build model from per-request `x-ai-api-key` |
332
360
  | `createServerModelFn` | Server-side model factory (e.g. Vertex ADC) without per-request key |
333
361
  | `demoMode` | Return canned responses when no AI service resolves |
334
- | `fileStorageService` + `gcsBucket` | Enable file upload routes |
362
+ | `fileStorageService` + `gcsBucket` | Enable file upload routes and durable `/gpt/prompt` attachments |
363
+ | `fileUploadsEnabled` | `false` or a function returning `false` rejects uploads and chat attachments with `403`. Omit to leave uploads enabled |
335
364
  | `mcpService` | Enable MCP routes and tool discovery in chat |
336
365
  | `tools` | Static Vercel AI SDK tool definitions for chat |
337
366
  | `toolChoice` | `"auto"` \| `"none"` \| `"required"` (default `"auto"` when tools present) |
@@ -447,10 +476,10 @@ import {
447
476
  } from "@terreno/ai";
448
477
 
449
478
  const vertex = await createVertexProvider({project: "my-gcp-project"});
450
- const model = vertex.languageModel("gemini-2.5-flash");
479
+ const model = vertex.languageModel("gemini-3.8-flash");
451
480
  ```
452
481
 
453
- Env fallbacks: `GOOGLE_VERTEX_PROJECT`, `GOOGLE_VERTEX_LOCATION` (default `us-central1`).
482
+ Env fallbacks: `GOOGLE_VERTEX_PROJECT`, `GOOGLE_VERTEX_LOCATION` (default `global`).
454
483
 
455
484
  ## Web search types
456
485
 
@@ -463,7 +492,7 @@ import {TerrenoApp} from "@terreno/api";
463
492
  import {AiApp, AIService, LangfuseApp} from "@terreno/ai";
464
493
  import {google} from "@ai-sdk/google";
465
494
 
466
- const aiService = new AIService({model: google("gemini-2.5-flash")});
495
+ const aiService = new AIService({model: google("gemini-3.8-flash")});
467
496
 
468
497
  new TerrenoApp({userModel: User})
469
498
  .register(new AiApp({aiService, openApiOptions: {}}))
@@ -483,7 +512,7 @@ Legacy `setupServer` pattern: call `addGptHistoryRoutes`, `addGptRoutes`, etc. i
483
512
  | Variable | Used by | Description |
484
513
  |----------|---------|-------------|
485
514
  | `GOOGLE_VERTEX_PROJECT` | `createVertexProvider` | GCP project for Vertex models |
486
- | `GOOGLE_VERTEX_LOCATION` | `createVertexProvider` | Vertex region (default `us-central1`) |
515
+ | `GOOGLE_VERTEX_LOCATION` | `createVertexProvider` | Vertex region (default `global`) |
487
516
  | `LANGFUSE_PUBLIC_KEY` | `LangfuseApp` | Langfuse public key |
488
517
  | `LANGFUSE_SECRET_KEY` | `LangfuseApp` | Langfuse secret key |
489
518
  | `LANGFUSE_BASE_URL` | Langfuse client | Langfuse host URL |
@@ -120,7 +120,7 @@ Resolution order for API base URL (`rtk/src/constants.ts`):
120
120
  | `GEMINI_API_KEY` | `@terreno/ai` | ❌ | — | Yes | server |
121
121
  | `OPENAI_API_KEY` | `@terreno/ai` | ❌ | — | Yes | server |
122
122
  | `GOOGLE_VERTEX_PROJECT` | `@terreno/ai` | ❌ | — | No | server |
123
- | `GOOGLE_VERTEX_LOCATION` | `@terreno/ai` | ❌ | — | No | server |
123
+ | `GOOGLE_VERTEX_LOCATION` | `@terreno/ai` | ❌ | `global` | No | server |
124
124
  | `GOOGLE_VERTEX_ALLOWED_MODELS` | `@terreno/ai` | ❌ | all | No | server |
125
125
  | `CLAUDE_KEY` | tooling | ❌ | — | Yes | tooling |
126
126
  | `LANGFUSE_SECRET_KEY` | `@terreno/ai` | ❌ | — | Yes | server |
@@ -63,7 +63,7 @@ New code can also use OpenFeature React hooks (`useBooleanFlagValue`, `useString
63
63
 
64
64
  ## OpenFeature integration
65
65
 
66
- `FeatureFlagsApp` registers an OpenFeature **server** provider (`MongoFeatureFlagProvider`) on a dedicated domain (default `"feature-flags"`) so the global default provider is unchanged. The authenticated bulk endpoint **`GET {basePath}/flagConfiguration`** returns a `FlagConfiguration`-compatible map (suitable for `TypedInMemoryProvider` on the client). The legacy **`GET {basePath}/evaluate`** response shape is unchanged but is **deprecated**: responses include `Deprecation: true` and a `Sunset` HTTP-date header (~90 days). Prefer `useTerrenoFeatureFlags` / `useFeatureFlags` from `@terreno/rtk`, or migrate direct HTTP callers to `/flagConfiguration`.
66
+ `FeatureFlagsApp` registers an OpenFeature **server** provider (`MongoFeatureFlagProvider`) on a dedicated domain (default `"feature-flags"`) so the global default provider is unchanged. The authenticated bulk endpoint **`GET {basePath}/flagConfiguration`** returns a `FlagConfiguration`-compatible map (suitable for `TypedInMemoryProvider` on the client). Disabled **boolean** flags are included and resolve to `off`, so a client can tell a turned-off flag from one that was never created. Disabled variant flags and archived flags are omitted. The legacy **`GET {basePath}/evaluate`** response shape is unchanged but is **deprecated**: responses include `Deprecation: true` and a `Sunset` HTTP-date header (~90 days). Prefer `useTerrenoFeatureFlags` / `useFeatureFlags` from `@terreno/rtk`, or migrate direct HTTP callers to `/flagConfiguration`.
67
67
 
68
68
  ### `defaultVariant` on `FeatureFlag`
69
69