@terreno/mcp 57.0.0-beta.1 → 57.1.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 (222) hide show
  1. package/dist/bootstrap.d.ts +1 -1
  2. package/dist/bootstrap.d.ts.map +1 -1
  3. package/dist/bootstrap.js +381 -191
  4. package/dist/bootstrap.js.map +1 -1
  5. package/dist/docs/guidelines/api/skills/mongoose-schema-safety/SKILL.md +8 -0
  6. package/dist/docs/resources/overview.md +6 -1
  7. package/dist/docs/resources/rtk.md +3 -1
  8. package/dist/docs/resources/syncdb.md +43 -0
  9. package/dist/docs/upgrades/56.0.0-beta.2.md +34 -0
  10. package/dist/docs/upgrades/57.0.0.md +49 -0
  11. package/dist/docs/upgrades/57.1.0.md +126 -0
  12. package/dist/docs/versioned/next/README.md +15 -3
  13. package/dist/docs/versioned/next/explanation/README.md +3 -0
  14. package/dist/docs/versioned/next/explanation/admin-consent-migration.md +15 -0
  15. package/dist/docs/versioned/next/explanation/admin-plugin-frontend.md +12 -0
  16. package/dist/docs/versioned/next/explanation/announcements/rtk-deprecation.md +29 -0
  17. package/dist/docs/versioned/next/explanation/authentication.md +51 -58
  18. package/dist/docs/versioned/next/explanation/local-first-data.md +52 -0
  19. package/dist/docs/versioned/next/explanation/loop-engineering.md +117 -0
  20. package/dist/docs/versioned/next/explanation/repository-settings.md +8 -0
  21. package/dist/docs/versioned/next/explanation/roadmap-process.md +22 -9
  22. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +39 -0
  23. package/dist/docs/versioned/next/how-to/README.md +3 -0
  24. package/dist/docs/versioned/next/how-to/admin-add-model.md +41 -0
  25. package/dist/docs/versioned/next/how-to/admin-custom-home.md +31 -0
  26. package/dist/docs/versioned/next/how-to/admin-custom-widget.md +42 -0
  27. package/dist/docs/versioned/next/how-to/admin-import-prebuilt.md +25 -0
  28. package/dist/docs/versioned/next/how-to/circleci.md +173 -0
  29. package/dist/docs/versioned/next/how-to/configure-better-auth.md +13 -10
  30. package/dist/docs/versioned/next/how-to/expose-mcp-tools.md +194 -0
  31. package/dist/docs/versioned/next/how-to/install-agent-skills.md +46 -0
  32. package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +26 -1
  33. package/dist/docs/versioned/next/how-to/websocket-integration.md +2 -1
  34. package/dist/docs/versioned/next/implementationPlans/ModularAPI.md +1 -0
  35. package/dist/docs/versioned/next/implementationPlans/README.md +31 -17
  36. package/dist/docs/versioned/next/implementationPlans/admin-improvements-research.md +136 -0
  37. package/dist/docs/versioned/next/implementationPlans/admin-improvements.md +157 -527
  38. package/dist/docs/versioned/next/implementationPlans/admin-only-research.md +4 -0
  39. package/dist/docs/versioned/next/implementationPlans/admin-only.md +3 -1
  40. package/dist/docs/versioned/next/implementationPlans/admin-script-runner.md +1 -0
  41. package/dist/docs/versioned/next/implementationPlans/admin-ui-v2-django-parity.md +1 -0
  42. package/dist/docs/versioned/next/implementationPlans/agentic-sdlc-plugin.md +160 -130
  43. package/dist/docs/versioned/next/implementationPlans/apierror-standard-error-redesign.md +1 -0
  44. package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +5 -4
  45. package/dist/docs/versioned/next/implementationPlans/comms-abstraction-research.md +48 -0
  46. package/dist/docs/versioned/next/implementationPlans/comms-abstraction.md +152 -62
  47. package/dist/docs/versioned/next/implementationPlans/comms-adapter-sendgrid.md +9 -9
  48. package/dist/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +1 -1
  49. package/dist/docs/versioned/next/implementationPlans/consent-forms.md +4 -0
  50. package/dist/docs/versioned/next/implementationPlans/design-blend-skill.md +1 -0
  51. package/dist/docs/versioned/next/implementationPlans/docs-site-and-versioning.md +1 -0
  52. package/dist/docs/versioned/next/implementationPlans/feature-flags-openfeature.md +1 -0
  53. package/dist/docs/versioned/next/implementationPlans/feature-flags.md +4 -0
  54. package/dist/docs/versioned/next/implementationPlans/infra-mcp-research.md +1 -0
  55. package/dist/docs/versioned/next/implementationPlans/infra-mcp.md +3 -3
  56. package/dist/docs/versioned/next/implementationPlans/mcp-boost-parity.md +1 -0
  57. package/dist/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci-research.md +1 -0
  58. package/dist/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci.md +2 -2
  59. package/dist/docs/versioned/next/implementationPlans/model-router-actions.md +4 -0
  60. package/dist/docs/versioned/next/implementationPlans/model-router-mcp.md +5 -4
  61. package/dist/docs/versioned/next/implementationPlans/offline-mode.md +3 -1
  62. package/dist/docs/versioned/next/implementationPlans/oss-launch-program.md +11 -2
  63. package/dist/docs/versioned/next/implementationPlans/pluggable-database-sqlite-research.md +150 -0
  64. package/dist/docs/versioned/next/implementationPlans/pluggable-database-sqlite.md +247 -0
  65. package/dist/docs/versioned/next/implementationPlans/positioning-django-rails-universal.md +1 -1
  66. package/dist/docs/versioned/next/implementationPlans/public-roadmap-github.md +2 -2
  67. package/dist/docs/versioned/next/implementationPlans/rbac-permissions.md +52 -15
  68. package/dist/docs/versioned/next/implementationPlans/rtk-to-syncdb-migration-docs.md +13 -12
  69. package/dist/docs/versioned/next/implementationPlans/syncdb-codegen-research.md +137 -0
  70. package/dist/docs/versioned/next/implementationPlans/syncdb-codegen.md +63 -59
  71. package/dist/docs/versioned/next/implementationPlans/syncdb-local-first.md +1 -0
  72. package/dist/docs/versioned/next/implementationPlans/syncdb-phase-c-design.md +1 -0
  73. package/dist/docs/versioned/next/implementationPlans/terreno-langfuse-integration.md +1 -0
  74. package/dist/docs/versioned/next/implementationPlans/terreno-syncdb-2.md +1 -0
  75. package/dist/docs/versioned/next/implementationPlans/upgrade-banner.md +4 -0
  76. package/dist/docs/versioned/next/reference/README.md +6 -1
  77. package/dist/docs/versioned/next/reference/admin-config.md +51 -0
  78. package/dist/docs/versioned/next/reference/ai.md +15 -1
  79. package/dist/docs/versioned/next/reference/api.md +18 -0
  80. package/dist/docs/versioned/next/reference/comms.md +88 -7
  81. package/dist/docs/versioned/next/reference/environment-variables.md +9 -1
  82. package/dist/docs/versioned/next/reference/{rtk.md → legacy/rtk.md} +11 -7
  83. package/dist/docs/versioned/next/reference/lifecycle-plugin.md +54 -0
  84. package/dist/docs/versioned/next/reference/mcp-server.md +7 -0
  85. package/dist/docs/versioned/next/reference/syncdb.md +30 -3
  86. package/dist/docs/versioned/next/tasks/README.md +21 -10
  87. package/dist/docs/versioned/next/tasks/admin-improvements.md +189 -353
  88. package/dist/docs/versioned/next/tasks/admin-only.md +2 -0
  89. package/dist/docs/versioned/next/tasks/agentic-sdlc-plugin.md +54 -160
  90. package/dist/docs/versioned/next/tasks/comms-abstraction.md +30 -24
  91. package/dist/docs/versioned/next/tasks/comms-adapter-sendgrid.md +4 -4
  92. package/dist/docs/versioned/next/tasks/consent-forms.md +2 -0
  93. package/dist/docs/versioned/next/tasks/feature-flags.md +2 -0
  94. package/dist/docs/versioned/next/tasks/migrate-cicd-to-circleci.md +14 -10
  95. package/dist/docs/versioned/next/tasks/model-router-actions.md +2 -0
  96. package/dist/docs/versioned/next/tasks/model-router-mcp.md +27 -27
  97. package/dist/docs/versioned/next/tasks/offline-mode.md +2 -0
  98. package/dist/docs/versioned/next/tasks/pluggable-database-sqlite.md +44 -0
  99. package/dist/docs/versioned/next/tasks/public-roadmap-github.md +1 -1
  100. package/dist/docs/versioned/next/tasks/rtk-to-syncdb-migration-docs.md +15 -15
  101. package/dist/docs/versioned/next/tasks/syncdb-codegen.md +37 -31
  102. package/dist/docs/versioned/next/tasks/upgrade-banner.md +2 -0
  103. package/dist/index.js +24 -55
  104. package/dist/index.js.map +1 -1
  105. package/dist/local/localServer.d.ts.map +1 -1
  106. package/dist/local/localServer.js +9 -8
  107. package/dist/local/localServer.js.map +1 -1
  108. package/dist/local/localTools.d.ts +1 -1
  109. package/dist/local/localTools.d.ts.map +1 -1
  110. package/dist/prompts.d.ts.map +1 -1
  111. package/dist/prompts.js +44 -45
  112. package/dist/prompts.js.map +1 -1
  113. package/dist/resources.d.ts.map +1 -1
  114. package/dist/resources.js +9 -2
  115. package/dist/resources.js.map +1 -1
  116. package/dist/tools.d.ts +1 -1
  117. package/dist/tools.d.ts.map +1 -1
  118. package/dist/tools.js +4 -2
  119. package/dist/tools.js.map +1 -1
  120. package/package.json +7 -3
  121. package/src/docs/guidelines/api/skills/mongoose-schema-safety/SKILL.md +8 -0
  122. package/src/docs/resources/overview.md +6 -1
  123. package/src/docs/resources/rtk.md +3 -1
  124. package/src/docs/resources/syncdb.md +43 -0
  125. package/src/docs/upgrades/56.0.0-beta.2.md +34 -0
  126. package/src/docs/upgrades/57.0.0.md +49 -0
  127. package/src/docs/upgrades/57.1.0.md +126 -0
  128. package/src/docs/versioned/next/README.md +15 -3
  129. package/src/docs/versioned/next/explanation/README.md +3 -0
  130. package/src/docs/versioned/next/explanation/admin-consent-migration.md +15 -0
  131. package/src/docs/versioned/next/explanation/admin-plugin-frontend.md +12 -0
  132. package/src/docs/versioned/next/explanation/announcements/rtk-deprecation.md +29 -0
  133. package/src/docs/versioned/next/explanation/authentication.md +51 -58
  134. package/src/docs/versioned/next/explanation/local-first-data.md +52 -0
  135. package/src/docs/versioned/next/explanation/loop-engineering.md +117 -0
  136. package/src/docs/versioned/next/explanation/repository-settings.md +8 -0
  137. package/src/docs/versioned/next/explanation/roadmap-process.md +22 -9
  138. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +39 -0
  139. package/src/docs/versioned/next/how-to/README.md +3 -0
  140. package/src/docs/versioned/next/how-to/admin-add-model.md +41 -0
  141. package/src/docs/versioned/next/how-to/admin-custom-home.md +31 -0
  142. package/src/docs/versioned/next/how-to/admin-custom-widget.md +42 -0
  143. package/src/docs/versioned/next/how-to/admin-import-prebuilt.md +25 -0
  144. package/src/docs/versioned/next/how-to/circleci.md +173 -0
  145. package/src/docs/versioned/next/how-to/configure-better-auth.md +13 -10
  146. package/src/docs/versioned/next/how-to/expose-mcp-tools.md +194 -0
  147. package/src/docs/versioned/next/how-to/install-agent-skills.md +46 -0
  148. package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +26 -1
  149. package/src/docs/versioned/next/how-to/websocket-integration.md +2 -1
  150. package/src/docs/versioned/next/implementationPlans/ModularAPI.md +1 -0
  151. package/src/docs/versioned/next/implementationPlans/README.md +31 -17
  152. package/src/docs/versioned/next/implementationPlans/admin-improvements-research.md +136 -0
  153. package/src/docs/versioned/next/implementationPlans/admin-improvements.md +157 -527
  154. package/src/docs/versioned/next/implementationPlans/admin-only-research.md +4 -0
  155. package/src/docs/versioned/next/implementationPlans/admin-only.md +3 -1
  156. package/src/docs/versioned/next/implementationPlans/admin-script-runner.md +1 -0
  157. package/src/docs/versioned/next/implementationPlans/admin-ui-v2-django-parity.md +1 -0
  158. package/src/docs/versioned/next/implementationPlans/agentic-sdlc-plugin.md +160 -130
  159. package/src/docs/versioned/next/implementationPlans/apierror-standard-error-redesign.md +1 -0
  160. package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +5 -4
  161. package/src/docs/versioned/next/implementationPlans/comms-abstraction-research.md +48 -0
  162. package/src/docs/versioned/next/implementationPlans/comms-abstraction.md +152 -62
  163. package/src/docs/versioned/next/implementationPlans/comms-adapter-sendgrid.md +9 -9
  164. package/src/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +1 -1
  165. package/src/docs/versioned/next/implementationPlans/consent-forms.md +4 -0
  166. package/src/docs/versioned/next/implementationPlans/design-blend-skill.md +1 -0
  167. package/src/docs/versioned/next/implementationPlans/docs-site-and-versioning.md +1 -0
  168. package/src/docs/versioned/next/implementationPlans/feature-flags-openfeature.md +1 -0
  169. package/src/docs/versioned/next/implementationPlans/feature-flags.md +4 -0
  170. package/src/docs/versioned/next/implementationPlans/infra-mcp-research.md +1 -0
  171. package/src/docs/versioned/next/implementationPlans/infra-mcp.md +3 -3
  172. package/src/docs/versioned/next/implementationPlans/mcp-boost-parity.md +1 -0
  173. package/src/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci-research.md +1 -0
  174. package/src/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci.md +2 -2
  175. package/src/docs/versioned/next/implementationPlans/model-router-actions.md +4 -0
  176. package/src/docs/versioned/next/implementationPlans/model-router-mcp.md +5 -4
  177. package/src/docs/versioned/next/implementationPlans/offline-mode.md +3 -1
  178. package/src/docs/versioned/next/implementationPlans/oss-launch-program.md +11 -2
  179. package/src/docs/versioned/next/implementationPlans/pluggable-database-sqlite-research.md +150 -0
  180. package/src/docs/versioned/next/implementationPlans/pluggable-database-sqlite.md +247 -0
  181. package/src/docs/versioned/next/implementationPlans/positioning-django-rails-universal.md +1 -1
  182. package/src/docs/versioned/next/implementationPlans/public-roadmap-github.md +2 -2
  183. package/src/docs/versioned/next/implementationPlans/rbac-permissions.md +52 -15
  184. package/src/docs/versioned/next/implementationPlans/rtk-to-syncdb-migration-docs.md +13 -12
  185. package/src/docs/versioned/next/implementationPlans/syncdb-codegen-research.md +137 -0
  186. package/src/docs/versioned/next/implementationPlans/syncdb-codegen.md +63 -59
  187. package/src/docs/versioned/next/implementationPlans/syncdb-local-first.md +1 -0
  188. package/src/docs/versioned/next/implementationPlans/syncdb-phase-c-design.md +1 -0
  189. package/src/docs/versioned/next/implementationPlans/terreno-langfuse-integration.md +1 -0
  190. package/src/docs/versioned/next/implementationPlans/terreno-syncdb-2.md +1 -0
  191. package/src/docs/versioned/next/implementationPlans/upgrade-banner.md +4 -0
  192. package/src/docs/versioned/next/reference/README.md +6 -1
  193. package/src/docs/versioned/next/reference/admin-config.md +51 -0
  194. package/src/docs/versioned/next/reference/ai.md +15 -1
  195. package/src/docs/versioned/next/reference/api.md +18 -0
  196. package/src/docs/versioned/next/reference/comms.md +88 -7
  197. package/src/docs/versioned/next/reference/environment-variables.md +9 -1
  198. package/src/docs/versioned/next/reference/{rtk.md → legacy/rtk.md} +11 -7
  199. package/src/docs/versioned/next/reference/lifecycle-plugin.md +54 -0
  200. package/src/docs/versioned/next/reference/mcp-server.md +7 -0
  201. package/src/docs/versioned/next/reference/syncdb.md +30 -3
  202. package/src/docs/versioned/next/tasks/README.md +21 -10
  203. package/src/docs/versioned/next/tasks/admin-improvements.md +189 -353
  204. package/src/docs/versioned/next/tasks/admin-only.md +2 -0
  205. package/src/docs/versioned/next/tasks/agentic-sdlc-plugin.md +54 -160
  206. package/src/docs/versioned/next/tasks/comms-abstraction.md +30 -24
  207. package/src/docs/versioned/next/tasks/comms-adapter-sendgrid.md +4 -4
  208. package/src/docs/versioned/next/tasks/consent-forms.md +2 -0
  209. package/src/docs/versioned/next/tasks/feature-flags.md +2 -0
  210. package/src/docs/versioned/next/tasks/migrate-cicd-to-circleci.md +14 -10
  211. package/src/docs/versioned/next/tasks/model-router-actions.md +2 -0
  212. package/src/docs/versioned/next/tasks/model-router-mcp.md +27 -27
  213. package/src/docs/versioned/next/tasks/offline-mode.md +2 -0
  214. package/src/docs/versioned/next/tasks/pluggable-database-sqlite.md +44 -0
  215. package/src/docs/versioned/next/tasks/public-roadmap-github.md +1 -1
  216. package/src/docs/versioned/next/tasks/rtk-to-syncdb-migration-docs.md +15 -15
  217. package/src/docs/versioned/next/tasks/syncdb-codegen.md +37 -31
  218. package/src/docs/versioned/next/tasks/upgrade-banner.md +2 -0
  219. package/dist/docs/versioned/next/implementationPlans/PLAN_INDEX.md +0 -73
  220. package/dist/docs/versioned/next/implementationPlans/syncdb-api-inventory.md +0 -303
  221. package/src/docs/versioned/next/implementationPlans/PLAN_INDEX.md +0 -73
  222. package/src/docs/versioned/next/implementationPlans/syncdb-api-inventory.md +0 -303
