@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,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
 
@@ -4,9 +4,9 @@ React Native UI component library (a large component library). Layout (Box, Page
4
4
 
5
5
  ## Key exports
6
6
 
7
- - Layout: `Box`, `Page`, `SplitPage`, `Card`, `DashboardGrid`
7
+ - Layout: `Box`, `Page`, `SplitPage`, `Card`, `ChartCard`, `DashboardGrid`, `DashboardGridItem`
8
8
  - Forms: `TextField`, `SelectField`, `DateTimeField`, `CheckBox`
9
- - Display: `Text`, `Heading`, `Badge`, `DataTable`, `LineChart`, `BarChart`, `AreaChart`, `DonutChart`
9
+ - Display: `Text`, `Heading`, `Badge`, `DataTable`, `Scorecard`, `SparklineChart`, `LineChart`, `BarChart`, `AreaChart`, `DonutChart`
10
10
  - Actions: `Button`, `IconButton`, `Link`
11
11
  - Feedback: `Spinner`, `Modal`, `Toast`
12
12
  - Notifications: `NotificationBell`, `NotificationInbox`, `NotificationPreferences`
@@ -32,11 +32,11 @@ supported and is convenient when startup cost is not material:
32
32
  import {Box, DataTable, Icon} from "@terreno/ui";
33
33
  ```
34
34
 
35
- Heavy optional widgets (`GPTChat`, `EmojiSelector`, `MarkdownEditor`, consent flows, `LineChart`, `BarChart`, `AreaChart`, `DonutChart`, and related admin tools) are
35
+ Heavy optional widgets (`GPTChat`, `EmojiSelector`, `MarkdownEditor`, consent flows, `ChartCard`, `Scorecard`, `SparklineChart`, `LineChart`, `BarChart`, `AreaChart`, `DonutChart`, and related admin tools) are
36
36
  re-exported from the root entry through lazy boundaries. Importing them from `@terreno/ui` stays type-compatible, but
37
37
  their implementation modules load on first render instead of during the initial root import. `DashboardGrid` stays eager.
38
- their implementation modules load on first render instead of during the initial root import. `MarkdownView` and
39
- `DataTable` header info defer `react-native-markdown-display`; `EmojiSelector` defers `emoji-datasource` until open.
38
+ `MarkdownView` and `DataTable` header info defer `react-native-markdown-display`;
39
+ `EmojiSelector` defers `emoji-datasource` until open.
40
40
 
41
41
  For the smallest cold-start graph, keep using subpaths for screens that only need a few primitives (for example
42
42
  `import {Button} from "@terreno/ui/Button"`).
@@ -60,21 +60,72 @@ const customStyle: StyleProp<ViewStyle> = {
60
60
  - Ensures type compatibility when passing styles to @terreno/ui components
61
61
  - Simplifies imports (one package instead of two)
62
62
 
63
- ### DashboardGrid
63
+ ### DashboardGrid and DashboardGridItem
64
64
 
65
65
  Eager layout-only wrapping grid. Default columns `{sm: 1, md: 2, lg: 3}`. Children stay caller-supplied `Card`s. Cell width is `(rowWidth - gap × (columns - 1)) / columns` so flex `gap` does not wrap extra columns.
66
+ Wrap a child in `DashboardGridItem` when it must span more than one responsive column.
67
+ Plain children remain one column.
66
68
 
67
69
  ```tsx
68
- <DashboardGrid>
70
+ <DashboardGrid columns={{sm: 1, md: 2, lg: 4}}>
71
+ <DashboardGridItem span={{sm: 1, md: 2, lg: 2}}>
72
+ <Card>
73
+ <LineChart data={points} legendLabel="Signups" />
74
+ </Card>
75
+ </DashboardGridItem>
69
76
  <Card>
70
77
  <LineChart data={points} legendLabel="Signups" />
71
78
  </Card>
72
79
  </DashboardGrid>
73
80
  ```
74
81
 
82
+ ### ChartCard
83
+
84
+ Card chrome for a chart or table: title, optional filter summary, and an optional period
85
+ badge. Pass `onPeriodPress` to make the badge a button; omit it for display-only copy.
86
+
87
+ ```tsx
88
+ <ChartCard title="Cost by Device" periodLabel="Last 30 days">
89
+ <DonutChart data={points} />
90
+ </ChartCard>
91
+ ```
92
+
93
+ ### Scorecard
94
+
95
+ Compact KPI tile with a title, formatted value, and optional current/comparison
96
+ sparkline. Numeric values use `formatValue`; string values render unchanged.
97
+
98
+ ```tsx
99
+ <Scorecard
100
+ title="Cost"
101
+ value={569}
102
+ formatValue={(value) => `$${value}`}
103
+ sparklineData={current}
104
+ comparisonData={previous}
105
+ />
106
+ ```
107
+
108
+ ### SparklineChart
109
+
110
+ Plot-only line chart for compact metrics. `data` draws the current solid line;
111
+ `comparisonData` draws a dotted previous-period line on the same scale. It has no axes,
112
+ grid, legend, tooltip row, or empty-state copy.
113
+
114
+ ```tsx
115
+ <SparklineChart
116
+ data={[{label: "Mon", value: 3}, {label: "Tue", value: 5}]}
117
+ comparisonData={[{label: "Mon", value: 4}, {label: "Tue", value: 4}]}
118
+ />
119
+ ```
120
+
75
121
  ### LineChart
76
122
 
77
- Single-series line chart drawn with `react-native-svg`. Empty data shows `emptyText` (default `"No data"`). `loading` shows a `Spinner`. Press or hover a point for `{label}: {value}`.
123
+ Line chart drawn with `react-native-svg`. Keep the simple `data` + `legendLabel` path for
124
+ one series, or pass named `series` for multiple lines and legend items.
125
+ `comparisonData` adds a dotted previous-period overlay on the shared scale. Empty data
126
+ shows `emptyText` (default `"No data"`). `loading` shows a `Spinner`.
127
+ Set `xTickPolicy` to `auto` (default), `rotate`, or `truncate`. `title`,
128
+ `periodLabel`, and `onPeriodPress` use the same header chrome as `ChartCard`.
78
129
 
79
130
  ```tsx
80
131
  <LineChart
@@ -88,7 +139,10 @@ Hit targets use `Box` `onClick`, so testIDs are `{testID}.point.{index}-clickabl
88
139
 
89
140
  ### BarChart
90
141
 
91
- Single-series bar chart on the same owned-SVG contract as `LineChart` (empty, loading, legend, tooltip).
142
+ Bar chart on the same owned-SVG contract as `LineChart` (empty, loading, legend,
143
+ tooltip, tick policy, and header shortcuts). `comparisonData` paints lighter bars behind
144
+ the current period. When `series` is passed in this facade, its first entry supplies the
145
+ bars and legend; grouped bars remain future work.
92
146
 
93
147
  ```tsx
94
148
  <BarChart
@@ -101,6 +155,8 @@ Single-series bar chart on the same owned-SVG contract as `LineChart` (empty, lo
101
155
  ### AreaChart
102
156
 
103
157
  Filled area plus line on the same owned-SVG contract as `LineChart`.
158
+ Named `series` render one fill/line pair and legend item each; `comparisonData` stays a
159
+ dotted line rather than a filled area.
104
160
 
105
161
  ```tsx
106
162
  <AreaChart
@@ -112,11 +168,16 @@ Filled area plus line on the same owned-SVG contract as `LineChart`.
112
168
 
113
169
  ### DonutChart
114
170
 
115
- One slice per `{label, value}` point. Per-slice `color` overrides the theme paint. Legend is one row per slice (`legendLabel` is ignored).
171
+ One slice per `{label, value}` point. Per-slice `color` overrides the theme paint.
172
+ Legend rows show label + percent by default; customize with `formatShare`.
173
+ `centerValue` and `centerTitle` put an aggregate in the donut hole. `title` /
174
+ `periodLabel` use `ChartCard` chrome (`legendLabel` remains ignored).
116
175
 
117
176
  ```tsx
118
177
  <DonutChart
119
178
  data={[{label: "Open", value: 3}, {color: "#112233", label: "Closed", value: 5}]}
179
+ centerValue="8"
180
+ centerTitle="Total"
120
181
  testID="status-donut"
121
182
  />
122
183
  ```
@@ -189,6 +250,14 @@ Buttons automatically size to their content unless `fullWidth` is specified:
189
250
 
190
251
  Internally, Button sets `alignSelf: 'flex-start'` when `fullWidth={false}` to prevent stretching in column layouts.
191
252
 
253
+ ### TextField grow and maxHeight
254
+
255
+ `grow` makes a multiline field expand with its content. Pair it with `maxHeight` (pixels) to cap the expansion; past the cap the content scrolls.
256
+
257
+ ```tsx
258
+ <TextArea grow maxHeight={200} onChange={setNotes} value={notes} />
259
+ ```
260
+
192
261
  ### TextField password visibility
193
262
 
194
263
  `type="password"` masks the value and renders a show/hide eye control at the end of the field.
@@ -295,7 +364,15 @@ Streaming chat surface for `@terreno/ai`. Histories, messages, submit, and optio
295
364
 
296
365
  Pass `mascot` when the app owns a character. Terreno ships none. The node renders only while `currentMessages` is empty, centered in the chat panel above the suggested prompts. The empty hero uses the message viewport height as a minimum so short content stays centered, while taller mascots still scroll. Streaming feedback shares that centered hero instead of creating a second pane. Omit the prop for the default empty chat.
297
366
 
298
- The composer row (attachment picker, tools, input, Send) is vertically centered, so controls stay aligned with the input as it grows. The attachment cell is omitted when `onAttachFiles` is not provided.
367
+ The composer row (attachment picker, tools, input, Send) is vertically centered, so controls stay aligned with the input as it grows. The composer input grows with its text up to 200px, then scrolls. The attachment cell is omitted when `onAttachFiles` is not provided.
368
+
369
+ Each history row keeps rename and delete in a three-dot overflow menu (`gpt-history-menu-{id}`), anchored with `DropdownPanel`. Rename uses an outlined pencil in the dark secondary text color. Titles truncate before the menu trigger, so the trigger stays aligned at any sidebar width.
370
+
371
+ "Scroll to bottom" appears only when content sits more than 100px below the viewport. It never shows in an empty chat.
372
+
373
+ Image content parts render copy-image and (web) download actions. The message copy button on an image-only reply copies the image, not text. On web the image is written to the clipboard as PNG; on native it uses `expo-clipboard` `setImageAsync` for `data:` URLs.
374
+
375
+ The attach control (`FilePickerButton`) opens an anchored dropdown with **Photo Library** and **Document**. On web both open a browser file input from the press itself and return `data:` URLs, so attachments survive a reload; on native they use `expo-image-picker` and `expo-document-picker`.
299
376
 
300
377
  ```tsx
301
378
  <GPTChat
@@ -362,6 +439,12 @@ Buttons use a scale animation by default. Set `pressAnimation="opacity"` for an
362
439
 
363
440
  Disabled and loading buttons use a non-interactive pressable regardless of the selected animation.
364
441
 
442
+ ### Toast
443
+
444
+ `TerrenoProvider` mounts the toast container. Call `useToast()` for `success`, `info`, `warn`, `error`, `show`, `hide`, and `catch`.
445
+
446
+ On web, that container is portaled to `document.body` (`position: fixed`, `z-index: 999999`, `pointerEvents: "box-none"`), so a toast stays above an open `Modal` and its backdrop. The web container is sized with `100%` rather than the measured window, so statically exported pages (where `Dimensions` reports 0×0 at build time) still center the toast on screen. Native iOS and Android keep the in-tree absolute container sized to the window.
447
+
365
448
  ## Authentication Components
366
449
 
367
450
  ### SocialLoginButton