@terreno/mcp 57.3.0 → 57.4.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.
- package/README.md +7 -1
- package/dist/bootstrap.d.ts +0 -18
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +10 -2428
- package/dist/bootstrap.js.map +1 -1
- package/dist/docs/guidelines/admin-frontend/guidelines/core.md +5 -1
- package/dist/docs/guidelines/api/skills/mongoose-schema-safety/SKILL.md +23 -10
- package/dist/docs/resources/syncdb.md +4 -1
- package/dist/docs/upgrades/57.4.0.md +140 -0
- package/dist/docs/versioned/next/README.md +3 -0
- package/dist/docs/versioned/next/explanation/README.md +4 -1
- package/dist/docs/versioned/next/explanation/admin-interface.md +20 -13
- package/dist/docs/versioned/next/explanation/charts.md +29 -0
- package/dist/docs/versioned/next/explanation/deployment-architecture-gcp.md +32 -0
- package/dist/docs/versioned/next/explanation/deployment-baseline.md +1 -1
- package/dist/docs/versioned/next/explanation/example-coverage.md +43 -0
- package/dist/docs/versioned/next/explanation/local-first-data.md +5 -2
- package/dist/docs/versioned/next/explanation/loop-engineering.md +9 -6
- package/dist/docs/versioned/next/explanation/model-router-actions.md +19 -2
- package/dist/docs/versioned/next/explanation/organizations.md +88 -0
- package/dist/docs/versioned/next/explanation/planning-plugins.md +114 -0
- package/dist/docs/versioned/next/explanation/positioning.md +3 -3
- package/dist/docs/versioned/next/explanation/repository-settings.md +6 -5
- package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +44 -34
- package/dist/docs/versioned/next/explanation/source-rules.md +1 -1
- package/dist/docs/versioned/next/explanation/static-analysis.md +76 -16
- package/dist/docs/versioned/next/how-to/README.md +10 -0
- package/dist/docs/versioned/next/how-to/add-gpt-chat-mascot.md +92 -0
- package/dist/docs/versioned/next/how-to/add-organizations.md +204 -0
- package/dist/docs/versioned/next/how-to/admin-add-model.md +9 -1
- package/dist/docs/versioned/next/how-to/admin-import-prebuilt.md +7 -1
- package/dist/docs/versioned/next/how-to/audit-log.md +85 -0
- package/dist/docs/versioned/next/how-to/background-jobs.md +296 -0
- package/dist/docs/versioned/next/how-to/build-admin-screens.md +42 -13
- package/dist/docs/versioned/next/how-to/charts-and-dashboards.md +37 -0
- package/dist/docs/versioned/next/how-to/circleci.md +136 -27
- package/dist/docs/versioned/next/how-to/compare-chart-rendered-snapshots.md +36 -0
- package/dist/docs/versioned/next/how-to/configure-better-auth.md +4 -0
- package/dist/docs/versioned/next/how-to/create-a-terreno-app.md +154 -0
- package/dist/docs/versioned/next/how-to/deploy-backend-to-cloud-run.md +2 -0
- package/dist/docs/versioned/next/how-to/github-actions-ci.md +4 -3
- package/dist/docs/versioned/next/how-to/github-issue-lifecycle.md +12 -8
- package/dist/docs/versioned/next/how-to/in-app-notifications.md +147 -0
- package/dist/docs/versioned/next/how-to/install-agent-skills.md +24 -12
- package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +19 -3
- package/dist/docs/versioned/next/how-to/product-announcements.md +155 -0
- package/dist/docs/versioned/next/how-to/run-mongodb-migrations.md +127 -0
- package/dist/docs/versioned/next/how-to/run-tests-locally.md +102 -0
- package/dist/docs/versioned/next/how-to/seed-a-database.md +7 -2
- package/dist/docs/versioned/next/implementationPlans/README.md +8 -5
- package/dist/docs/versioned/next/implementationPlans/admin-scripts-durable-jobs.md +44 -0
- package/dist/docs/versioned/next/implementationPlans/agent-ui-blocks.md +386 -0
- package/dist/docs/versioned/next/implementationPlans/agentic-sdlc-plugin.md +11 -8
- package/dist/docs/versioned/next/implementationPlans/announcements-surfaces.md +231 -0
- package/dist/docs/versioned/next/implementationPlans/announcements.md +289 -0
- package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +3 -1
- package/dist/docs/versioned/next/implementationPlans/charts-and-dashboards.md +246 -0
- package/dist/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +4 -3
- package/dist/docs/versioned/next/implementationPlans/create-terreno-app.md +177 -0
- package/dist/docs/versioned/next/implementationPlans/datatable-server-side-filtering.md +219 -0
- package/dist/docs/versioned/next/implementationPlans/examples-demo-coverage.md +17 -17
- package/dist/docs/versioned/next/implementationPlans/framework-audit-log.md +247 -0
- package/dist/docs/versioned/next/implementationPlans/gpt-chat-mascot.md +40 -0
- package/dist/docs/versioned/next/implementationPlans/job-queues.md +330 -0
- package/dist/docs/versioned/next/implementationPlans/knip-cleanup.md +136 -0
- package/dist/docs/versioned/next/implementationPlans/migrate-admin-to-syncdb.md +210 -0
- package/dist/docs/versioned/next/implementationPlans/mongodb-migrations-tooling.md +254 -0
- package/dist/docs/versioned/next/implementationPlans/notification-center.md +185 -0
- package/dist/docs/versioned/next/implementationPlans/org-management-ui.md +8 -3
- package/dist/docs/versioned/next/implementationPlans/positioning-django-rails-universal.md +1 -1
- package/dist/docs/versioned/next/reference/README.md +4 -0
- package/dist/docs/versioned/next/reference/admin-backend.md +83 -5
- package/dist/docs/versioned/next/reference/admin-config.md +7 -0
- package/dist/docs/versioned/next/reference/admin-frontend.md +178 -5
- package/dist/docs/versioned/next/reference/admin-spa.md +18 -1
- package/dist/docs/versioned/next/reference/ai.md +2 -0
- package/dist/docs/versioned/next/reference/announcements.md +179 -0
- package/dist/docs/versioned/next/reference/api.md +343 -4
- package/dist/docs/versioned/next/reference/comms.md +33 -0
- package/dist/docs/versioned/next/reference/create-terreno-app.md +44 -0
- package/dist/docs/versioned/next/reference/environment-variables.md +29 -0
- package/dist/docs/versioned/next/reference/jobs.md +319 -0
- package/dist/docs/versioned/next/reference/legacy/rtk.md +5 -1
- package/dist/docs/versioned/next/reference/lifecycle-plugin.md +29 -11
- package/dist/docs/versioned/next/reference/mcp-server.md +2 -43
- package/dist/docs/versioned/next/reference/scan-plugin.md +169 -0
- package/dist/docs/versioned/next/reference/syncdb.md +38 -12
- package/dist/docs/versioned/next/reference/test.md +57 -6
- package/dist/docs/versioned/next/reference/ui-performance.md +2 -1
- package/dist/docs/versioned/next/reference/ui.md +257 -3
- package/dist/docs/versioned/next/tasks/admin-scripts-durable-jobs.md +10 -0
- package/dist/docs/versioned/next/tasks/agent-ui-blocks.md +114 -0
- package/dist/docs/versioned/next/tasks/announcements-surfaces.md +101 -0
- package/dist/docs/versioned/next/tasks/announcements.md +173 -0
- package/dist/docs/versioned/next/tasks/charts-and-dashboards.md +120 -0
- package/dist/docs/versioned/next/tasks/create-terreno-app.md +82 -0
- package/dist/docs/versioned/next/tasks/datatable-server-side-filtering.md +87 -0
- package/dist/docs/versioned/next/tasks/examples-demo-coverage.md +123 -17
- package/dist/docs/versioned/next/tasks/framework-audit-log.md +86 -0
- package/dist/docs/versioned/next/tasks/gpt-chat-mascot.md +13 -0
- package/dist/docs/versioned/next/tasks/job-queues.md +144 -0
- package/dist/docs/versioned/next/tasks/knip-cleanup.md +146 -0
- package/dist/docs/versioned/next/tasks/migrate-admin-to-syncdb.md +209 -0
- package/dist/docs/versioned/next/tasks/mongodb-migrations-tooling.md +107 -0
- package/dist/docs/versioned/next/tasks/notification-center.md +102 -0
- package/dist/docs/versioned/next/tasks/org-management-ui.md +14 -14
- package/dist/docs/versioned/next/tutorials/README.md +1 -1
- package/dist/docs/versioned/next/tutorials/getting-started.md +62 -22
- package/dist/help/excerpt.d.ts +2 -0
- package/dist/help/excerpt.d.ts.map +1 -0
- package/dist/help/excerpt.js +8 -0
- package/dist/help/excerpt.js.map +1 -0
- package/dist/help/updateNotes.d.ts +42 -0
- package/dist/help/updateNotes.d.ts.map +1 -0
- package/dist/help/updateNotes.js +235 -0
- package/dist/help/updateNotes.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/local/tools/databaseQuery.d.ts +1 -0
- package/dist/local/tools/databaseQuery.d.ts.map +1 -1
- package/dist/local/tools/databaseQuery.js +1 -0
- package/dist/local/tools/databaseQuery.js.map +1 -1
- package/dist/packageGuidelines.d.ts +2 -2
- package/dist/packageGuidelines.d.ts.map +1 -1
- package/dist/packageGuidelines.js +2 -8
- package/dist/packageGuidelines.js.map +1 -1
- package/dist/resources.d.ts +3 -0
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +3 -0
- package/dist/resources.js.map +1 -1
- package/dist/search/chunker.d.ts +1 -0
- package/dist/search/chunker.d.ts.map +1 -1
- package/dist/search/chunker.js +1 -0
- package/dist/search/chunker.js.map +1 -1
- package/dist/search/docIndex.d.ts +1 -5
- package/dist/search/docIndex.d.ts.map +1 -1
- package/dist/search/docIndex.js +1 -0
- package/dist/search/docIndex.js.map +1 -1
- package/dist/search/docVersion.d.ts +2 -1
- package/dist/search/docVersion.d.ts.map +1 -1
- package/dist/search/docVersion.js +3 -1
- package/dist/search/docVersion.js.map +1 -1
- package/dist/tools.d.ts +2 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +113 -2
- package/dist/tools.js.map +1 -1
- package/package.json +28 -27
- package/src/docs/guidelines/admin-frontend/guidelines/core.md +5 -1
- package/src/docs/guidelines/api/skills/mongoose-schema-safety/SKILL.md +23 -10
- package/src/docs/resources/syncdb.md +4 -1
- package/src/docs/upgrades/57.4.0.md +140 -0
- package/src/docs/versioned/next/README.md +3 -0
- package/src/docs/versioned/next/explanation/README.md +4 -1
- package/src/docs/versioned/next/explanation/admin-interface.md +20 -13
- package/src/docs/versioned/next/explanation/charts.md +29 -0
- package/src/docs/versioned/next/explanation/deployment-architecture-gcp.md +32 -0
- package/src/docs/versioned/next/explanation/deployment-baseline.md +1 -1
- package/src/docs/versioned/next/explanation/example-coverage.md +43 -0
- package/src/docs/versioned/next/explanation/local-first-data.md +5 -2
- package/src/docs/versioned/next/explanation/loop-engineering.md +9 -6
- package/src/docs/versioned/next/explanation/model-router-actions.md +19 -2
- package/src/docs/versioned/next/explanation/organizations.md +88 -0
- package/src/docs/versioned/next/explanation/planning-plugins.md +114 -0
- package/src/docs/versioned/next/explanation/positioning.md +3 -3
- package/src/docs/versioned/next/explanation/repository-settings.md +6 -5
- package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +44 -34
- package/src/docs/versioned/next/explanation/source-rules.md +1 -1
- package/src/docs/versioned/next/explanation/static-analysis.md +76 -16
- package/src/docs/versioned/next/how-to/README.md +10 -0
- package/src/docs/versioned/next/how-to/add-gpt-chat-mascot.md +92 -0
- package/src/docs/versioned/next/how-to/add-organizations.md +204 -0
- package/src/docs/versioned/next/how-to/admin-add-model.md +9 -1
- package/src/docs/versioned/next/how-to/admin-import-prebuilt.md +7 -1
- package/src/docs/versioned/next/how-to/audit-log.md +85 -0
- package/src/docs/versioned/next/how-to/background-jobs.md +296 -0
- package/src/docs/versioned/next/how-to/build-admin-screens.md +42 -13
- package/src/docs/versioned/next/how-to/charts-and-dashboards.md +37 -0
- package/src/docs/versioned/next/how-to/circleci.md +136 -27
- package/src/docs/versioned/next/how-to/compare-chart-rendered-snapshots.md +36 -0
- package/src/docs/versioned/next/how-to/configure-better-auth.md +4 -0
- package/src/docs/versioned/next/how-to/create-a-terreno-app.md +154 -0
- package/src/docs/versioned/next/how-to/deploy-backend-to-cloud-run.md +2 -0
- package/src/docs/versioned/next/how-to/github-actions-ci.md +4 -3
- package/src/docs/versioned/next/how-to/github-issue-lifecycle.md +12 -8
- package/src/docs/versioned/next/how-to/in-app-notifications.md +147 -0
- package/src/docs/versioned/next/how-to/install-agent-skills.md +24 -12
- package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +19 -3
- package/src/docs/versioned/next/how-to/product-announcements.md +155 -0
- package/src/docs/versioned/next/how-to/run-mongodb-migrations.md +127 -0
- package/src/docs/versioned/next/how-to/run-tests-locally.md +102 -0
- package/src/docs/versioned/next/how-to/seed-a-database.md +7 -2
- package/src/docs/versioned/next/implementationPlans/README.md +8 -5
- package/src/docs/versioned/next/implementationPlans/admin-scripts-durable-jobs.md +44 -0
- package/src/docs/versioned/next/implementationPlans/agent-ui-blocks.md +386 -0
- package/src/docs/versioned/next/implementationPlans/agentic-sdlc-plugin.md +11 -8
- package/src/docs/versioned/next/implementationPlans/announcements-surfaces.md +231 -0
- package/src/docs/versioned/next/implementationPlans/announcements.md +289 -0
- package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +3 -1
- package/src/docs/versioned/next/implementationPlans/charts-and-dashboards.md +246 -0
- package/src/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +4 -3
- package/src/docs/versioned/next/implementationPlans/create-terreno-app.md +177 -0
- package/src/docs/versioned/next/implementationPlans/datatable-server-side-filtering.md +219 -0
- package/src/docs/versioned/next/implementationPlans/examples-demo-coverage.md +17 -17
- package/src/docs/versioned/next/implementationPlans/framework-audit-log.md +247 -0
- package/src/docs/versioned/next/implementationPlans/gpt-chat-mascot.md +40 -0
- package/src/docs/versioned/next/implementationPlans/job-queues.md +330 -0
- package/src/docs/versioned/next/implementationPlans/knip-cleanup.md +136 -0
- package/src/docs/versioned/next/implementationPlans/migrate-admin-to-syncdb.md +210 -0
- package/src/docs/versioned/next/implementationPlans/mongodb-migrations-tooling.md +254 -0
- package/src/docs/versioned/next/implementationPlans/notification-center.md +185 -0
- package/src/docs/versioned/next/implementationPlans/org-management-ui.md +8 -3
- package/src/docs/versioned/next/implementationPlans/positioning-django-rails-universal.md +1 -1
- package/src/docs/versioned/next/reference/README.md +4 -0
- package/src/docs/versioned/next/reference/admin-backend.md +83 -5
- package/src/docs/versioned/next/reference/admin-config.md +7 -0
- package/src/docs/versioned/next/reference/admin-frontend.md +178 -5
- package/src/docs/versioned/next/reference/admin-spa.md +18 -1
- package/src/docs/versioned/next/reference/ai.md +2 -0
- package/src/docs/versioned/next/reference/announcements.md +179 -0
- package/src/docs/versioned/next/reference/api.md +343 -4
- package/src/docs/versioned/next/reference/comms.md +33 -0
- package/src/docs/versioned/next/reference/create-terreno-app.md +44 -0
- package/src/docs/versioned/next/reference/environment-variables.md +29 -0
- package/src/docs/versioned/next/reference/jobs.md +319 -0
- package/src/docs/versioned/next/reference/legacy/rtk.md +5 -1
- package/src/docs/versioned/next/reference/lifecycle-plugin.md +29 -11
- package/src/docs/versioned/next/reference/mcp-server.md +2 -43
- package/src/docs/versioned/next/reference/scan-plugin.md +169 -0
- package/src/docs/versioned/next/reference/syncdb.md +38 -12
- package/src/docs/versioned/next/reference/test.md +57 -6
- package/src/docs/versioned/next/reference/ui-performance.md +2 -1
- package/src/docs/versioned/next/reference/ui.md +257 -3
- package/src/docs/versioned/next/tasks/admin-scripts-durable-jobs.md +10 -0
- package/src/docs/versioned/next/tasks/agent-ui-blocks.md +114 -0
- package/src/docs/versioned/next/tasks/announcements-surfaces.md +101 -0
- package/src/docs/versioned/next/tasks/announcements.md +173 -0
- package/src/docs/versioned/next/tasks/charts-and-dashboards.md +120 -0
- package/src/docs/versioned/next/tasks/create-terreno-app.md +82 -0
- package/src/docs/versioned/next/tasks/datatable-server-side-filtering.md +87 -0
- package/src/docs/versioned/next/tasks/examples-demo-coverage.md +123 -17
- package/src/docs/versioned/next/tasks/framework-audit-log.md +86 -0
- package/src/docs/versioned/next/tasks/gpt-chat-mascot.md +13 -0
- package/src/docs/versioned/next/tasks/job-queues.md +144 -0
- package/src/docs/versioned/next/tasks/knip-cleanup.md +146 -0
- package/src/docs/versioned/next/tasks/migrate-admin-to-syncdb.md +209 -0
- package/src/docs/versioned/next/tasks/mongodb-migrations-tooling.md +107 -0
- package/src/docs/versioned/next/tasks/notification-center.md +102 -0
- package/src/docs/versioned/next/tasks/org-management-ui.md +14 -14
- package/src/docs/versioned/next/tutorials/README.md +1 -1
- package/src/docs/versioned/next/tutorials/getting-started.md +62 -22
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Implementation Plan: In-app notification center
|
|
2
|
+
|
|
3
|
+
**Status:** Approved
|
|
4
|
+
**Branch:** `cursor/notification-center-ip-b11f`
|
|
5
|
+
**Owner:** —
|
|
6
|
+
**Created:** 2026-09-10
|
|
7
|
+
**Approved:** 2026-09-10
|
|
8
|
+
**Roadmap issue:** https://github.com/FlourishHealth/terreno/issues/1179 (this feature **closes** that issue; implementation PRs use `Fixes #1179`)
|
|
9
|
+
**Task list:** [notification-center.md](../tasks/notification-center.md)
|
|
10
|
+
**Depends on:** [comms-abstraction](comms-abstraction.md) (shipped)
|
|
11
|
+
**RTK deprecation flag:** Partial — inbox is a syncdb collection; no new RTK CRUD hooks
|
|
12
|
+
**Program:** [B2B platform](b2b-platform-program.md)
|
|
13
|
+
|
|
14
|
+
## Goal
|
|
15
|
+
|
|
16
|
+
Apps can show an in-app inbox next to `@terreno/comms` mail/SMS/push. Registering
|
|
17
|
+
`NotificationsApp` on `TerrenoApp` adds owner-scoped `Notification` and
|
|
18
|
+
`NotificationPreference` models, a server `notify()` that writes the inbox and fans out
|
|
19
|
+
to enabled comms channels, and `@terreno/ui` bell / inbox / preferences surfaces wired
|
|
20
|
+
through syncdb in the example app.
|
|
21
|
+
|
|
22
|
+
## Non-Goals
|
|
23
|
+
|
|
24
|
+
- Preference **categories** (marketing vs system). v1 is four channel toggles only.
|
|
25
|
+
- Client-authored inbox rows (`create: []` on the notification router).
|
|
26
|
+
- Owner edits to `title` / `body` / `href` / `kind`.
|
|
27
|
+
- Broadcast or fan-out to all users.
|
|
28
|
+
- Email digests, admin notification templates, org-scoped inboxes.
|
|
29
|
+
- Forced `Linking.openURL` for taps.
|
|
30
|
+
- New Socket.io event types or `modelRouter.realtime` (removed in Terreno 58).
|
|
31
|
+
- Making `@terreno/ui` depend on `@terreno/syncdb` or `@terreno/comms`.
|
|
32
|
+
- `@terreno/api` depending on `@terreno/comms` (comms already depends on api).
|
|
33
|
+
|
|
34
|
+
## Decisions
|
|
35
|
+
|
|
36
|
+
| Question | Decision |
|
|
37
|
+
|----------|----------|
|
|
38
|
+
| Package | **`NotificationsApp` in `@terreno/api`** (ConsentApp pattern). UI in `@terreno/ui`. No new workspace package. Comms stays outbound adapters. |
|
|
39
|
+
| Inbox transport | Synced collections, `sync: {scope: {type: "owner"}}`. Live updates via existing `RealtimeApp` `sync:delta`. Hooks: `useQuery` / `useMutate`. |
|
|
40
|
+
| Create | Router `create: []`. Only `getNotificationService().notify(...)` (and tests) insert rows. Direct `Model.create` from app code is not the public seam. |
|
|
41
|
+
| Owner patch | `preUpdate` keeps `readAt` and `archivedAt` (set or `null`). Dismiss archives via `archivedAt` so the row stays in syncdb. `deleted` is retention-only. |
|
|
42
|
+
| Preferences | One `NotificationPreference` per user: `inapp`, `mail`, `push`, `sms`. Missing row = all **on**. |
|
|
43
|
+
| `notify()` | Writes inbox if `inapp` is on; then `sendMail` / `sendSms` / `sendPushToUser` when that channel is on **and** a destination exists. Missing email/phone/tokens skip that channel; inbox write still succeeds. Direct `sendMail` still works; prefs apply only if the app registers `notificationsBeforeSend`. |
|
|
44
|
+
| Comms hook | `@terreno/api` exports a duck-typed `notificationsBeforeSend({channel, userId})` (no import of comms types). Apps pass it as `CommsApp({beforeSend})`. Compose if the app already has a hook. |
|
|
45
|
+
| Tap | Optional `href` on the row. UI calls `onOpen(notification)`. Example-frontend routes internal paths; does not call `Linking.openURL` by default. |
|
|
46
|
+
| Retention | `NotificationsApp({retainDays?: number})` default **`0`** (keep). `retainDays > 0` tombstones (`deleted`) rows older than that; no Mongo TTL index (hard delete breaks syncdb). |
|
|
47
|
+
| Mark all read | `POST /notifications/mark-all-read` (authenticated, owner filter) sets `readAt` on unread rows; change streams update clients. |
|
|
48
|
+
| UI split | Presentational components receive data and callbacks. Example-frontend supplies them with syncdb hooks; the demo uses fixtures. |
|
|
49
|
+
| Admin | No dedicated admin explorer in this IP. Consumer may add the models to `AdminApp` later. |
|
|
50
|
+
| Kind | Optional `kind` string on `notify()` / the document for icons; **not** used for preferences. |
|
|
51
|
+
|
|
52
|
+
## Architecture
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
notify({userId, title, body, href?, kind?})
|
|
56
|
+
→ load NotificationPreference (defaults all on)
|
|
57
|
+
→ if inapp: Notification.create({ownerId: userId, ...}) // syncPlugin stamps _syncSeq
|
|
58
|
+
→ if mail && email: getCommsService().sendMail(...) // optional; skip if no comms
|
|
59
|
+
→ if sms && phone: getCommsService().sendSms(...)
|
|
60
|
+
→ if push: getCommsService().sendPushToUser(...)
|
|
61
|
+
|
|
62
|
+
RealtimeApp change stream → sync:delta → useQuery("notifications")
|
|
63
|
+
|
|
64
|
+
CommsApp({beforeSend: notificationsBeforeSend})
|
|
65
|
+
→ cancel send when prefs.<channel> === false
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`@terreno/api` must not import `@terreno/comms`. Fan-out uses an optional
|
|
69
|
+
`getComms?: () => {sendMail; sendSms; sendPushToUser}` passed into `NotificationsApp`.
|
|
70
|
+
example-backend passes `getCommsService`.
|
|
71
|
+
|
|
72
|
+
### Models
|
|
73
|
+
|
|
74
|
+
**Notification** (`ownerId`, `title`, `body`, `href?`, `kind?`, `readAt?`)
|
|
75
|
+
|
|
76
|
+
- Plugins: `createdUpdatedPlugin`, `isDeletedPlugin`, `syncPlugin`, `findExactlyOne` / `findOneOrNone`.
|
|
77
|
+
- Index: `{ownerId: 1, created: -1}`.
|
|
78
|
+
- Every field has `description`.
|
|
79
|
+
|
|
80
|
+
**NotificationPreference** (`ownerId` unique, `inapp`, `mail`, `push`, `sms` default true)
|
|
81
|
+
|
|
82
|
+
- Same plugins. Synced owner scope so the preferences screen is local-first.
|
|
83
|
+
- Client **may** create/update this collection (upsert defaults). Unique `ownerId`.
|
|
84
|
+
|
|
85
|
+
### APIs
|
|
86
|
+
|
|
87
|
+
| Method | Path | Permissions | Notes |
|
|
88
|
+
|---|---|---|---|
|
|
89
|
+
| — | `GET/PATCH/DELETE /notifications/:id` + list | list/read: authenticated + owner filter; update: owner; delete: owner; **create: []** | `modelRouter` + `sync: {scope: {type: "owner"}}` |
|
|
90
|
+
| POST | `/notifications/mark-all-read` | IsAuthenticated | Sets `readAt` now on owner's unread, non-deleted rows |
|
|
91
|
+
| — | preference CRUD | owner + sync owner | Create allowed (lazy defaults) |
|
|
92
|
+
| service | `notify(input)` | server only | Public TypeScript seam |
|
|
93
|
+
|
|
94
|
+
`preUpdate` on notifications: drop every key except `readAt`. Invalid `readAt` (non-date / non-null) → 400.
|
|
95
|
+
|
|
96
|
+
### UI
|
|
97
|
+
|
|
98
|
+
| Component | Role |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `NotificationBell` | Icon button + unread badge; opens inbox sheet/dropdown |
|
|
101
|
+
| `NotificationInbox` | List unread/read; mark read/unread; dismiss; tap → `onOpen` |
|
|
102
|
+
| `NotificationPreferences` | Four `BooleanField` toggles |
|
|
103
|
+
|
|
104
|
+
Props take data + callbacks (`items`, `unreadCount`, `onMarkRead`, `onDismiss`, `onOpen`, `preferences`, `onChangePreference`). No syncdb import in `@terreno/ui`.
|
|
105
|
+
|
|
106
|
+
### Example app
|
|
107
|
+
|
|
108
|
+
- Register `NotificationsApp` + pass `getCommsService`.
|
|
109
|
+
- `CommsApp({beforeSend: notificationsBeforeSend})` composed with any existing hook.
|
|
110
|
+
- Authenticated demo `POST` (pattern: `commsDev`) to call `notify()` for the current user.
|
|
111
|
+
- `example-frontend`: add collections to syncdb; bell on the todos screen header; settings preferences route; `onOpen` uses Expo Router when `href` is an in-app path.
|
|
112
|
+
|
|
113
|
+
## Notifications
|
|
114
|
+
|
|
115
|
+
This IP **is** the notification system. No extra product pings required to ship it.
|
|
116
|
+
|
|
117
|
+
## Phases
|
|
118
|
+
|
|
119
|
+
1. **Backend tracer:** models, `notify()` inbox write, owner read/update/delete, sync, tests.
|
|
120
|
+
2. **Prefs + comms:** preference document, `notificationsBeforeSend`, optional comms fan-out, mark-all-read, `retainDays`.
|
|
121
|
+
3. **UI + demo:** presentational components, stories, tests.
|
|
122
|
+
4. **Example + docs:** example-backend/frontend, Diátaxis pages, rules, changelog.
|
|
123
|
+
|
|
124
|
+
## Feature Flags & Migrations
|
|
125
|
+
|
|
126
|
+
None. New collections only. `retainDays` default 0 so existing (empty) data is never tombstoned.
|
|
127
|
+
|
|
128
|
+
## Activity Log & User Updates
|
|
129
|
+
|
|
130
|
+
Inbox rows are the user-visible audit. Comms sends still log `CommsMessage`. Do not duplicate comms explorer here.
|
|
131
|
+
|
|
132
|
+
## Not Included / Future Work
|
|
133
|
+
|
|
134
|
+
- Category matrix and marketing defaults.
|
|
135
|
+
- Broadcast / topics / org inbox (`org-management-ui`).
|
|
136
|
+
- Digest email job (`job-queues`).
|
|
137
|
+
- Admin templates and a notification explorer (`comms-admin-dashboard` stays delivery logs).
|
|
138
|
+
- Client create for “reminders I wrote myself”.
|
|
139
|
+
|
|
140
|
+
## Files to Create / Modify
|
|
141
|
+
|
|
142
|
+
**Create**
|
|
143
|
+
|
|
144
|
+
- `api/src/notifications/notificationsApp.ts`
|
|
145
|
+
- `api/src/notifications/notificationService.ts`
|
|
146
|
+
- `api/src/models/notification.ts`, `notificationPreference.ts`
|
|
147
|
+
- `api/src/types/notification.ts`, `notificationPreference.ts`
|
|
148
|
+
- matching `*.test.ts`
|
|
149
|
+
- `ui/src/NotificationBell.tsx`, `NotificationInbox.tsx`, `NotificationPreferences.tsx` + tests
|
|
150
|
+
- `docs/how-to/in-app-notifications.md`
|
|
151
|
+
- `demo/stories/NotificationCenter.stories.tsx` (or equivalent demoConfig entry)
|
|
152
|
+
|
|
153
|
+
**Modify**
|
|
154
|
+
|
|
155
|
+
- `api/src/index.ts` exports
|
|
156
|
+
- `example-backend/src/server.ts` (register plugin + beforeSend)
|
|
157
|
+
- `example-frontend` sync collections, todos header, settings
|
|
158
|
+
- `docs/reference/api.md` (new **In-app notifications** section; do not overload Webhooks & Notifications)
|
|
159
|
+
- `docs/reference/ui.md`, `docs/reference/comms.md` (beforeSend composition)
|
|
160
|
+
- `docs/how-to/README.md`
|
|
161
|
+
- `.rulesync/rules/api/00-api.md` (then `bun run rules`)
|
|
162
|
+
- `changelog/unreleased/`
|
|
163
|
+
|
|
164
|
+
## Task List
|
|
165
|
+
|
|
166
|
+
See [docs/tasks/notification-center.md](../tasks/notification-center.md).
|
|
167
|
+
|
|
168
|
+
## Acceptance Criteria
|
|
169
|
+
|
|
170
|
+
- [ ] `notify()` inserts an owner-scoped row when `inapp` is on; no row when the user turned in-app off.
|
|
171
|
+
- [ ] Sync create of `notifications` is rejected; mark read and dismiss work; `sync:delta` delivers new rows.
|
|
172
|
+
- [ ] `notificationsBeforeSend` cancels mail/SMS/push when that toggle is off.
|
|
173
|
+
- [ ] `notify()` still writes inbox if comms is unregistered or a channel destination is missing.
|
|
174
|
+
- [ ] `retainDays: 0` keeps rows; `retainDays > 0` tombstones old rows without a Mongo TTL index.
|
|
175
|
+
- [ ] Bell shows unread count; inbox mark/dismiss/open; preferences persist via syncdb in example-frontend.
|
|
176
|
+
- [ ] Docs: how-to + api reference + ui props; comms beforeSend note; issue 1179 closable.
|
|
177
|
+
|
|
178
|
+
## Risks
|
|
179
|
+
|
|
180
|
+
| Risk | Mitigation |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `create: []` blocks `notify()` if someone uses the HTTP/sync mutate path only | `notify()` uses the Mongoose model (syncPlugin still stamps `_syncSeq`) |
|
|
183
|
+
| api ↔ comms cycle | Optional `getComms` callback; duck-typed beforeSend |
|
|
184
|
+
| Hard-delete TTL breaks snapshot catch-up | Tombstone via `isDeletedPlugin` only |
|
|
185
|
+
| PHI in `body` | Owner scope + optional `retainDays`; no admin list in v1 |
|
|
@@ -51,7 +51,10 @@ frames are **not** acceptance criteria for this IP (see [Claude Design brief](#c
|
|
|
51
51
|
| Q12 | Who creates orgs | `operator` and `superadmin` only (API + `/admin/orgs`) |
|
|
52
52
|
|
|
53
53
|
**Recorded assumptions (not grilled):** `org-admin` / `member` live on Membership (per org).
|
|
54
|
-
`operator` / `superadmin` live on `user.roles`.
|
|
54
|
+
`operator` / `superadmin` live on `user.roles`. Organizations are **opt-in**
|
|
55
|
+
(`createAccess({ organizations: true })` + `TerrenoApp({ organizations: true })` or `OrgsApp`);
|
|
56
|
+
existing single-tenant apps omit that flag. New apps from bootstrap enable organizations by
|
|
57
|
+
default. Inherit native models, slugs, soft-delete,
|
|
55
58
|
last-admin guards, and `X-Organization-Id` from the superseded IP. Admin data layer stays
|
|
56
59
|
RTK/OpenAPI.
|
|
57
60
|
|
|
@@ -300,8 +303,10 @@ logs in, sees switcher, members table for that org only.
|
|
|
300
303
|
|
|
301
304
|
## Feature Flags & Migrations
|
|
302
305
|
|
|
303
|
-
New collections;
|
|
304
|
-
|
|
306
|
+
New collections; organizations are opt-in (`createAccess({ organizations: true })` plus
|
|
307
|
+
`OrgsApp` / `TerrenoApp({ organizations: true })`). Existing single-tenant apps are unchanged.
|
|
308
|
+
New apps from `terreno_bootstrap_app` enable organizations by default. Adopting apps backfill
|
|
309
|
+
`organizationId` with a documented recipe (formal migrator is `mongo-migrations`).
|
|
305
310
|
|
|
306
311
|
Example-backend **breaking for the example app only:** remove `User.organizationIds`;
|
|
307
312
|
resolve tenants via Membership. Seed users: `operator@example.com` (`operator`), two
|
|
@@ -81,7 +81,7 @@ The Django/Rails claim invites the obvious question. `docs/explanation/positioni
|
|
|
81
81
|
| Auth + permissions | Better Auth + `Permissions` / RBAC | RBAC shipped (`rbac-permissions.md`, Complete) |
|
|
82
82
|
| Templates / views | `@terreno/ui` components, one codebase for iOS/Android/web | SSR is not shipped yet — see [`web-ssr-and-admin-spa`](web-ssr-and-admin-spa.md) |
|
|
83
83
|
| `manage.py` / generators | MCP server tools (the *tool* layer) + the `/terreno-*` SDLC pipeline (the *process* layer) | Agent-driven rather than CLI-driven. Django gives you `manage.py startapp`; Terreno gives you a reviewed path from a request to a mergeable PR |
|
|
84
|
-
| Celery / ActiveJob |
|
|
84
|
+
| Celery / ActiveJob | `@terreno/jobs` `JobsApp` (`jobs/`) — shipped [`job-queues`](job-queues.md) | [How-to](../how-to/background-jobs.md), [reference](../reference/jobs.md); optional GCP Cloud Tasks and Vercel Queues runners (was Not shipped when this IP was written) |
|
|
85
85
|
| Migrations | No migrations framework | Schema evolution is convention + the `mongoose-schema-safety` skill |
|
|
86
86
|
|
|
87
87
|
Being straight about the gaps is what makes the analogy credible rather than marketing.
|
|
@@ -12,9 +12,12 @@ Technical reference for Terreno packages and APIs. Information-oriented, precise
|
|
|
12
12
|
- [@terreno/admin-frontend](admin-frontend.md) — Admin panel UI components
|
|
13
13
|
- [@terreno/admin-spa](admin-spa.md) — Standalone admin SPA + Express serve plugin
|
|
14
14
|
- [@terreno/api-health](api-health.md) — Health check TerrenoPlugin
|
|
15
|
+
- [@terreno/announcements](announcements.md) — In-app product update announcements
|
|
15
16
|
- [@terreno/comms](comms.md) — Pluggable mail, SMS, push, and verification providers
|
|
17
|
+
- [@terreno/jobs](jobs.md) — Durable background jobs, schedules, and queue runners
|
|
16
18
|
- [@terreno/feature-flags](feature-flags.md) — Feature flags and A/B testing plugin
|
|
17
19
|
- [@terreno/mcp](mcp-server.md) — AI coding assistant integration (MCP)
|
|
20
|
+
- [create-terreno-app](create-terreno-app.md) — Scaffold CLI (`bunx create-terreno-app`)
|
|
18
21
|
- [@terreno/test](test.md) — Bun test helpers and in-memory MongoDB
|
|
19
22
|
|
|
20
23
|
## Legacy
|
|
@@ -30,6 +33,7 @@ Technical reference for Terreno packages and APIs. Information-oriented, precise
|
|
|
30
33
|
|
|
31
34
|
- [UI performance](ui-performance.md) — Component render benchmarks
|
|
32
35
|
- [Lifecycle plugin](lifecycle-plugin.md) — Grow/Pick/Roast/Brew/Taste contracts, results, and transitions
|
|
36
|
+
- [Scan plugin](scan-plugin.md) — Aim/Sweep/Sift/Plot/Track goal campaigns that feed the lifecycle
|
|
33
37
|
- [Install agent skills](../how-to/install-agent-skills.md) — Published `skills/` tree for `npx skills`
|
|
34
38
|
- [GitHub issue lifecycle](../how-to/github-issue-lifecycle.md) — Pick-ready issues and Pick plan comments
|
|
35
39
|
- Root [package.json](https://github.com/flourishhealth/terreno/blob/master/package.json) — Workspace scripts and catalog
|
|
@@ -34,7 +34,10 @@ admin.register(app);
|
|
|
34
34
|
``````
|
|
35
35
|
|
|
36
36
|
This creates:
|
|
37
|
-
- `GET /admin/config` — Model metadata endpoint
|
|
37
|
+
- `GET /admin/config` — Model metadata endpoint (`migrations.enabled` when `AdminApp` is given `migrations.dir`)
|
|
38
|
+
- `GET /admin/migrations/status` (`admin:access`) and `POST /admin/migrations/run?wetRun=` (`admin:runScripts`) as `modelRouter` collection actions when `migrations.dir` is set. CRUD on `/admin/migrations` is disabled (405), action permission denials are 405, and responses use `{data: ...}`. Both operations are documented under the `adminMigrations` OpenAPI tag.
|
|
39
|
+
- Poll/cancel those tasks at `GET`/`DELETE /admin/scripts/tasks/:id`
|
|
40
|
+
- Pass `migrations: {dir: "./migrations"}` on `AdminApp`
|
|
38
41
|
- Standard CRUD routes for each model at `{basePath}{routePath}`
|
|
39
42
|
- All routes protected with `Permissions.IsAdmin`, or fine-grained RBAC when `accessControl` is set
|
|
40
43
|
|
|
@@ -42,8 +45,9 @@ This creates:
|
|
|
42
45
|
|
|
43
46
|
``````typescript
|
|
44
47
|
interface AdminOptions {
|
|
45
|
-
models
|
|
48
|
+
models?: AdminModelConfig[];
|
|
46
49
|
basePath?: string; // Default: "/admin"
|
|
50
|
+
organizations?: boolean; // Default: false
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
interface AdminModelConfig {
|
|
@@ -55,16 +59,60 @@ interface AdminModelConfig {
|
|
|
55
59
|
}
|
|
56
60
|
``````
|
|
57
61
|
|
|
62
|
+
## Organization scoping
|
|
63
|
+
|
|
64
|
+
Organization-aware admin CRUD is opt-in so existing single-tenant apps remain
|
|
65
|
+
unchanged. Enable organizations in both access and admin wiring:
|
|
66
|
+
|
|
67
|
+
``````typescript
|
|
68
|
+
const access = createAccess({
|
|
69
|
+
connection: mongoose.connection,
|
|
70
|
+
organizations: true,
|
|
71
|
+
statements,
|
|
72
|
+
userModel: User,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
new AdminApp({
|
|
76
|
+
accessControl: access,
|
|
77
|
+
organizations: true,
|
|
78
|
+
});
|
|
79
|
+
``````
|
|
80
|
+
|
|
81
|
+
When enabled, models whose schema contains `organizationId`:
|
|
82
|
+
|
|
83
|
+
- require organization context through `X-Organization-Id` (single-org
|
|
84
|
+
`org-admin` callers may use context inference);
|
|
85
|
+
- AND `organizationId` into list and search queries;
|
|
86
|
+
- deny read, update, delete, and bulk update for records outside the current org;
|
|
87
|
+
- overwrite client-provided `organizationId` on create with the current org.
|
|
88
|
+
|
|
89
|
+
Models without `organizationId` and every admin app that omits `organizations`
|
|
90
|
+
keep their existing behavior. Organization directory, settings, and membership
|
|
91
|
+
requests use the shared `OrgsApp` routes mounted by
|
|
92
|
+
`TerrenoApp({organizations: true, accessControl: access})`.
|
|
93
|
+
|
|
58
94
|
## Generated Routes
|
|
59
95
|
|
|
60
|
-
For each model, creates standard modelRouter CRUD endpoints:
|
|
96
|
+
For each model, creates standard modelRouter CRUD endpoints plus admin membership helpers:
|
|
61
97
|
|
|
62
|
-
- `GET {basePath}{routePath}` — List (paginated, sortable)
|
|
98
|
+
- `GET {basePath}{routePath}` — List (paginated, sortable). Query params: `page`, `limit`, `sort`, `q` (partial search across string `searchFields`), plus `queryFields` from list/filter metadata. Envelope: `{data, limit, more, page, total}` (`page` is the raw query string when provided)
|
|
99
|
+
- `GET {basePath}{routePath}/search?q=` — Typeahead search. Envelope: `{data}` (limit 20; empty `q` returns `{data: []}`)
|
|
100
|
+
- `POST {basePath}{routePath}/bulk-patch` — Body `{ids: string[], patch: object}`. Success body `{updated}` plus `failures` when any id fails. Ids must pass `mongoose.isValidObjectId` (hex String `_id` values work; arbitrary UUID-like strings are rejected as `"Invalid id"`)
|
|
63
101
|
- `POST {basePath}{routePath}` — Create
|
|
64
102
|
- `GET {basePath}{routePath}/:id` — Read
|
|
65
103
|
- `PATCH {basePath}{routePath}/:id` — Update
|
|
66
104
|
- `DELETE {basePath}{routePath}/:id` — Delete
|
|
67
105
|
|
|
106
|
+
## Scripts
|
|
107
|
+
|
|
108
|
+
`POST {basePath}/scripts/:name/run` creates a `BackgroundTask` and returns `{taskId}`. Poll
|
|
109
|
+
`GET {basePath}/scripts/tasks/:id`. Cancel `DELETE {basePath}/scripts/tasks/:id`.
|
|
110
|
+
|
|
111
|
+
When `JobsApp` is already registered, the run is enqueued as job `admin/script` (see
|
|
112
|
+
[Durable background jobs](../how-to/background-jobs.md#admin-scripts)). Call
|
|
113
|
+
`defineAdminScriptJob` on the worker process with the same script list. The CLI
|
|
114
|
+
(`runScriptCli`) does not enqueue.
|
|
115
|
+
|
|
68
116
|
## Config Endpoint
|
|
69
117
|
|
|
70
118
|
`GET {basePath}/config` returns metadata for all registered models:
|
|
@@ -89,7 +137,8 @@ For each model, creates standard modelRouter CRUD endpoints:
|
|
|
89
137
|
required: false,
|
|
90
138
|
default: false
|
|
91
139
|
}
|
|
92
|
-
}
|
|
140
|
+
},
|
|
141
|
+
adminBroadcast: false
|
|
93
142
|
}
|
|
94
143
|
]
|
|
95
144
|
}
|
|
@@ -102,6 +151,11 @@ Field metadata includes:
|
|
|
102
151
|
- `enum` — Enum values if applicable
|
|
103
152
|
- `default` — Default value
|
|
104
153
|
- `ref` — Referenced model name for ObjectId refs
|
|
154
|
+
- `adminBroadcast` — Always present. `true` when the app `modelRouter` `sync` config set
|
|
155
|
+
`adminBroadcast`, except organization-scoped models (forced to `false`)
|
|
156
|
+
- `organizationScoped` — `true` when `AdminApp({organizations: true})` manages a model with
|
|
157
|
+
an `organizationId` path, except `AuditEvent` (platform log; rows may omit the field)
|
|
158
|
+
- `syncCollection` — Sync collection tag (app `routePath` without a leading slash, e.g. `todos`) when `adminBroadcast` is true; omitted otherwise
|
|
105
159
|
|
|
106
160
|
Field metadata is built from `describeModel()` via `modelDescriptionToAdminFields()` — not from a second OpenAPI property walk. Widget overrides (`fieldOverrides`) remain admin-backend configuration.
|
|
107
161
|
|
|
@@ -112,6 +166,24 @@ without it. Script, configuration, RBAC, and per-model permissions never grant e
|
|
|
112
166
|
|
|
113
167
|
Without `accessControl`, that same page gate uses `Permissions.IsAdmin` (`user.admin`).
|
|
114
168
|
|
|
169
|
+
`AdminApp.register` also installs each model's list/read permissions and `queryFilter` on
|
|
170
|
+
the sync admin window (`registerAdminBroadcastScope`). `GET /sync/entities` and
|
|
171
|
+
`{collection}|admin` deltas then use that contract, not product `IsOwner`. Organization-scoped
|
|
172
|
+
models are excluded because the current sync window protocol does not carry the selected
|
|
173
|
+
organization context; they stay on REST so `X-Organization-Id` and `OrgQueryFilter` cannot be
|
|
174
|
+
bypassed.
|
|
175
|
+
|
|
176
|
+
For writes, AdminApp registers an admin-window mutation scope (`registerAdminWindowMutationScope`).
|
|
177
|
+
Sync clients listed in `createSyncDb({windowCollections})` tag outbox rows with
|
|
178
|
+
`mutationMode: "adminWindow"`. The server does not trust the marker alone: it also requires
|
|
179
|
+
`adminBroadcast`, admin-window access (`admin:access` with RBAC, else `user.admin`), and the
|
|
180
|
+
registered scope. Successful admin-window sync mutations enforce the same create/update/delete
|
|
181
|
+
enabled flags, RBAC/`writeOwned` ownership, readonly/hidden stripping, User admin-flag/role gates,
|
|
182
|
+
and `onAdminAudit` post hooks as REST — via AdminApp executor callbacks on the shared sync write
|
|
183
|
+
pipeline (Mongoose validation, conflict/baseVersion checks, and ledger ordering unchanged).
|
|
184
|
+
Organization-scoped models do not register this scope and use REST mutations. Product clients
|
|
185
|
+
that omit the marker keep product sync permissions and hooks.
|
|
186
|
+
|
|
115
187
|
With `accessControl`, each model can use a standard admin resource with three actions:
|
|
116
188
|
|
|
117
189
|
| Action | Access |
|
|
@@ -165,6 +237,7 @@ writable controls are disabled, and `platformTools` reports visibility for Scrip
|
|
|
165
237
|
and Configuration. Built-in tools use the existing editable permissions:
|
|
166
238
|
|
|
167
239
|
- Scripts: `admin:runScripts` or `admin:viewBackgroundTasks`
|
|
240
|
+
- Migrations apply/dry-run: `admin:runScripts` (status listing uses `admin:access`)
|
|
168
241
|
- Roles: `rbac:read`
|
|
169
242
|
- Version and Configuration: `configuration:read`
|
|
170
243
|
- Audit Log and Feature Flags: their model's admin `read` permission
|
|
@@ -174,6 +247,11 @@ Read and list responses include `_adminCapabilities.update` and
|
|
|
174
247
|
read-only for records the current user does not own. Script metadata separately exposes run and
|
|
175
248
|
history permissions so a history-only role never receives an enabled Run control.
|
|
176
249
|
|
|
250
|
+
When `AuditApp` is registered, successful admin POST/PATCH/DELETE persist append-only
|
|
251
|
+
`AuditEvent` rows with `source: "admin"`. `onAdminAudit` is an extra best-effort sink; it is
|
|
252
|
+
not required for the framework log. Failures in either sink do not change the mutation HTTP
|
|
253
|
+
status. `AuditEvent` itself is never audited.
|
|
254
|
+
|
|
177
255
|
**Important:** Only expose models that should be editable via admin panel. Avoid sensitive internal models.
|
|
178
256
|
|
|
179
257
|
## Best Practices
|
|
@@ -44,6 +44,13 @@ Model contributions contain `model`, `routePath`, and `admin`. Duplicate `routeP
|
|
|
44
44
|
throw for registered routers; the same Mongoose model at two paths gets unique config
|
|
45
45
|
`name`s so the admin UI can route to each.
|
|
46
46
|
|
|
47
|
+
`AdminApp({migrations: {dir}})` enables `GET /admin/migrations/status` (`admin:access`)
|
|
48
|
+
and `POST /admin/migrations/run?wetRun=` (`admin:runScripts`, same as Scripts) as
|
|
49
|
+
`modelRouter` collection actions, plus `migrations.enabled` on `GET /admin/config`.
|
|
50
|
+
Poll/cancel uses existing `/admin/scripts/tasks/:id`. Production wet apply requires
|
|
51
|
+
`ALLOW_MIGRATIONS=true`. Unconfigured apps return 404 for those action routes.
|
|
52
|
+
|
|
53
|
+
|
|
47
54
|
## Legacy migration
|
|
48
55
|
|
|
49
56
|
Move each object from `AdminApp.models` to the model's `modelRouter({admin: ...})`; remove
|
|
@@ -10,6 +10,22 @@ and [Build admin screens](../how-to/build-admin-screens.md).
|
|
|
10
10
|
Home is `AdminHome` inside `AdminProvider` + `AdminShellLayout`. Generic models use
|
|
11
11
|
`AdminScreenRouter` on `[model]/index`. See the how-to for `apiBase` vs `routeBase`.
|
|
12
12
|
|
|
13
|
+
Pass fetch auth on `AdminProvider`:
|
|
14
|
+
|
|
15
|
+
| Host | Props |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| Standalone SPA | `credentials="same-origin"` and `getAuthHeaders` that return `{}` (cookie session). Omit `apiOrigin`. |
|
|
18
|
+
| Embedded app | `getAuthHeaders` that return `Authorization: Bearer …`, plus `apiOrigin` set to the API origin (`@terreno/rtk` `baseUrl`) |
|
|
19
|
+
|
|
20
|
+
RPC that has left RTK uses `bindAdminRequest({credentials, getAuthHeaders, origin})` then `adminRequest`. Relative URLs such as `/admin/config` are prefixed with `origin` when it is set. When `AdminProvider` has `credentials` or `getAuthHeaders`, RPC hooks (`useAdminConfig`, scripts, roles, configuration, documents, comms, consent, version-config, background-tasks, AI explorer, object picker) use that client. Successful comms, scripts, and configuration mutations invalidate mounted fetch queries through the same tag contracts as RTK; those background refetches keep cached data with `isLoading: false` and report `isFetching: true`. Passing only `api` keeps `injectEndpoints`. Do not add axios.
|
|
21
|
+
|
|
22
|
+
Optional `syncDb` on `AdminProvider` enables windowed changelists for models whose
|
|
23
|
+
config includes `adminBroadcast: true` and `syncCollection`. When product screens
|
|
24
|
+
also sync the same collection, inject a dedicated admin client/store configured
|
|
25
|
+
with `windowCollections`; one client cannot join owner/tenant and admin modes for
|
|
26
|
+
the same collection. Do not add `@terreno/syncdb` as a hard dependency of
|
|
27
|
+
admin-frontend.
|
|
28
|
+
|
|
13
29
|
``````typescript
|
|
14
30
|
// app/admin/index.tsx
|
|
15
31
|
import {AdminHome} from "@terreno/admin-frontend";
|
|
@@ -22,6 +38,104 @@ export default function AdminScreen() {
|
|
|
22
38
|
|
|
23
39
|
## Components
|
|
24
40
|
|
|
41
|
+
### OrgDirectoryScreen
|
|
42
|
+
|
|
43
|
+
`OrgDirectoryScreen` is the operator-only organization directory. It renders
|
|
44
|
+
loading, error, and empty states plus actions to create, disable, and open an
|
|
45
|
+
organization. **Open** also calls `selectOrganization` when the directory is
|
|
46
|
+
inside `OrgContextProvider`, so `X-Organization-Id` is set even if the host
|
|
47
|
+
callback only navigates.
|
|
48
|
+
|
|
49
|
+
``````typescript
|
|
50
|
+
const {selectOrganization} = useOrgContext();
|
|
51
|
+
|
|
52
|
+
<OrgDirectoryScreen
|
|
53
|
+
api={api}
|
|
54
|
+
isOperator={currentUser.roles?.includes("operator") ?? false}
|
|
55
|
+
onEnterOrganization={(organization) => {
|
|
56
|
+
selectOrganization(organization);
|
|
57
|
+
router.push(`/admin/orgs/${organization._id}`);
|
|
58
|
+
}}
|
|
59
|
+
routeBase="/admin"
|
|
60
|
+
/>
|
|
61
|
+
``````
|
|
62
|
+
|
|
63
|
+
Pass the same operator check to `AdminShellLayout` to expose the directory in
|
|
64
|
+
navigation. Org-admins do not receive this link:
|
|
65
|
+
|
|
66
|
+
``````typescript
|
|
67
|
+
<AdminShellLayout
|
|
68
|
+
api={api}
|
|
69
|
+
apiBase="/admin"
|
|
70
|
+
isOrganizationOperator={isOperator}
|
|
71
|
+
organizationDirectoryPath="/orgs"
|
|
72
|
+
routeBase="/admin"
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
</AdminShellLayout>
|
|
76
|
+
``````
|
|
77
|
+
|
|
78
|
+
### OrgContextProvider and OrgSwitcher
|
|
79
|
+
|
|
80
|
+
Wrap organization-aware admin routes with `OrgContextProvider` and render
|
|
81
|
+
`OrgSwitcher` through the shell's `organizationSwitcher` slot. Selecting an
|
|
82
|
+
organization navigates to `{routeBase}/orgs/:orgId` and adds
|
|
83
|
+
`X-Organization-Id` to subsequent `useAdminApi` requests. Query cache keys also
|
|
84
|
+
include the organization id, preventing rows cached for one org from appearing
|
|
85
|
+
in another. Derive `initialOrganization` from the current pathname so context
|
|
86
|
+
follows URL changes after mount without overriding a switcher selection while
|
|
87
|
+
navigation is in progress.
|
|
88
|
+
|
|
89
|
+
``````typescript
|
|
90
|
+
const pathname = usePathname();
|
|
91
|
+
const routeOrganization = useMemo(
|
|
92
|
+
() => organizationFromPath(pathname),
|
|
93
|
+
[pathname]
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
<OrgContextProvider initialOrganization={routeOrganization}>
|
|
97
|
+
<AdminShellLayout
|
|
98
|
+
api={api}
|
|
99
|
+
apiBase="/admin"
|
|
100
|
+
organizationSwitcher={<OrgSwitcher api={api} routeBase="/admin" />}
|
|
101
|
+
routeBase="/admin"
|
|
102
|
+
>
|
|
103
|
+
{children}
|
|
104
|
+
</AdminShellLayout>
|
|
105
|
+
</OrgContextProvider>
|
|
106
|
+
``````
|
|
107
|
+
|
|
108
|
+
When `/orgs/mine` returns one organization, `OrgSwitcher` shows its name and
|
|
109
|
+
selects it automatically. With multiple organizations it selects the first by
|
|
110
|
+
name (then id) as a deterministic default and renders a selector.
|
|
111
|
+
Its **Organization** label uses the shell's inverted text color.
|
|
112
|
+
When `/orgs/mine` returns **403** (callers without org-admin memberships, per API
|
|
113
|
+
docs), the switcher renders nothing instead of an error banner. Other failures
|
|
114
|
+
still show "Organizations unavailable".
|
|
115
|
+
|
|
116
|
+
`OrgDirectoryScreen` (operators only) lists every organization, including
|
|
117
|
+
disabled ones. Each row offers **Disable** or **Re-enable**; re-enabling sends
|
|
118
|
+
`PATCH /orgs/:id` with `{disabled: false}`.
|
|
119
|
+
|
|
120
|
+
### OrgSettingsScreen and OrgMembersScreen
|
|
121
|
+
|
|
122
|
+
Use `OrgSettingsScreen` at `/admin/orgs/:orgId` and `OrgMembersScreen` at
|
|
123
|
+
`/admin/orgs/:orgId/members`. Both send the selected organization header.
|
|
124
|
+
|
|
125
|
+
`OrgSettingsScreen` edits the organization name and app-defined settings JSON.
|
|
126
|
+
Type settings in the host app by augmenting `OrganizationSettings` (exported from
|
|
127
|
+
this package and from `@terreno/api`) and reading them with
|
|
128
|
+
`organizationSettingsOf(organization)`. The JSON editor still sends a full
|
|
129
|
+
settings object on save; the backend schema rejects unknown keys.
|
|
130
|
+
It includes a Billing card marked unavailable; billing is outside the
|
|
131
|
+
organization-management feature.
|
|
132
|
+
|
|
133
|
+
`OrgMembersScreen` lists current memberships, attaches an existing user by
|
|
134
|
+
email, changes member roles, and removes members. Backend errors such as
|
|
135
|
+
`Cannot remove the last org-admin` are shown inline. Invite is intentionally
|
|
136
|
+
disabled because invitation tokens and email belong to the later invitations
|
|
137
|
+
feature. The members page uses the full available admin content width.
|
|
138
|
+
|
|
25
139
|
### AdminModelList
|
|
26
140
|
|
|
27
141
|
Entry screen showing all available models as cards.
|
|
@@ -49,10 +163,25 @@ Table view for a specific model with pagination, sorting, and actions.
|
|
|
49
163
|
|
|
50
164
|
Features:
|
|
51
165
|
- DataTable with columns from backend `listFields`
|
|
166
|
+
- Toolbar search maps to list `q` (backend partial match + ObjectId lookup)
|
|
167
|
+
- Declared `filters` map to DataTable column filters (`text` contains, `choice`
|
|
168
|
+
multi `$in`, `boolean`, `dateRange`, `ref` via `AdminRefField` in `renderFilter`)
|
|
169
|
+
- Optional choice fields include **Empty**, which matches missing and null values.
|
|
170
|
+
Admin list requests serialize nested operators with bracket notation independently
|
|
171
|
+
of the host application's RTK base-query configuration.
|
|
52
172
|
- Click row to edit
|
|
53
173
|
- "Create New" button
|
|
54
174
|
- Pagination controls
|
|
55
175
|
- Reference fields render as clickable links
|
|
176
|
+
- Windowed TinyBase path when `AdminProvider` has `syncDb` plus a fetch client (`credentials` or `getAuthHeaders`) and `GET /admin/config` reports `adminBroadcast` + `syncCollection` on a String `_id` model: REST list is membership only, rows overlay TinyBase, and a TinyBase table listener rerenders known rows as `{collection}|admin` deltas arrive. **Refresh** (`testID="admin-table-refresh"`) re-queries REST and calls `hydrateWindow`. **Create** (`testID="admin-create-button"`) is in the table chrome, not the navigator header, because admin stacks use `headerShown: false`. **Save** / **Delete** (`testID="admin-save-button"` / `admin-delete-button`) are in the form chrome for the same reason. Page select-all and bulk actions use the rendered rows, so a row a live tombstone removed leaves the selection. RTK `refetch` error envelopes (`error` / `isError`) toast and skip hydrate; an in-flight Refresh is discarded when page, search, or sort changes. A windowed create or delete never touches the cached REST list, so the form flags the collection through `markAdminWindowMembershipStale` and the changelist refetches membership automatically — whether it stayed mounted behind the form or remounts when the form pops. A create also passes the new id as `awaitId`, because `mutate` only enqueues on the outbox and the first refetch can beat the server; the changelist retries up to three times, 700 ms apart — including after transient list failures — then leaves **Refresh** as the fallback. Passing only `api` keeps the RTK list.
|
|
177
|
+
- Organization-scoped models report `organizationScoped: true` and
|
|
178
|
+
`adminBroadcast: false`. They use REST list and mutation paths until the sync
|
|
179
|
+
window protocol carries selected-organization context. Home widgets delay
|
|
180
|
+
those list queries until `OrgContextProvider` has selected an organization.
|
|
181
|
+
`AdminModelForm` uses the same skip for org-scoped edit reads so create/edit
|
|
182
|
+
does not hit the API with a missing `X-Organization-Id`.
|
|
183
|
+
`AuditEvent` stays `organizationScoped: false` so Recent Activity and the
|
|
184
|
+
audit changelist still load unscoped rows when no organization is selected.
|
|
56
185
|
|
|
57
186
|
### AdminModelForm
|
|
58
187
|
|
|
@@ -77,6 +206,38 @@ Auto-generates fields from model schema:
|
|
|
77
206
|
|
|
78
207
|
System fields (`_id`, `__v`, `created`, `updated`, `deleted`) are automatically skipped.
|
|
79
208
|
|
|
209
|
+
When `AdminProvider` has `syncDb` plus a fetch client and the model config reports
|
|
210
|
+
`adminBroadcast`, `syncCollection`, and a String `_id`, create/update/delete use the
|
|
211
|
+
syncdb mutation outbox. Edit update/delete first hydrate the REST-loaded record so a
|
|
212
|
+
deep-linked form can mutate locally. ObjectId models and hosts without the full
|
|
213
|
+
windowed configuration keep the REST/RTK mutation path. Organization-scoped
|
|
214
|
+
models always use REST/RTK regardless of String `_id`.
|
|
215
|
+
|
|
216
|
+
Pass the host's `useConflicts()` result as `syncConflicts` on `AdminProvider`.
|
|
217
|
+
Windowed form mutations set their own pending state before any asynchronous work, so
|
|
218
|
+
Save/Delete show loading and duplicate presses cannot enqueue a second mutation.
|
|
219
|
+
|
|
220
|
+
Windowed tables and forms render `AdminConflictSheet`, filtered to the ids loaded
|
|
221
|
+
on that page or form, and forward **Use server** / **Keep mine** to syncdb's
|
|
222
|
+
resolver. Only the most recently mounted sheet per collection renders, so a form
|
|
223
|
+
stacked over its changelist shows one sheet instead of two.
|
|
224
|
+
This adapter keeps `@terreno/syncdb` optional for admin-frontend.
|
|
225
|
+
Bulk actions remain server operations: windowed models call the host fetch
|
|
226
|
+
client at `{routePath}/bulk-patch`, while API-only/ObjectId hosts retain RTK.
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
const syncConflicts = useConflicts();
|
|
230
|
+
|
|
231
|
+
<AdminProvider
|
|
232
|
+
api={api}
|
|
233
|
+
apiBase="/admin"
|
|
234
|
+
syncConflicts={syncConflicts}
|
|
235
|
+
syncDb={syncDb}
|
|
236
|
+
>
|
|
237
|
+
{children}
|
|
238
|
+
</AdminProvider>;
|
|
239
|
+
```
|
|
240
|
+
|
|
80
241
|
### AdminRolesList
|
|
81
242
|
|
|
82
243
|
Role editing starts with a dedicated **Admin page** toggle for `admin:access`. That is the only
|
|
@@ -134,20 +295,31 @@ Returns model metadata from `{baseUrl}/config`.
|
|
|
134
295
|
|
|
135
296
|
### useAdminApi
|
|
136
297
|
|
|
137
|
-
|
|
298
|
+
> Deprecated in Terreno 57: do not add new admin `injectEndpoints`. Terreno 58
|
|
299
|
+
> removes `useAdminApi` and the required `api` prop. During the compatibility
|
|
300
|
+
> window, continue passing `api` for ObjectId model CRUD and API-only hosts.
|
|
301
|
+
|
|
302
|
+
Generates compatibility RTK Query hooks for list/read/create/update/delete plus
|
|
303
|
+
`POST {routePath}/bulk-patch`.
|
|
304
|
+
Pass the model's `routePath` from config (for example `/admin/users` or `/admin/todos`), not the admin `baseUrl`.
|
|
305
|
+
|
|
306
|
+
Admin RPC that is leaving RTK uses native `adminRequest` (`AbortController` timeout, JSON or `FormData`, `credentials` forwarded). Bind host auth with `bindAdminRequest`. Do not add axios.
|
|
138
307
|
|
|
139
308
|
``````typescript
|
|
140
309
|
const {
|
|
141
310
|
useListQuery,
|
|
142
|
-
|
|
311
|
+
useReadQuery,
|
|
143
312
|
useCreateMutation,
|
|
144
313
|
useUpdateMutation,
|
|
145
314
|
useDeleteMutation,
|
|
146
|
-
|
|
315
|
+
useBulkPatchMutation,
|
|
316
|
+
} = useAdminApi(api, "/admin/users", "User");
|
|
147
317
|
|
|
148
|
-
const {data, isLoading} = useListQuery({limit: 20, page: 1});
|
|
318
|
+
const {data, isLoading} = useListQuery({limit: 20, page: 1, q: "Ada", sort: "-created"});
|
|
149
319
|
const [create] = useCreateMutation();
|
|
150
320
|
await create({email: "user@example.com"}).unwrap();
|
|
321
|
+
const [bulkPatch] = useBulkPatchMutation();
|
|
322
|
+
await bulkPatch({ids: ["abc"], patch: {name: "Ada"}}).unwrap();
|
|
151
323
|
``````
|
|
152
324
|
|
|
153
325
|
## Expo Router Setup
|
|
@@ -228,7 +400,8 @@ Expects backend to provide:
|
|
|
228
400
|
|
|
229
401
|
When RBAC is enabled, `/admin/config` is filtered for the current user. `AdminShell` uses its
|
|
230
402
|
`platformTools` flags to hide denied Scripts, Roles, Version, and Configuration links, and only
|
|
231
|
-
renders model or custom-screen links returned by the server.
|
|
403
|
+
renders model or custom-screen links returned by the server. The shell lifts Audit Log, Feature
|
|
404
|
+
Flags, and Jobs into the Platform section (Jobs still comes from `customScreens`).
|
|
232
405
|
|
|
233
406
|
### Custom screen page chrome
|
|
234
407
|
|