@@ -1,577 +1,207 @@
1
- # Implementation Plan: Admin Interface Overhaul (Django-Admin Parity)
2
-
1
+ # Implementation Plan: Admin Improvements (post–Admin UI v2)
2
+
3
+ **Status:** Approved
4
+ **Branch:** (implementation TBD)
5
+ **Owner:** Josh Gachnang
6
+ **Created:** 2026-06-01 (original); revised 2026-08-20 post–v2 gap analysis
7
+ **Roadmap issue:** https://github.com/FlourishHealth/terreno/issues/1099
8
+ **Prerequisite:** [Admin UI v2 — Django-parity admin](./admin-ui-v2-django-parity.md) (**Complete**)
9
+ **Research:** [admin-improvements-research.md](./admin-improvements-research.md)
3
10
  ## Goal
4
11
 
5
- Bring Terreno's admin from "rendered CRUD" to "Django-admin-class developer experience": admin config co-located on `modelRouter`, pre-built admins composable via the existing `TerrenoPlugin` contract, a richer table (search, filter sidebar, bulk actions, fieldsets, readonlyFields), a declarative customizable home page, a widget registry consumers can extend, and an API surface that's forward-compatible with realtime via Socket.io.
12
+ Finish the **architectural** admin overhaul that Admin UI v2 did not ship: co-locate admin config on `modelRouter`, aggregate admins from plugins via `adminContribution()`, introduce `AdminProvider` + a three-bucket widget registry + `AdminScreenRouter`, harden list filters with a strict parser, lift field scrubbing into `@terreno/api`, and migrate pre-built plugin admins — **on top of** the v2 shell, `home.slots`, `bulk-patch`, and script runner already in production.
6
13
 
