@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.
Files changed (250) hide show
  1. package/README.md +7 -1
  2. package/dist/bootstrap.d.ts +0 -18
  3. package/dist/bootstrap.d.ts.map +1 -1
  4. package/dist/bootstrap.js +10 -2428
  5. package/dist/bootstrap.js.map +1 -1
  6. package/dist/docs/guidelines/admin-frontend/guidelines/core.md +5 -1
  7. package/dist/docs/guidelines/api/skills/mongoose-schema-safety/SKILL.md +23 -10
  8. package/dist/docs/resources/syncdb.md +4 -1
  9. package/dist/docs/upgrades/57.4.0.md +140 -0
  10. package/dist/docs/versioned/next/README.md +3 -0
  11. package/dist/docs/versioned/next/explanation/README.md +4 -1
  12. package/dist/docs/versioned/next/explanation/admin-interface.md +20 -13
  13. package/dist/docs/versioned/next/explanation/charts.md +29 -0
  14. package/dist/docs/versioned/next/explanation/deployment-architecture-gcp.md +32 -0
  15. package/dist/docs/versioned/next/explanation/deployment-baseline.md +1 -1
  16. package/dist/docs/versioned/next/explanation/example-coverage.md +43 -0
  17. package/dist/docs/versioned/next/explanation/local-first-data.md +5 -2
  18. package/dist/docs/versioned/next/explanation/loop-engineering.md +9 -6
  19. package/dist/docs/versioned/next/explanation/model-router-actions.md +19 -2
  20. package/dist/docs/versioned/next/explanation/organizations.md +88 -0
  21. package/dist/docs/versioned/next/explanation/planning-plugins.md +114 -0
  22. package/dist/docs/versioned/next/explanation/positioning.md +3 -3
  23. package/dist/docs/versioned/next/explanation/repository-settings.md +6 -5
  24. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +44 -34
  25. package/dist/docs/versioned/next/explanation/source-rules.md +1 -1
  26. package/dist/docs/versioned/next/explanation/static-analysis.md +76 -16
  27. package/dist/docs/versioned/next/how-to/README.md +10 -0
  28. package/dist/docs/versioned/next/how-to/add-gpt-chat-mascot.md +92 -0
  29. package/dist/docs/versioned/next/how-to/add-organizations.md +204 -0
  30. package/dist/docs/versioned/next/how-to/admin-add-model.md +9 -1
  31. package/dist/docs/versioned/next/how-to/admin-import-prebuilt.md +7 -1
  32. package/dist/docs/versioned/next/how-to/audit-log.md +85 -0
  33. package/dist/docs/versioned/next/how-to/background-jobs.md +296 -0
  34. package/dist/docs/versioned/next/how-to/build-admin-screens.md +42 -13
  35. package/dist/docs/versioned/next/how-to/charts-and-dashboards.md +37 -0
  36. package/dist/docs/versioned/next/how-to/circleci.md +136 -27
  37. package/dist/docs/versioned/next/how-to/compare-chart-rendered-snapshots.md +36 -0
  38. package/dist/docs/versioned/next/how-to/configure-better-auth.md +4 -0
  39. package/dist/docs/versioned/next/how-to/create-a-terreno-app.md +154 -0
  40. package/dist/docs/versioned/next/how-to/deploy-backend-to-cloud-run.md +2 -0
  41. package/dist/docs/versioned/next/how-to/github-actions-ci.md +4 -3
  42. package/dist/docs/versioned/next/how-to/github-issue-lifecycle.md +12 -8
  43. package/dist/docs/versioned/next/how-to/in-app-notifications.md +147 -0
  44. package/dist/docs/versioned/next/how-to/install-agent-skills.md +24 -12
  45. package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +19 -3
  46. package/dist/docs/versioned/next/how-to/product-announcements.md +155 -0
  47. package/dist/docs/versioned/next/how-to/run-mongodb-migrations.md +127 -0
  48. package/dist/docs/versioned/next/how-to/run-tests-locally.md +102 -0
  49. package/dist/docs/versioned/next/how-to/seed-a-database.md +7 -2
  50. package/dist/docs/versioned/next/implementationPlans/README.md +8 -5
  51. package/dist/docs/versioned/next/implementationPlans/admin-scripts-durable-jobs.md +44 -0
  52. package/dist/docs/versioned/next/implementationPlans/agent-ui-blocks.md +386 -0
  53. package/dist/docs/versioned/next/implementationPlans/agentic-sdlc-plugin.md +11 -8
  54. package/dist/docs/versioned/next/implementationPlans/announcements-surfaces.md +231 -0
  55. package/dist/docs/versioned/next/implementationPlans/announcements.md +289 -0
  56. package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +3 -1
  57. package/dist/docs/versioned/next/implementationPlans/charts-and-dashboards.md +246 -0
  58. package/dist/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +4 -3
  59. package/dist/docs/versioned/next/implementationPlans/create-terreno-app.md +177 -0
  60. package/dist/docs/versioned/next/implementationPlans/datatable-server-side-filtering.md +219 -0
  61. package/dist/docs/versioned/next/implementationPlans/examples-demo-coverage.md +17 -17
  62. package/dist/docs/versioned/next/implementationPlans/framework-audit-log.md +247 -0
  63. package/dist/docs/versioned/next/implementationPlans/gpt-chat-mascot.md +40 -0
  64. package/dist/docs/versioned/next/implementationPlans/job-queues.md +330 -0
  65. package/dist/docs/versioned/next/implementationPlans/knip-cleanup.md +136 -0
  66. package/dist/docs/versioned/next/implementationPlans/migrate-admin-to-syncdb.md +210 -0
  67. package/dist/docs/versioned/next/implementationPlans/mongodb-migrations-tooling.md +254 -0
  68. package/dist/docs/versioned/next/implementationPlans/notification-center.md +185 -0
  69. package/dist/docs/versioned/next/implementationPlans/org-management-ui.md +8 -3
  70. package/dist/docs/versioned/next/implementationPlans/positioning-django-rails-universal.md +1 -1
  71. package/dist/docs/versioned/next/reference/README.md +4 -0
  72. package/dist/docs/versioned/next/reference/admin-backend.md +83 -5
  73. package/dist/docs/versioned/next/reference/admin-config.md +7 -0
  74. package/dist/docs/versioned/next/reference/admin-frontend.md +178 -5
  75. package/dist/docs/versioned/next/reference/admin-spa.md +18 -1
  76. package/dist/docs/versioned/next/reference/ai.md +2 -0
  77. package/dist/docs/versioned/next/reference/announcements.md +179 -0
  78. package/dist/docs/versioned/next/reference/api.md +343 -4
  79. package/dist/docs/versioned/next/reference/comms.md +33 -0
  80. package/dist/docs/versioned/next/reference/create-terreno-app.md +44 -0
  81. package/dist/docs/versioned/next/reference/environment-variables.md +29 -0
  82. package/dist/docs/versioned/next/reference/jobs.md +319 -0
  83. package/dist/docs/versioned/next/reference/legacy/rtk.md +5 -1
  84. package/dist/docs/versioned/next/reference/lifecycle-plugin.md +29 -11
  85. package/dist/docs/versioned/next/reference/mcp-server.md +2 -43
  86. package/dist/docs/versioned/next/reference/scan-plugin.md +169 -0
  87. package/dist/docs/versioned/next/reference/syncdb.md +38 -12
  88. package/dist/docs/versioned/next/reference/test.md +57 -6
  89. package/dist/docs/versioned/next/reference/ui-performance.md +2 -1
  90. package/dist/docs/versioned/next/reference/ui.md +257 -3
  91. package/dist/docs/versioned/next/tasks/admin-scripts-durable-jobs.md +10 -0
  92. package/dist/docs/versioned/next/tasks/agent-ui-blocks.md +114 -0
  93. package/dist/docs/versioned/next/tasks/announcements-surfaces.md +101 -0
  94. package/dist/docs/versioned/next/tasks/announcements.md +173 -0
  95. package/dist/docs/versioned/next/tasks/charts-and-dashboards.md +120 -0
  96. package/dist/docs/versioned/next/tasks/create-terreno-app.md +82 -0
  97. package/dist/docs/versioned/next/tasks/datatable-server-side-filtering.md +87 -0
  98. package/dist/docs/versioned/next/tasks/examples-demo-coverage.md +123 -17
  99. package/dist/docs/versioned/next/tasks/framework-audit-log.md +86 -0
  100. package/dist/docs/versioned/next/tasks/gpt-chat-mascot.md +13 -0
  101. package/dist/docs/versioned/next/tasks/job-queues.md +144 -0
  102. package/dist/docs/versioned/next/tasks/knip-cleanup.md +146 -0
  103. package/dist/docs/versioned/next/tasks/migrate-admin-to-syncdb.md +209 -0
  104. package/dist/docs/versioned/next/tasks/mongodb-migrations-tooling.md +107 -0
  105. package/dist/docs/versioned/next/tasks/notification-center.md +102 -0
  106. package/dist/docs/versioned/next/tasks/org-management-ui.md +14 -14
  107. package/dist/docs/versioned/next/tutorials/README.md +1 -1
  108. package/dist/docs/versioned/next/tutorials/getting-started.md +62 -22
  109. package/dist/help/excerpt.d.ts +2 -0
  110. package/dist/help/excerpt.d.ts.map +1 -0
  111. package/dist/help/excerpt.js +8 -0
  112. package/dist/help/excerpt.js.map +1 -0
  113. package/dist/help/updateNotes.d.ts +42 -0
  114. package/dist/help/updateNotes.d.ts.map +1 -0
  115. package/dist/help/updateNotes.js +235 -0
  116. package/dist/help/updateNotes.js.map +1 -0
  117. package/dist/index.js +1 -1
  118. package/dist/index.js.map +1 -1
  119. package/dist/local/tools/databaseQuery.d.ts +1 -0
  120. package/dist/local/tools/databaseQuery.d.ts.map +1 -1
  121. package/dist/local/tools/databaseQuery.js +1 -0
  122. package/dist/local/tools/databaseQuery.js.map +1 -1
  123. package/dist/packageGuidelines.d.ts +2 -2
  124. package/dist/packageGuidelines.d.ts.map +1 -1
  125. package/dist/packageGuidelines.js +2 -8
  126. package/dist/packageGuidelines.js.map +1 -1
  127. package/dist/resources.d.ts +3 -0
  128. package/dist/resources.d.ts.map +1 -1
  129. package/dist/resources.js +3 -0
  130. package/dist/resources.js.map +1 -1
  131. package/dist/search/chunker.d.ts +1 -0
  132. package/dist/search/chunker.d.ts.map +1 -1
  133. package/dist/search/chunker.js +1 -0
  134. package/dist/search/chunker.js.map +1 -1
  135. package/dist/search/docIndex.d.ts +1 -5
  136. package/dist/search/docIndex.d.ts.map +1 -1
  137. package/dist/search/docIndex.js +1 -0
  138. package/dist/search/docIndex.js.map +1 -1
  139. package/dist/search/docVersion.d.ts +2 -1
  140. package/dist/search/docVersion.d.ts.map +1 -1
  141. package/dist/search/docVersion.js +3 -1
  142. package/dist/search/docVersion.js.map +1 -1
  143. package/dist/tools.d.ts +2 -2
  144. package/dist/tools.d.ts.map +1 -1
  145. package/dist/tools.js +113 -2
  146. package/dist/tools.js.map +1 -1
  147. package/package.json +28 -27
  148. package/src/docs/guidelines/admin-frontend/guidelines/core.md +5 -1
  149. package/src/docs/guidelines/api/skills/mongoose-schema-safety/SKILL.md +23 -10
  150. package/src/docs/resources/syncdb.md +4 -1
  151. package/src/docs/upgrades/57.4.0.md +140 -0
  152. package/src/docs/versioned/next/README.md +3 -0
  153. package/src/docs/versioned/next/explanation/README.md +4 -1
  154. package/src/docs/versioned/next/explanation/admin-interface.md +20 -13
  155. package/src/docs/versioned/next/explanation/charts.md +29 -0
  156. package/src/docs/versioned/next/explanation/deployment-architecture-gcp.md +32 -0
  157. package/src/docs/versioned/next/explanation/deployment-baseline.md +1 -1
  158. package/src/docs/versioned/next/explanation/example-coverage.md +43 -0
  159. package/src/docs/versioned/next/explanation/local-first-data.md +5 -2
  160. package/src/docs/versioned/next/explanation/loop-engineering.md +9 -6
  161. package/src/docs/versioned/next/explanation/model-router-actions.md +19 -2
  162. package/src/docs/versioned/next/explanation/organizations.md +88 -0
  163. package/src/docs/versioned/next/explanation/planning-plugins.md +114 -0
  164. package/src/docs/versioned/next/explanation/positioning.md +3 -3
  165. package/src/docs/versioned/next/explanation/repository-settings.md +6 -5
  166. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +44 -34
  167. package/src/docs/versioned/next/explanation/source-rules.md +1 -1
  168. package/src/docs/versioned/next/explanation/static-analysis.md +76 -16
  169. package/src/docs/versioned/next/how-to/README.md +10 -0
  170. package/src/docs/versioned/next/how-to/add-gpt-chat-mascot.md +92 -0
  171. package/src/docs/versioned/next/how-to/add-organizations.md +204 -0
  172. package/src/docs/versioned/next/how-to/admin-add-model.md +9 -1
  173. package/src/docs/versioned/next/how-to/admin-import-prebuilt.md +7 -1
  174. package/src/docs/versioned/next/how-to/audit-log.md +85 -0
  175. package/src/docs/versioned/next/how-to/background-jobs.md +296 -0
  176. package/src/docs/versioned/next/how-to/build-admin-screens.md +42 -13
  177. package/src/docs/versioned/next/how-to/charts-and-dashboards.md +37 -0
  178. package/src/docs/versioned/next/how-to/circleci.md +136 -27
  179. package/src/docs/versioned/next/how-to/compare-chart-rendered-snapshots.md +36 -0
  180. package/src/docs/versioned/next/how-to/configure-better-auth.md +4 -0
  181. package/src/docs/versioned/next/how-to/create-a-terreno-app.md +154 -0
  182. package/src/docs/versioned/next/how-to/deploy-backend-to-cloud-run.md +2 -0
  183. package/src/docs/versioned/next/how-to/github-actions-ci.md +4 -3
  184. package/src/docs/versioned/next/how-to/github-issue-lifecycle.md +12 -8
  185. package/src/docs/versioned/next/how-to/in-app-notifications.md +147 -0
  186. package/src/docs/versioned/next/how-to/install-agent-skills.md +24 -12
  187. package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +19 -3
  188. package/src/docs/versioned/next/how-to/product-announcements.md +155 -0
  189. package/src/docs/versioned/next/how-to/run-mongodb-migrations.md +127 -0
  190. package/src/docs/versioned/next/how-to/run-tests-locally.md +102 -0
  191. package/src/docs/versioned/next/how-to/seed-a-database.md +7 -2
  192. package/src/docs/versioned/next/implementationPlans/README.md +8 -5
  193. package/src/docs/versioned/next/implementationPlans/admin-scripts-durable-jobs.md +44 -0
  194. package/src/docs/versioned/next/implementationPlans/agent-ui-blocks.md +386 -0
  195. package/src/docs/versioned/next/implementationPlans/agentic-sdlc-plugin.md +11 -8
  196. package/src/docs/versioned/next/implementationPlans/announcements-surfaces.md +231 -0
  197. package/src/docs/versioned/next/implementationPlans/announcements.md +289 -0
  198. package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +3 -1
  199. package/src/docs/versioned/next/implementationPlans/charts-and-dashboards.md +246 -0
  200. package/src/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +4 -3
  201. package/src/docs/versioned/next/implementationPlans/create-terreno-app.md +177 -0
  202. package/src/docs/versioned/next/implementationPlans/datatable-server-side-filtering.md +219 -0
  203. package/src/docs/versioned/next/implementationPlans/examples-demo-coverage.md +17 -17
  204. package/src/docs/versioned/next/implementationPlans/framework-audit-log.md +247 -0
  205. package/src/docs/versioned/next/implementationPlans/gpt-chat-mascot.md +40 -0
  206. package/src/docs/versioned/next/implementationPlans/job-queues.md +330 -0
  207. package/src/docs/versioned/next/implementationPlans/knip-cleanup.md +136 -0
  208. package/src/docs/versioned/next/implementationPlans/migrate-admin-to-syncdb.md +210 -0
  209. package/src/docs/versioned/next/implementationPlans/mongodb-migrations-tooling.md +254 -0
  210. package/src/docs/versioned/next/implementationPlans/notification-center.md +185 -0
  211. package/src/docs/versioned/next/implementationPlans/org-management-ui.md +8 -3
  212. package/src/docs/versioned/next/implementationPlans/positioning-django-rails-universal.md +1 -1
  213. package/src/docs/versioned/next/reference/README.md +4 -0
  214. package/src/docs/versioned/next/reference/admin-backend.md +83 -5
  215. package/src/docs/versioned/next/reference/admin-config.md +7 -0
  216. package/src/docs/versioned/next/reference/admin-frontend.md +178 -5
  217. package/src/docs/versioned/next/reference/admin-spa.md +18 -1
  218. package/src/docs/versioned/next/reference/ai.md +2 -0
  219. package/src/docs/versioned/next/reference/announcements.md +179 -0
  220. package/src/docs/versioned/next/reference/api.md +343 -4
  221. package/src/docs/versioned/next/reference/comms.md +33 -0
  222. package/src/docs/versioned/next/reference/create-terreno-app.md +44 -0
  223. package/src/docs/versioned/next/reference/environment-variables.md +29 -0
  224. package/src/docs/versioned/next/reference/jobs.md +319 -0
  225. package/src/docs/versioned/next/reference/legacy/rtk.md +5 -1
  226. package/src/docs/versioned/next/reference/lifecycle-plugin.md +29 -11
  227. package/src/docs/versioned/next/reference/mcp-server.md +2 -43
  228. package/src/docs/versioned/next/reference/scan-plugin.md +169 -0
  229. package/src/docs/versioned/next/reference/syncdb.md +38 -12
  230. package/src/docs/versioned/next/reference/test.md +57 -6
  231. package/src/docs/versioned/next/reference/ui-performance.md +2 -1
  232. package/src/docs/versioned/next/reference/ui.md +257 -3
  233. package/src/docs/versioned/next/tasks/admin-scripts-durable-jobs.md +10 -0
  234. package/src/docs/versioned/next/tasks/agent-ui-blocks.md +114 -0
  235. package/src/docs/versioned/next/tasks/announcements-surfaces.md +101 -0
  236. package/src/docs/versioned/next/tasks/announcements.md +173 -0
  237. package/src/docs/versioned/next/tasks/charts-and-dashboards.md +120 -0
  238. package/src/docs/versioned/next/tasks/create-terreno-app.md +82 -0
  239. package/src/docs/versioned/next/tasks/datatable-server-side-filtering.md +87 -0
  240. package/src/docs/versioned/next/tasks/examples-demo-coverage.md +123 -17
  241. package/src/docs/versioned/next/tasks/framework-audit-log.md +86 -0
  242. package/src/docs/versioned/next/tasks/gpt-chat-mascot.md +13 -0
  243. package/src/docs/versioned/next/tasks/job-queues.md +144 -0
  244. package/src/docs/versioned/next/tasks/knip-cleanup.md +146 -0
  245. package/src/docs/versioned/next/tasks/migrate-admin-to-syncdb.md +209 -0
  246. package/src/docs/versioned/next/tasks/mongodb-migrations-tooling.md +107 -0
  247. package/src/docs/versioned/next/tasks/notification-center.md +102 -0
  248. package/src/docs/versioned/next/tasks/org-management-ui.md +14 -14
  249. package/src/docs/versioned/next/tutorials/README.md +1 -1
  250. 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`. Inherit native models, slugs, soft-delete,
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; `OrgsApp` opt-in. Adopting apps backfill `organizationId` with a documented
304
- recipe (formal migrator is `mongo-migrations`).
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 | Not shipped | On the roadmap |
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: AdminModelConfig[];
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
- Generates RTK Query hooks for CRUD operations.
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
- useGetQuery,
311
+ useReadQuery,
143
312
  useCreateMutation,
144
313
  useUpdateMutation,
145
314
  useDeleteMutation,
146
- } = useAdminApi(api, baseUrl, modelName);
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