7
- ## Non-goals (v1)
14
+ ## Non-Goals
8
15
 
9
- - Shipping a Socket.io server (the API surface is realtime-ready; the transport ships in a follow-up).
10
- - True related-model inlines (Django `TabularInline`/`StackedInline`). Subdoc inlines via `AdminNestedArrayField` continue to work.
11
- - `date_hierarchy`, `prepopulated_fields`, `list_editable`, `show_facets`. Roadmapped for v1.1+.
12
- - Replacing the existing AdminApp config (`AdminApp.models`) — kept with deprecation warning for one minor cycle.
16
+ - Replacing Admin UI v2 UX that already works (shell, slots layout, bulk-patch, background tasks, audit log, scripts).
17
+ - `POST /admin/{resource}/actions/:name` or imperative `AdminAction.run()` handlers — **bulk actions stay on v2** (`bulk-patch` + declarative `patchKeys`, `background: true` → `/admin/background-tasks`).
18
+ - Per-user permission evaluation on `/admin/config` — defer to [RBAC permissions](./rbac-permissions.md); v2 static `permissions.{create,update,delete}` flags remain.
19
+ - `home.widgets` typed array — canonical layout is v2 **`home.slots`** only.
20
+ - Socket.io server / realtime UI push (event bus API surface only).
21
+ - True related-model inlines, `date_hierarchy`, `prepopulated_fields`, `list_editable`, `show_facets`.
13
22
 
14
- ## Decisions from adversarial review
23
+ ## Decisions
15
24
 
16
- Verified findings folded back in. The five most consequential:
25
+ | Question | Decision |
26
+ |----------|----------|
27
+ | Delivery scope | **Full IP** (provider, screen router, filter drawer, filterParser, plugin migrations, docs, E2E) — not architecture-only |
28
+ | Bulk actions API | **v2 only** — `bulk-patch` + `background-tasks`; no `/actions/:name` |
29
+ | Home layout | **`home.slots`** (`navGlobal`, `contentTop`, `main`, `sidebar`); plugin widgets by string id in slots |
30
+ | `/admin/config` permissions | **Static flags** (v2) — not evaluated per requesting user |
31
+ | Filter hardening | **`filterParser`** module with injection tests; **keep v2 query param shape** where compatible |
32
+ | Plugin frontend widgets | **`admin-frontend/src/widgets/`** (option A) |
33
+ | Example migration | **Single atomic PR** — backend + frontend + admin-spa together |
34
+ | Roadmap | **New tracking issue** (not extending admin-ui-v2 issue) |
35
+ | Field names | **No renames** — `listFields`, `defaultSort`, etc. stay; additive options only |
36
+ | TerrenoPlugin signature | Extend `register(app, openApi?, terrenoApp?)` additively; `TerrenoApp.build()` passes `this` |
37
+ | Collision precedence | Registered `modelRouter` with `admin:` **>** plugin `adminContribution()` **>** legacy `AdminApp.models` |
38
+ | Widget registry shape | `{ fields, home, screens }` — locked before implementation |
17
39
 
18
- 1. **TerrenoPlugin signature is extended (not changed).** `register(app, openApi?, terrenoApp?)` — third argument optional, ignored by existing plugins. `TerrenoApp.build()` passes itself as the third arg. This is how `AdminApp` discovers registered routers and plugin contributions, and how `modelRouter` reaches the app to emit events. No new global registry.
19
- 2. **No renames of existing fields.** `listFields`, `defaultSort`, `fieldOverrides`, `fieldOrder`, `hiddenFields` stay as-is. Only *new* options are added (`searchFields`, `sortableFields`, `filters`, `fieldsets`, `readonlyFields`, `excludeFields`, `autocompleteFields`, `actions`, `adminPermissions`, `adminFilter`, `realtime`, `listDisplayLinks`, `pageSize`, `group`, `icon`). The "Alias + warning" migration applies only to the *shape* (passing `AdminModelConfig` to `AdminApp.models` vs. setting `admin:` on the modelRouter).
20
- 3. **Event payloads are scrubbed at emit time.** `admin:model.changed` events are emitted *after* `excludeFields`/`hiddenFields` filtering (not from raw `postCreate`/etc. hooks). The emit step runs `removeHiddenFields` against the merged exclude+hidden set so future socket listeners cannot leak secrets.
21
- 4. **`adminFilter` is async from day one.** Signature: `(req: AuthedRequest) => Promise<FilterQuery<T>> | FilterQuery<T>`. Matches the existing async `queryFilter` contract.
22
- 5. **Action runs use a frozen context, not the live `req`.** Background actions especially: at enqueue time we snapshot `{ userId, userSnapshot, ids, actionName, requestId, startedAt }` into an `AdminActionContext` and pass *that* to `action.run`. Mirrors the existing `ScriptContext` pattern (admin-backend/src/adminApp.ts:622-638).
40
+ ### Already shipped (Admin UI v2 — out of scope for re-implementation)
23
41
 
24
- Also folded (lower severity, see Risks/Files-to-Touch/Tasks for specifics):
42
+ Treat as baseline; extend, do not rewrite:
25
43
 
26
- - Widget registry locked to `{ fields, home, screens }` three-bucket shape *before* Phase 3 begins. Field-widgets and home-widgets and screen-widgets are semantically different and use the same lookup mechanism.
27
- - Collision precedence reversed: **registered modelRouter > plugin contribution > legacy `AdminApp.models`**. Most-specific source wins, "legacy" is purely additive. Duplicate `routePath` from two registered modelRouters → throw at build time.
28
- - Filter parser is per-type strict: each `AdminFilter.type` accepts exactly one value shape; anything else → 400. Test matrix in Task 2.6 covers array-injection, operator-injection (`$ne`, `$gt`, `$where`, `$regex`), prototype-pollution, and shape mismatches.
29
- - `readonlyFields` enforced on all body-accepting routes including modelRouter's array sub-routes (`POST /:id/:field`, `PATCH /:id/:field/:itemId`, `DELETE /:id/:field/:itemId`). Implementation point: wrap `preUpdate` AND `preCreate` once; enforcement runs unconditionally regardless of route.
30
- - `excludeFields` / `hiddenFields` recursion through `populatePaths`: each populated document is scrubbed with *its* model's admin config. If the referenced model has no admin config, populated docs are returned as-is (consumer's choice not to admin-register that model).
31
- - Bulk actions capped at `MAX_ACTION_IDS = 1000` per request. Bulk validation uses one `find({_id: {$in: ids}, ...adminFilter})` round-trip — no N+1.
32
- - Per-action permission booleans in `/admin/config`: class-level evaluation calls `checkPermissions(action, ..., user, undefined)` and treats any throw as `false`. `IsOwner` returns `false` (no object) — that's correct: admins can't generally "own" all rows pre-fetch.
33
- - Soft-deleted documents: out of scope for this IP. `AdminConfig.includeDeleted?: boolean` added to the type for v1.1; no implementation now (documented in non-goals).
34
- - `ConfigurationApp` and the existing `VersionConfig` singleton routes: stay where they are. Singleton models get a separate IP. `VersionConfig` continues to be served by `AdminApp` directly (until then) but no longer appears in the synthetic `customScreens` array — it's surfaced via a built-in `versionConfig` home widget.
35
- - CSRF + rate-limit: `/actions/:name` inherits whatever auth+rate-limit middleware the app has installed. Documented; no new middleware in this IP. Better Auth cookie sessions are a known risk (out-of-scope until rate-limit IP).
44
+ - `schemaVersion: 2`, extended model meta, `home.slots`, `customScreens`, `scripts`
45
+ - `POST .../bulk-patch`, `POST /admin/background-tasks`, script runner routes
46
+ - `AdminShell`, `AdminHome` (built-in widget ids), `AdminModelTable` (search, inline filters, bulk select, declarative actions), `AdminModelForm` (fieldsets, readonly PATCH strip)
47
+ - `AdminAuditLog` + `recentActivity` widget, `admin-spa` + example v2 wiring
36
48
 
37
- ## High-level architecture
49
+ ## Architecture
38
50
 
39
51
  ```
40
52
  @terreno/api
41
- modelRouter(...) { ...options, admin?: AdminConfig } ← new option
42
- TerrenoApp.getRegistrations() ← new accessor
43
- TerrenoPlugin.adminContribution?(): AdminContribution ← new optional method
44
- AdminEventBus (typed emit/on, no transport) ← new (lays realtime groundwork)
53
+ modelRouter(..., { admin?: AdminConfig })
54
+ TerrenoApp.getRegistrations() / getPlugins()
55
+ TerrenoPlugin.adminContribution?()
56
+ scrubAdminFields() — shared exclude/hidden + populated recursion
57
+ TerrenoApp.on/off/emit("admin:model.changed") — no transport
45
58
 
46
59
  @terreno/admin-backend
47
- AdminApp ← rewritten to aggregate:
48
- 1. admin configs on registered model routers
49
- 2. plugin adminContribution() results
50
- 3. legacy AdminApp({models:[]}) input (deprecated, accepted)
51
- Server-side enforcement:
52
- - readonlyFields stripped from PATCH bodies
53
- - excludeFields removed from forms AND responses
54
- - filters/search/ordering parsed from query params
55
- - actions executed (sync or background via BackgroundTask)
60
+ AdminApp.register() aggregates:
61
+ 1. modelRouter registrations with admin:
62
+ 2. plugin adminContribution()
63
+ 3. legacy AdminApp.models[] (deprecated, one release)
64
+ filterParser.ts — strict validation on declared filters
65
+ /admin/config — + widgetIds, capabilities (additive)
56
66
 
57
67
  @terreno/admin-frontend
58
- <AdminProvider api baseUrl widgets={...}> ← new top-level context
59
- <AdminScreenRouter> ← new data-driven dispatcher (replaces consumer's switch)
60
- <AdminHome> ← rewrite of AdminModelList; widget-driven
61
- AdminModelTable (extended) ← search box, filter drawer, bulk-select, actions
62
- AdminModelForm (extended) ← fieldsets, readonlyFields, autocomplete refs
63
- AdminFieldRenderer ← widget map from context
64
- widgets/ ← built-in widgets (ModelStats, RecentActivity, ScriptRunner)
68
+ AdminProvider { api, baseUrl, widgets: { fields, home, screens } }
69
+ AdminScreenRouter — custom screen vs model table
70
+ AdminHome — slots + registry (refactor existing)
71
+ AdminFilterDrawer — replaces inline filter rail
72
+ widgets/* — built-ins + plugin widgets (FeatureFlagOverrides, Documents, AI, consent field widgets)
65
73
  ```
66
74
 
67
- ## Configuration shapes (canonical)
68
-
69
- ### `AdminConfig` (on modelRouter)
70
-
71
- **Naming note:** existing fields (`listFields`, `defaultSort`, `fieldOverrides`, `fieldOrder`, `hiddenFields`) keep their names. The "alias + warning" migration applies to the *shape* (passing `AdminModelConfig` to `AdminApp.models` vs. setting `admin:` on the modelRouter), not to renaming individual fields.
72
-
73
- ```typescript
74
- export interface AdminConfig<T = unknown> {
75
- /** Human-readable name for the model in the admin (e.g., "Users"). */
76
- displayName: string;
77
-
78
- /** Optional sidebar group label. Models without a group go in "Other". */
79
- group?: string;
80
-
81
- /** Optional icon (FontAwesome 6 name) for navigation. */
82
- icon?: string;
83
-
84
- // ── List view ───────────────────────────────────────────────────
85
- listFields: string[]; // columns shown in the table (existing name kept)
86
- listDisplayLinks?: string[]; // NEW. columns that click-through to edit (default: [first])
87
- searchFields?: string[]; // NEW. strings → regex search; surfaces search box in UI
88
- sortableFields?: string[]; // NEW. allowlist; default = all in listFields
89
- defaultSort?: string | string[]; // existing name kept
90
- pageSize?: number; // NEW. default rows per page (default: 25)
91
-
92
- filters?: AdminFilter[]; // NEW. sidebar filters (see below)
93
-
94
- // ── Form view ───────────────────────────────────────────────────
95
- fieldsets?: AdminFieldset[]; // NEW. grouped form layout; mutually exclusive with fieldOrder
96
- fieldOrder?: string[]; // existing
97
- readonlyFields?: string[]; // NEW. display-only; enforced server-side
98
- excludeFields?: string[]; // NEW. stronger than hiddenFields — not in forms OR detail responses
99
- hiddenFields?: string[]; // existing (not in list/form; still in raw detail unless also excluded)
100
- autocompleteFields?: string[]; // NEW. ref fields → async search (uses /search)
101
- fieldOverrides?: Record<string, AdminFieldOverride>; // existing
102
-
103
- // ── Permissions & scoping ───────────────────────────────────────
104
- adminPermissions?: Partial<{ // NEW. per-action override (default: IsAdmin for all)
105
- list: Permission[];
106
- read: Permission[];
107
- create: Permission[];
108
- update: Permission[];
109
- delete: Permission[];
110
- }>;
111
- /** NEW. Row-level scoping for admins. Async to allow fetching scope info (e.g., user.organizationId → org members). */
112
- adminFilter?: (req: AuthedRequest) => Promise<mongoose.FilterQuery<T>> | mongoose.FilterQuery<T>;
113
-
114
- // ── Bulk actions ────────────────────────────────────────────────
115
- actions?: AdminAction<T>[]; // NEW
116
-
117
- // ── Realtime (API surface only in this IP) ──────────────────────
118
- realtime?: boolean; // NEW. when true, scrubbed emit fires after each mutation
119
-
120
- // ── Forward-compat placeholder (v1.1) ───────────────────────────
121
- includeDeleted?: boolean; // NEW (type only; behavior in v1.1)
122
- }
123
-
124
- export interface AdminFilter {
125
- field: string; // schema field name (must be in queryFields)
126
- type: "boolean" | "choice" | "date" | "dateRange" | "ref" | "text";
127
- label?: string; // display label (default: humanized field name)
128
- choices?: { value: string; label: string }[]; // for "choice"; auto-derived from schema enum if absent
129
- refModel?: string; // for "ref"; auto-derived from schema ref if absent
130
- }
131
-
132
- export interface AdminFieldset {
133
- title: string;
134
- fields: string[];
135
- collapsed?: boolean;
136
- description?: string;
137
- }
138
-
139
- export interface AdminFieldOverride {
140
- widget?: string; // widget key (resolved against the frontend widget registry)
141
- label?: string;
142
- helpText?: string;
143
- }
144
-
145
- export interface AdminAction<T = unknown> {
146
- name: string; // url-safe id
147
- label: string; // shown in dropdown
148
- description?: string;
149
- confirm?: string; // confirmation prompt text
150
- permissions?: Permission[]; // defaults to model's adminPermissions.update
151
- background?: boolean; // route through BackgroundTask + AdminScriptRunModal
152
- run: (ctx: AdminActionContext<T>) =>
153
- Promise<{ updatedIds?: string[]; message?: string; results?: string[] }>;
154
- }
155
-
156
- /**
157
- * Frozen, request-independent context handed to `action.run`. For sync actions, captured immediately
158
- * before invocation. For `background: true` actions, captured at enqueue time and persisted on the
159
- * BackgroundTask — `req` may be torn down by the time the background runner picks it up.
160
- *
161
- * Mirrors the `ScriptContext` pattern in admin-backend/src/adminApp.ts:622-638.
162
- */
163
- export interface AdminActionContext<T = unknown> {
164
- /** Snapshot of `req.user._id` at enqueue. */
165
- userId: string;
166
- /** Snapshot of `req.user` (lean, no methods). May be stale for long-running background actions. */
167
- user: Pick<UserDocument, "_id" | "email" | "admin" | "name">;
168
- /** Validated ids that exist AND pass the model's `adminFilter`. Capped at `MAX_ACTION_IDS`. */
169
- ids: string[];
170
- /** Echoed back from URL. */
171
- actionName: string;
172
- /** Stable id for log correlation. */
173
- requestId: string;
174
- /** ISO timestamp captured at enqueue. */
175
- startedAt: string;
176
- /** Model the action is acting on (avoids importing model registry from inside run). */
177
- model: Model<T>;
178
- /** Logger pre-tagged with {actionName, requestId, userId}. */
179
- logger: Logger;
180
- }
181
-
182
- /** Server-side cap on `ids[]` accepted in a single action invocation. Documented; enforced; configurable per-app. */
183
- export const MAX_ACTION_IDS = 1000;
184
- ```
75
+ ## Models
185
76
 
186
- ### `AdminContribution` (on plugins)
187
-
188
- ```typescript
189
- export interface AdminContribution {
190
- /** Model admins this plugin provides. Equivalent to registering a modelRouter with admin set. */
191
- models?: AdminModelContribution[];
192
-
193
- /** Standalone screens for things like DocumentStorageBrowser. */
194
- customScreens?: AdminCustomScreen[];
195
-
196
- /** Optional contributions to the home page. Host must list the id in home.widgets to surface it. */
197
- homeWidgets?: AdminHomeWidgetContribution[];
198
-
199
- /** Server-side admin scripts (one-off jobs). */
200
- scripts?: AdminScriptConfig[];
201
- }
202
-
203
- export interface AdminModelContribution<T = unknown> {
204
- model: Model<T>;
205
- routePath: string;
206
- admin: AdminConfig<T>;
207
- permissions?: RESTPermissions<T>; // public-API permissions if the plugin owns the router
208
- }
209
-
210
- export interface AdminCustomScreen {
211
- name: string; // url-safe id, used in URL (/admin/{name})
212
- displayName: string;
213
- icon?: string;
214
- group?: string;
215
- }
216
-
217
- export interface AdminHomeWidgetContribution {
218
- id: string; // referenced by host from home.widgets
219
- displayName: string;
220
- icon?: string;
221
- /** Optional list of model names whose changes the widget cares about (used by the future realtime layer). */
222
- watches?: string[];
223
- }
224
-
225
- export interface TerrenoPlugin {
226
- /**
227
- * Existing signature is preserved with one BACKWARD-COMPATIBLE extension: a third optional `terrenoApp`
228
- * argument carrying the parent `TerrenoApp`. Plugins that ignore it work unchanged.
229
- *
230
- * `TerrenoApp.build()` always passes `(app, oapi, this)` from terrenoApp.ts:321.
231
- */
232
- register(app: express.Application, openApi?: unknown, terrenoApp?: TerrenoApp): void;
233
-
234
- /** NEW (optional). Called by `AdminApp` during its own `register()` to collect contributions. */
235
- adminContribution?(): AdminContribution;
236
- }
237
- ```
77
+ No new framework models. Consumers keep domain models; optional `AdminAuditLog` remains example-only (v2).
238
78
 
239
- **Why this signature, not constructor injection or a `terreno.useAdmin(...)` method:** keeps plugin construction order (`new AdminApp({...})`) free of references to `TerrenoApp`, mirrors how `openApi` is already passed, and is a backward-compatible additive change. All existing plugins (`HealthApp`, `BetterAuthApp`, `ConfigurationApp`, `FeatureFlagsApp`, `LangfuseApp`, `DocumentStorageApp`, `ConsentApp`) compile unchanged.
240
-
241
- **modelRouter discovery side:** `ModelRouterRegistration._buildWithOpenApi(openApi)` (api/src/api.ts:449) becomes `_buildWithContext({openApi, terrenoApp})`. `TerrenoApp.build()` passes itself in at registration time. The built router closes over the TerrenoApp reference so its lifecycle hooks can call `terrenoApp.emit("admin:model.changed", ...)`. The existing function signature is renamed; this is internal API. No public consumer should be calling `_buildWithOpenApi` directly.
242
-
243
- ### `AdminApp` options (host-side)
244
-
245
- ```typescript
246
- new AdminApp({
247
- basePath?: "/admin"; // existing
248
- models?: AdminModelContribution[]; // existing; accepts the legacy shape too (with warning)
249
- scripts?: AdminScriptConfig[]; // existing
250
- home?: AdminHomeConfig; // NEW
251
- });
252
-
253
- export interface AdminHomeConfig {
254
- title?: string;
255
- widgets?: (AdminHomeWidgetSpec | string)[]; // strings resolve to plugin-contributed widgets by id
256
- layout?: "grid" | "list";
257
- }
258
-
259
- export type AdminHomeWidgetSpec =
260
- | { type: "modelStats"; models?: string[] } // counts for selected models
261
- | { type: "recentActivity"; limit?: number; models?: string[] }
262
- | { type: "scriptRunner" }
263
- | { type: "customScreensList" }
264
- | { type: "modelsGrid"; group?: string } // legacy auto-grid (default if no home given)
265
- | string // plugin-contributed widget id
266
- | React.ComponentType; // ad-hoc inline component
267
- ```
79
+ ## APIs
268
80
 
269
- ## `/admin/config` response (extended)
270
-
271
- ```typescript
272
- interface AdminConfigResponse {
273
- models: AdminModelMeta[]; // existing, with fields below added
274
- scripts: AdminScriptMeta[]; // existing
275
- customScreens: AdminCustomScreen[]; // existing, plugin contributions merged in
276
- home?: AdminHomeMeta; // NEW
277
- widgetIds: string[]; // NEW: plugin-contributed widget ids the frontend should look up
278
- capabilities: { // NEW: simple feature flags so older frontends fail soft
279
- actions: boolean;
280
- filters: boolean;
281
- fieldsets: boolean;
282
- realtime: boolean;
283
- };
284
- }
285
-
286
- interface AdminModelMeta {
287
- name: string;
288
- routePath: string;
289
- displayName: string;
290
- group?: string;
291
- icon?: string;
292
-
293
- listFields: string[]; // existing name kept; canonical
294
- listDisplayLinks: string[]; // defaults to [first of listFields]
295
- searchFields: string[]; // empty array if search not configured
296
- sortableFields: string[]; // defaults to listFields
297
- defaultSort: string; // existing name kept
298
- pageSize: number; // default 25
299
-
300
- filters: AdminFilterMeta[];
301
-
302
- fieldsets?: AdminFieldset[]; // if absent, frontend falls back to fieldOrder
303
- fieldOrder?: string[]; // existing
304
- readonlyFields: string[];
305
- excludeFields: string[];
306
- hiddenFields: string[]; // existing; emitted alongside excludeFields for clarity
307
- autocompleteFields: string[];
308
- fields: Record<string, AdminFieldMeta>; // existing
309
- fieldOverrides: Record<string, AdminFieldOverride>; // emitted so the frontend can resolve widgets
310
-
311
- actions: AdminActionMeta[]; // {name, label, confirm, background, allowed: boolean}
312
- permissions: { // booleans for the current user (computed server-side per request)
313
- list: boolean; read: boolean; create: boolean; update: boolean; delete: boolean;
314
- };
315
- realtime: boolean;
316
- }
317
- ```
81
+ ### Unchanged (v2 — keep)
318
82
 
319
- ### Widget registry shape (locked here so Phase 3/4/5 don't reinvent it)
320
-
321
- ```typescript
322
- export interface AdminWidgetRegistry {
323
- /** Field widgets resolve `fieldOverrides[fieldName].widget` to a form-field component. */
324
- fields: Record<string, FieldWidgetComponent>;
325
- /** Home widgets resolve `home.widgets` string ids (and plugin-contributed widget ids) to a dashboard component. */
326
- home: Record<string, HomeWidgetComponent>;
327
- /** Screen widgets resolve `customScreens[].name` to a full-page screen component. Used by AdminScreenRouter. */
328
- screens: Record<string, ScreenWidgetComponent>;
329
- }
330
- ```
83
+ | Endpoint | Purpose |
84
+ |----------|---------|
85
+ | `GET /admin/config` | Aggregated metadata + `home.slots` + scripts + screens |
86
+ | CRUD `/admin/{resource}/*` | Per-model admin routes |
87
+ | `POST /admin/{resource}/bulk-patch` | Sync bulk patch (`ids`, `patch`); ≤1000 ids |
88
+ | `POST /admin/background-tasks` | Background bulk / long jobs |
89
+ | `POST /admin/scripts/:name/run` | Curated maintenance scripts |
331
90
 
332
- Three separate buckets because (a) the lookup happens in three different places, (b) the component props differ (field widgets receive `value`/`onChange`; home widgets receive `config`; screen widgets receive route params), and (c) lets each bucket evolve its prop contract independently. Same string id can exist in two buckets without conflict.
333
-
334
- ### Realtime emit step (no leakage)
335
-
336
- ```typescript
337
- // inside modelRouter postCreate/postUpdate/postDelete, guarded by options.admin?.realtime === true:
338
- const safeDoc = scrubAdminFields(savedDoc, options.admin); // strips excludeFields + hiddenFields
339
- terrenoApp.emit("admin:model.changed", {
340
- type: "create" | "update" | "delete",
341
- modelName: model.modelName,
342
- routePath: registration.path,
343
- documentId: String(savedDoc._id),
344
- document: safeDoc, // already scrubbed
345
- user: { id: String(req.user._id) },
346
- at: new Date().toISOString(),
347
- });
348
- ```
91
+ ### New / extended
349
92
 
350
- `scrubAdminFields` shares the implementation of `removeHiddenFields` (admin-backend/src/adminApp.ts:132). For delete events, `document` is omitted entirely.
93
+ | Surface | Change |
94
+ |---------|--------|
95
+ | `modelRouter` options | Optional `admin: AdminConfig` (types in `@terreno/api`) |
96
+ | `TerrenoPlugin` | Optional `adminContribution(): AdminContribution` |
97
+ | List queries | `filterParser` validates declared `filters[]` before merge into Mongoose query (same wire shape as v2 `queryFields`) |
98
+ | `TerrenoApp` | `admin:model.changed` emit when `admin.realtime === true` (scrubbed payload) |
351
99
 
352
- The current-user permission booleans are computed **per-request** (`/admin/config` is gated by `IsAdmin`, but per-action permissions may further restrict). This lets the frontend hide "Create" buttons the user can't use.
100
+ ### Explicitly rejected
353
101
 
354
- ## URL conventions
102
+ - `POST /admin/{resource}/actions/:name` — use `bulk-patch` / `background-tasks` instead.
355
103
 
356
- Concrete example (Users model with `routePath: "/users"` and an `activate` action):
104
+ ## Notifications
357
105
 
358
- ```
359
- GET /admin/config
360
- GET /admin/users?page=2&pageSize=25&sort=-created&q=alice&filter[admin]=true&filter[role]=staff
361
- GET /admin/users/507f1f77bcf86cd799439011
362
- POST /admin/users
363
- PATCH /admin/users/507f1f77bcf86cd799439011
364
- DELETE /admin/users/507f1f77bcf86cd799439011
365
- GET /admin/users/search?q=ali&fields=email,name ← used by autocompleteFields ref pickers
366
- POST /admin/users/actions/activate ← body: {ids: string[]} (≤ MAX_ACTION_IDS=1000)
367
- POST /admin/scripts/syncConsents/run ← existing
368
- GET /admin/scripts/tasks/abc123 ← existing (also used for background actions)
369
- GET /admin/widgets/feature-flags-overrides/data ← OPTIONAL — plugin widgets may register a data endpoint
370
- ```
106
+ In-app **Toast** only (v2). No email/push for admin events.
371
107
 
372
- ### Filter param parsing (strict per-type)
373
-
374
- Express+`qs` parses `filter[k]=v`, `filter[k][gte]=v`, and `filter[k][]=v` into different shapes. The filter parser declares one accepted shape per `AdminFilter.type`; everything else returns HTTP 400 with `{title: "Invalid filter", fields: {<filterName>: "..."}}`:
375
-
376
- | `type` | Accepted shape | Rejects |
377
- |--------|----------------|---------|
378
- | `boolean` | scalar `"true"` \| `"false"` | arrays, nested objects, anything else |
379
- | `text` | scalar string | arrays, objects |
380
- | `choice` | scalar string in declared choices | arrays, objects, unknown values |
381
- | `ref` | scalar string that is a valid ObjectId | arrays, objects, non-ObjectId strings |
382
- | `date` | scalar ISO date string | arrays, objects |
383
- | `dateRange` | `{ gte?: ISO, lte?: ISO }` only — both keys optional but no others | arrays, scalars, extra keys |
384
-
385
- Mongo operator keys (`$ne`, `$gt`, `$where`, `$regex`, `$in`, …) are NEVER passed through. Prototype-pollution attempts (`__proto__`, `constructor.prototype`, …) are dropped silently and logged at warn level. Only fields declared in `admin.filters[]` are honored at all.
386
-
387
- ## Server-side enforcement (security-critical)
388
-
389
- | Concern | Enforcement |
390
- |---------|-------------|
391
- | `readonlyFields` | Stripped from POST + PATCH bodies AND from array sub-routes (`POST /:id/:field`, `PATCH /:id/:field/:itemId`, `DELETE /:id/:field/:itemId`). Single enforcement point: a body-scrubbing middleware installed on every body-accepting admin route, not just `preUpdate`. |
392
- | `excludeFields` / `hiddenFields` | Stripped from POST + PATCH bodies and from list + detail responses. **Recursively applied through `populatePaths`**: when a populated doc is encountered, scrub it against *its* model's admin config (if any). Models with no admin config are returned as-is — consumer's responsibility not to populate sensitive refs without admin-registering them. |
393
- | `adminPermissions` | Per-route guard. Class-level evaluation (no object): `checkPermissions(action, perms, user, undefined)`; any throw → `false`. `IsOwner` returns `false` at the class level (no object to own). Per-action endpoints additionally run `action.permissions`. |
394
- | `adminFilter` | Async signature. Applied as a `$and` to the modelRouter's existing `queryFilter` for all list/read/update/delete on the admin namespace. |
395
- | `filters` | Per-type strict shape (see "Filter param parsing"). Only fields in `admin.filters[]` honored. Mongo operator keys never pass through. Prototype-pollution attempts dropped. |
396
- | `actions[].run` | Receives a frozen `AdminActionContext` (snapshot of user+ids+metadata at enqueue) — never the live `req`. Ids are validated via a single `find({_id: {$in: ids}, ...adminFilter})` round-trip; ids that don't pass `adminFilter` are silently excluded from the action's `ctx.ids`. |
397
- | `MAX_ACTION_IDS` | `ids[]` capped at 1000 per request. Above → 413 Payload Too Large. Configurable per-app via `AdminApp.maxActionIds`. |
398
- | `sortableFields` | Only allowlisted sort fields accepted. Unknown → 400. |
399
- | Search | Regex over `searchFields` only; query string escaped (existing pattern). ObjectId fast-path for valid IDs. List endpoint's `?q=` and the `/search` endpoint share a single helper function — no two implementations of the same semantics. |
400
- | Realtime emit | Emit happens *after* `removeHiddenFields`/`removeExcludeFields` scrub. Listeners (current AND future socket transport) never see hidden/excluded data. Delete events omit `document` entirely. |
401
-
402
- ## Realtime API surface (no transport in this IP)
403
-
404
- Introduce a typed event bus on `TerrenoApp`:
405
-
406
- ```typescript
407
- type AdminChangeEvent = {
408
- type: "create" | "update" | "delete";
409
- modelName: string;
410
- routePath: string;
411
- documentId: string;
412
- document?: unknown; // omitted for delete
413
- user: { id: string }; // who made the change
414
- at: string; // ISO timestamp
415
- };
416
-
417
- class TerrenoApp {
418
- on(event: "admin:model.changed", listener: (e: AdminChangeEvent) => void): void;
419
- off(event: "admin:model.changed", listener: (e: AdminChangeEvent) => void): void;
420
- // (internal) emit("admin:model.changed", event)
421
- }
422
- ```
108
+ ## UI
423
109
 
424
- `modelRouter` `postCreate`/`postUpdate`/`postDelete` hooks emit when `admin.realtime === true`. AdminApp registers a no-op listener that logs in debug mode. **No socket server is started.** The follow-up IP wires this to Socket.io. Frontend hooks accept an optional `socket` in `AdminProvider` and behave normally when absent.
425
-
426
- ## Files to Create / Modify
427
-
428
- ### `@terreno/api`
429
-
430
- | File | Change |
431
- |------|--------|
432
- | `api/src/api.ts` | Add `admin?: AdminConfig` to `ModelRouterOptions`. Wrap lifecycle hooks to emit `admin:model.changed` when `admin.realtime` is true. |
433
- | `api/src/terrenoApp.ts` | Add `getRegistrations()`, `getPlugins()`, `on/off/emit` typed event bus. |
434
- | `api/src/terrenoPlugin.ts` | Add `adminContribution?(): AdminContribution` to the interface. |
435
- | `api/src/adminTypes.ts` (NEW) | Export `AdminConfig`, `AdminFilter`, `AdminFieldset`, `AdminAction`, `AdminContribution`, `AdminChangeEvent`. (Live in `api` so plugins can import without depending on `admin-backend`.) |
436
- | `api/src/index.ts` | Re-export the new types. |
437
- | `api/src/api.test.ts` | Tests for emit-on-mutation gate, readonlyFields enforcement when admin config present. |
438
-
439
- ### `@terreno/admin-backend`
440
-
441
- | File | Change |
442
- |------|--------|
443
- | `admin-backend/src/adminApp.ts` | Rewrite to aggregate from registered model routers, plugin contributions, and legacy `models[]`. Extend `/admin/config` response. Add filter/sort/search parsing. Add `/actions/:name` endpoint. Compute per-action permission booleans. |
444
- | `admin-backend/src/legacy.ts` (NEW) | Adapt the legacy `AdminModelConfig` shape (`listFields`, etc.) to `AdminModelContribution`. Emit one-time deprecation warning per legacy entry. |
445
- | `admin-backend/src/filterParser.ts` (NEW) | Convert `filter[k]=v` query params into a safe Mongoose filter using the model's declared `AdminFilter[]`. |
446
- | `admin-backend/src/actionRunner.ts` (NEW) | Validate selected ids against `adminFilter`, run sync or background, return result. |
447
- | `admin-backend/src/index.ts` | Export `AdminApp` + types. |
448
- | `admin-backend/src/adminApp.test.ts` | Extend with tests for filter parsing, action invocation, plugin contribution aggregation, legacy shape acceptance. |
449
-
450
- ### `@terreno/admin-frontend`
451
-
452
- | File | Change |
453
- |------|--------|
454
- | `admin-frontend/src/AdminProvider.tsx` (NEW) | React context: `{ api, baseUrl, widgets, socket?, theme? }`. Wraps the admin tree. |
455
- | `admin-frontend/src/AdminScreenRouter.tsx` (NEW) | Data-driven dispatcher: reads `customScreens` from config, looks up component by name in registry, falls back to `AdminModelTable`. |
456
- | `admin-frontend/src/AdminHome.tsx` (NEW; replaces AdminModelList semantically) | Renders `home.widgets` from config. Built-in widget components for `modelStats`, `recentActivity`, `scriptRunner`, `customScreensList`, `modelsGrid`. |
457
- | `admin-frontend/src/AdminModelTable.tsx` | Add: search box (debounced, surfaces `?q=`), filter drawer (right-side, collapsible, mobile sheet), bulk-select column, actions dropdown, `sortableFields` enforcement, `pageSize` honored. |
458
- | `admin-frontend/src/AdminModelForm.tsx` | Add: fieldsets (collapsible groups), `readonlyFields`, `autocompleteFields` → async ref picker, `excludeFields` honored. |
459
- | `admin-frontend/src/AdminFieldRenderer.tsx` | Read widget map from `AdminProvider` context instead of hardcoded switch. |
460
- | `admin-frontend/src/AdminFilterDrawer.tsx` (NEW) | Right-side drawer with one input per `AdminFilter`. Builds `filter[k]=v` query string. |
461
- | `admin-frontend/src/AdminActionMenu.tsx` (NEW) | Dropdown next to bulk-select count. Confirms, sends POST to `/actions/:name`. For `background` actions, opens existing `AdminScriptRunModal`. |
462
- | `admin-frontend/src/widgets/ModelStatsWidget.tsx` (NEW) | Renders counts via `useListQuery({page:1,pageSize:1})`. |
463
- | `admin-frontend/src/widgets/RecentActivityWidget.tsx` (NEW) | Lists latest documents across selected models. |
464
- | `admin-frontend/src/widgets/ScriptRunnerWidget.tsx` (NEW) | Compact script list (existing AdminScriptList trimmed). |
465
- | `admin-frontend/src/widgets/ModelsGridWidget.tsx` (NEW) | The legacy "grid of every model" widget — the default home. |
466
- | `admin-frontend/src/widgets/CustomScreensListWidget.tsx` (NEW) | Lists plugin-contributed custom screens. |
467
- | `admin-frontend/src/types.ts` | Add: `AdminWidgetComponent`, `AdminWidgetRegistry`, `AdminContextValue`. Mirror the backend `AdminConfigResponse` shape. |
468
- | `admin-frontend/src/AdminModelList.tsx` | Deprecate — keep as a thin wrapper that renders `<AdminHome widgets={[{type:"modelsGrid"}, {type:"scriptRunner"}, ...]}>` so existing consumers don't break. Mark for removal in v2. |
469
- | `admin-frontend/src/index.tsx` | Export `AdminProvider`, `AdminHome`, `AdminScreenRouter`, widgets, types. |
470
-
471
- ### Pre-built admins (light contribution refactors)
472
-
473
- | File | Change |
474
- |------|--------|
475
- | `feature-flags/src/featureFlagsApp.ts` | Implement `adminContribution()` returning the model admin + a `feature-flags-overrides` home widget. Delete bare `featureFlagAdminConfig` export (deprecate first). |
476
- | `feature-flags/src/featureFlagAdminWidget.tsx` (NEW, in a frontend-shipping path) | The flag-overrides home widget component. |
477
- | `api/src/consent/consentApp.ts` (wherever it lives) | Implement `adminContribution()` returning ConsentForm + ConsentResponse admins, plus `widgetComponents` for `locale-content`, `locale-default`, `checkbox-list`. |
478
- | `admin-backend/src/documentStorageApp.ts` | Convert to a proper plugin contribution: `adminContribution()` returns `customScreens: [{name:"documents", displayName:"Documents", icon:"folder"}]`. Frontend component registered via the widget registry. |
479
- | `ai/src/aiAdminApp.ts` (NEW or split out of routes) | Implement `adminContribution()` returning a custom screen for AI Requests Explorer. Consumer no longer needs to hand-roll it. |
480
-
481
- ### Example apps
482
-
483
- | File | Change |
484
- |------|--------|
485
- | `example-backend/src/server.ts` | Demonstrate new pattern: pass `admin:` on each modelRouter, remove the verbose `AdminApp.models` repetition. Show declarative `home: { widgets }`. Leave one legacy `models[]` entry to demonstrate back-compat. |
486
- | `example-backend/src/api/users.ts`, `api/todos.ts` | Add `admin:` config to each. |
487
- | `example-frontend/app/admin/_layout.tsx` | Wrap in `<AdminProvider api={api} baseUrl="/admin" widgets={{...}}>`. |
488
- | `example-frontend/app/admin/[model]/index.tsx` | Replace string-switch with `<AdminScreenRouter modelName={...}>`. |
489
- | `example-frontend/app/admin/index.tsx` | Render `<AdminHome />` (was `<AdminModelList ...>`). |
490
-
491
- ### Documentation
492
-
493
- | File | Change |
494
- |------|--------|
495
- | `docs/how-to/admin-add-model.md` (NEW) | "Add an admin for your model in three lines." |
496
- | `docs/how-to/admin-custom-widget.md` (NEW) | "Build and register a custom field widget." |
497
- | `docs/how-to/admin-custom-home.md` (NEW) | "Customize the admin home page." |
498
- | `docs/how-to/admin-import-prebuilt.md` (NEW) | "Import the consent / feature-flags admins." |
499
- | `docs/reference/admin-config.md` (NEW) | Full `AdminConfig` reference, mapped from Django equivalents. |
500
- | `docs/implementationPlans/admin-improvements.md` | (this file) |
501
-
502
- ## Backward Compatibility & Migration
503
-
504
- - `AdminApp({models: [{model, listFields, ...}]})` — accepted as-is for one minor release. Logged once-per-process as `[admin] AdminApp.models entry for "Users" uses the legacy shape (listFields/fieldOverrides). Migrate to modelRouter({...,admin:{listDisplay,...}}) — see docs/how-to/admin-add-model.md.`
505
- - `listFields` accepted as an alias for `listDisplay` in the same window. Same one-time warning.
506
- - `defaultSort` accepted as an alias for `ordering`.
507
- - `hiddenFields` continues to mean "stripped from responses" (today's behavior). `excludeFields` is the new stronger version (stripped from forms AND responses). When both are set: union.
508
- - `customScreens` array prop on `AdminModelList` becomes a no-op once the consumer switches to `AdminProvider` + `AdminScreenRouter`. We log a one-time warning if both are used.
509
- - All existing endpoints (`GET /admin/config`, `GET /admin/{routePath}`, …) continue to work with the same payloads, plus additive fields. Old frontends see `capabilities.{actions,filters,fieldsets} === false` and skip the new UI affordances.
110
+ - **Shell:** Keep v2 `AdminShell` / `AdminShellLayout`.
111
+ - **Home:** Keep **`home.slots`**; resolve widget ids via `AdminProvider.widgets.home` + built-ins in `widgets/`.
112
+ - **List:** Extract **filter drawer** (`AdminFilterDrawer`); keep v2 search debounce and bulk UI (refactor to `AdminActionMenu` component, still calling `bulk-patch`).
113
+ - **Form:** Field widgets from `AdminProvider.widgets.fields`; `autocompleteFields` async ref search via list endpoint.
114
+ - **Screens:** `AdminScreenRouter` + `widgets.screens` for Documents, AI explorer, consent custom flows.
115
+ - **testIDs:** Preserve v2 ids; add drawer/menu ids from original task spec.
510
116
 
511
117
  ## Phases
512
118
 
119
+ ### Phase 0 — Baseline (complete)
120
+
121
+ Admin UI v2. See [admin-ui-v2-django-parity.md](./admin-ui-v2-django-parity.md).
122
+
513
123
  ### Phase 1 — Core API surface (`@terreno/api`)
514
- Adds `AdminConfig`, plugin contribution interface, and the event bus. No UI changes; everything stays backward compatible.
124
+
125
+ `adminTypes.ts`, `admin` on `modelRouter`, plugin + TerrenoApp extensions, `scrubAdminFields`, event bus, `excludeFields` / `readonlyFields` enforcement at router layer where applicable.
515
126
 
516
127
  ### Phase 2 — Admin backend aggregation
517
- Rewrites `AdminApp` to aggregate from registered routers and plugin contributions. Extends `/admin/config`. Adds filter parsing, action runner, server-side `readonlyFields`/`excludeFields` enforcement.
128
+
129
+ Rewrite `AdminApp` discovery; `legacy.ts`; `filterParser.ts`; `widgetIds` + `capabilities` on config; `adminFilter` async merge into list queries.
518
130
 
519
131
  ### Phase 3 — Admin frontend foundations
520
- `AdminProvider`, `AdminScreenRouter`, widget registry, `AdminHome`, built-in widgets. No new table/form features yet — keep existing screens working through the new context.
132
+
133
+ `AdminProvider`, registry, `AdminScreenRouter`, refactor `AdminHome` to use registry, extract widgets to `widgets/`.
521
134
 
522
135
  ### Phase 4 — Admin frontend feature lift
523
- Search box, filter drawer, bulk-select + actions, fieldsets, `readonlyFields`, autocomplete refs. Each is a small PR.
136
+
137
+ `AdminFilterDrawer`, `AdminActionMenu` (bulk-patch), field widgets from context, `autocompleteFields`, permission-gated UI (static flags).
524
138
 
525
139
  ### Phase 5 — Pre-built admin migrations
526
- `feature-flags`, `consent`, `documentStorage`, `ai` all adopt `adminContribution()`. Each pre-built admin gets a one-line consumer story.
527
140
 
528
- ### Phase 6 — Examples and docs
529
- `example-backend` and `example-frontend` rewritten to showcase. Four how-to docs and the reference.
141
+ `FeatureFlagsApp`, `ConsentApp`, `DocumentStorageApp`, `AIAdminApp` → `adminContribution()` + frontend widgets in `admin-frontend/src/widgets/`.
142
+
143
+ ### Phase 6 — Examples, docs, E2E (atomic)
144
+
145
+ Single PR: `example-backend` + `example-frontend` + `admin-spa`; four how-to docs + `admin-config` reference; Playwright specs.
530
146
 
531
147
  ### Phase 7 — Deprecations & polish
532
- Warning messages, telemetry/log lines, codemod note in CHANGELOG.
148
+
149
+ Legacy warnings, rule files, MCP `generate_route`, CHANGELOG.
150
+
151
+ ## Feature Flags & Migrations
152
+
153
+ - **No breaking config version bump** — stay on `schemaVersion: 2`; additive fields only.
154
+ - `AdminApp.models[]` deprecated one minor cycle with one-time warning per entry.
155
+ - `AdminModelList` `customScreens` prop deprecated when `AdminProvider` is used.
156
+
157
+ ## Activity Log & User Updates
158
+
159
+ Covered by v2 `AdminAuditLog` + `recentActivity`. No changes unless aggregation hooks need audit labels for plugin-registered models.
160
+
161
+ ## Not Included / Future Work
162
+
163
+ - Per-user `/admin/config` permission booleans ([rbac-permissions](./rbac-permissions.md)).
164
+ - Custom `AdminAction.run()` / `/actions/:name`.
165
+ - Socket.io fan-out of `admin:model.changed`.
166
+ - `home.widgets` alternate layout API.
167
+ - Cross-model global search, CSV export, granular admin roles beyond `IsAdmin`.
168
+
169
+ ## Files to Create / Modify (summary)
170
+
171
+ | Area | Key files |
172
+ |------|-----------|
173
+ | api | `adminTypes.ts`, `scrubAdminFields.ts`, `terrenoApp.ts`, `terrenoPlugin.ts`, `api.ts` |
174
+ | admin-backend | `adminApp.ts`, `legacy.ts`, `filterParser.ts`, tests |
175
+ | admin-frontend | `AdminProvider.tsx`, `AdminScreenRouter.tsx`, `AdminFilterDrawer.tsx`, `AdminActionMenu.tsx`, `widgets/*`, refactor `AdminHome.tsx` |
176
+ | plugins | `feature-flags`, consent, `documentStorageApp`, `ai/src/aiAdminApp.ts` |
177
+ | examples | `example-backend/src/server.ts`, `example-frontend/app/admin/**`, `admin-spa/**` |
178
+ | docs | `docs/how-to/admin-*.md`, `docs/reference/admin-config.md` |
179
+
180
+ ## Task List
181
+
182
+ See **[docs/tasks/admin-improvements.md](../tasks/admin-improvements.md)**.
183
+
184
+ ## Acceptance Criteria
185
+
186
+ - [ ] `admin: { ... }` on a `modelRouter` appears in `/admin/config` without duplicating entry in `AdminApp.models`.
187
+ - [ ] `new FeatureFlagsApp()` registered on `TerrenoApp` auto-registers FeatureFlag admin; `"feature-flags-overrides"` in `home.slots` renders when listed.
188
+ - [ ] `filterParser` rejects operator injection and prototype-pollution; compatible v2 list URLs still work.
189
+ - [ ] Bulk select + action uses **`bulk-patch`** or **background-tasks** (not `/actions/:name`).
190
+ - [ ] `AdminProvider` + `AdminScreenRouter` replace example-frontend string-switch; Documents + AI screens work via registry.
191
+ - [ ] `AdminFilterDrawer` replaces inline filter rail; mobile sheet behavior preserved.
192
+ - [ ] `excludeFields` / `hiddenFields` scrub populated refs using referenced model's admin config.
193
+ - [ ] `admin.realtime: true` emits scrubbed `admin:model.changed` (unit test); no socket server started.
194
+ - [ ] Legacy `AdminApp.models[]` works with one deprecation warning per entry.
195
+ - [ ] Atomic example migration: backend boots, admin-spa + example-frontend admin E2E green.
196
+ - [ ] How-to + reference docs published; types exported from `@terreno/api`, `@terreno/admin-backend`, `@terreno/admin-frontend`.
533
197
 
534
198
  ## Risks
535
199
 
536
200
  | Risk | Mitigation |
537
- |------|-----------|
538
- | Field-rename churn. | No renames. Only additive new options. Existing `listFields`, `defaultSort`, `fieldOverrides`, `fieldOrder`, `hiddenFields` keep their names. |
539
- | Plugin discovery — `AdminApp` needs the parent `TerrenoApp`. | Extend `TerrenoPlugin.register(app, openApi?, terrenoApp?)` backward-compat additively. `TerrenoApp.build()` passes itself. Internal `_buildWithContext` on `ModelRouterRegistration` carries the same reference into modelRouter. |
540
- | **routePath collisions.** Two registered modelRouters with the same `routePath` → fatal at build time (throw). | Plugin contribution and legacy `AdminApp.models` are additive: registered modelRouter wins over plugin wins over legacy. Loudest collision logs are the registered-vs-registered case. |
541
- | Filter-injection. | Per-type strict shape (see "Filter param parsing"). Mongo operator keys never pass through. Declared-only fields honored. |
542
- | Action runner footguns (deleting too many rows). | `confirm` text rendered server-side. `MAX_ACTION_IDS=1000`. Single `$in` validation; ids outside `adminFilter` scope silently excluded. Background actions get a frozen context, not the live `req`. |
543
- | Realtime payload leakage. | Emit fires *after* `excludeFields`/`hiddenFields` scrub. Verified by test that asserts `hash` is absent from the emitted payload for a User-like model. |
544
- | Widget registry typing. | Three buckets: `{ fields, home, screens }`. Each bucket has a typed component contract. Runtime fallback to a "missing widget" placeholder + one-time `console.warn` if a referenced key is absent. |
545
- | `realtime: true` emits events with nowhere to go in v1. | Default `false`. Listener is no-op (debug-logged). Documented as forward-compat. |
546
- | `AdminScreenRouter` rollout breaks consumers' existing string-switch. | Both paths supported during transition; warning emitted if consumer's `customScreens` prop and `AdminProvider` widgets are both set. |
547
- | Per-user permission booleans in `/admin/config` make it uncacheable. | `Cache-Control: private, no-store` header. Endpoint already gated by `IsAdmin`. Test guards response size stays under 50KB for 30 models. |
548
- | Background action runner uses stale `req.user`. | Captured snapshot only. Background task records `userId` separately; the snapshot is a hint, not authoritative. Re-resolve user via `userModel.findById(ctx.userId)` inside `action.run` if fresh data is needed. |
549
- | `EventEmitter` default max listeners (10). | `TerrenoApp` constructor calls `setMaxListeners(50)` on its emitter. Documented. |
550
- | Bundle size of widget registry. | Widgets are React components passed at provider construction time — consumers control which they import. Lazy-load via React.lazy is the consumer's choice (no built-in lazy support in v1). |
551
- | Custom-screen pages in `example-frontend/app/admin/consent-forms/` etc. | Phase 6 explicitly audits these directories; behaviors that the generic `AdminScreenRouter` doesn't subsume become contributed screen widgets. |
552
-
553
- ## Acceptance Criteria (high level — full per-task in the task file)
554
-
555
- 1. A model with `admin: {...}` on its `modelRouter` appears in `/admin/config` with full metadata, without being passed to `AdminApp.models`.
556
- 2. Registering `new FeatureFlagsApp(...)` causes the `FeatureFlag` admin to appear automatically and `home.widgets: ["feature-flags-overrides"]` renders its widget.
557
- 3. Search box in the table debounces and surfaces hits using the existing `/search` endpoint scoped to `searchFields`.
558
- 4. Filter drawer renders one control per `AdminFilter`; combining filters narrows the list correctly server-side.
559
- 5. Bulk-select + actions: selecting rows and choosing an action POSTs `{ids}` to `/admin/{routePath}/actions/:name`. Sync actions return updated rows; `background: true` actions route through `AdminScriptRunModal` and show progress.
560
- 6. `readonlyFields` are visible in the form but disabled, and a manual PATCH that tries to change them is silently dropped server-side.
561
- 7. `fieldsets` render as collapsible groups in `AdminModelForm`.
562
- 8. `autocompleteFields` ref pickers async-search via `/search` instead of fetching the whole referenced collection.
563
- 9. The admin home page renders the configured widgets in the configured order. Plugin-contributed widgets only appear when listed.
564
- 10. The legacy `AdminApp({models: [{listFields: [...]}]})` shape continues to work and emits exactly one deprecation warning per legacy entry.
565
- 11. No socket server is started; no realtime events fire to clients. `admin.realtime: true` causes `TerrenoApp.emit("admin:model.changed", ...)` to fire (verified in unit test).
566
- 12. `example-backend` boots; `example-frontend` admin renders; all existing example admin flows still work.
567
- 13. All new types are exported from `@terreno/api`, `@terreno/admin-backend`, `@terreno/admin-frontend`.
568
- 14. Test coverage: filter parser, action runner, readonlyFields enforcement, plugin aggregation, legacy shape adapter, widget registry resolution, search debounce.
569
-
570
- ## Out of scope (explicit)
571
-
572
- - Socket.io server / realtime UI updates.
573
- - MongoDB change-stream emitter.
574
- - True related-model inlines.
575
- - `date_hierarchy`, `prepopulated_fields`, `list_editable`, `show_facets`.
576
- - Theming / brand customization beyond `home.title`.
577
- - Multi-AdminSite (Django allows several `AdminSite` instances).
201
+ |------|------------|
202
+ | Duplicating v2 work | Phase 0 checklist; tasks mark v2 items complete; refactor don't rewrite |
203
+ | `TerrenoApp` ↔ `AdminApp` wiring | Third `terrenoApp` arg on `register`; `_buildWithContext` internal rename |
204
+ | routePath collisions | Throw on duplicate registered routers; warn on plugin vs legacy |
205
+ | Filter parser breaks existing URLs | Parser accepts v2 shapes; tests include v2 example-backend filter queries |
206
+ | Atomic example PR size | Single PR still split into logical commits inside the PR |
207
+ | Widget bundle size | Consumers pass only widgets they import at `AdminProvider` construction |