@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.
- package/dist/bootstrap.d.ts +1 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +381 -191
- package/dist/bootstrap.js.map +1 -1
- package/dist/docs/guidelines/api/skills/mongoose-schema-safety/SKILL.md +8 -0
- package/dist/docs/resources/overview.md +6 -1
- package/dist/docs/resources/rtk.md +3 -1
- package/dist/docs/resources/syncdb.md +43 -0
- package/dist/docs/upgrades/56.0.0-beta.2.md +34 -0
- package/dist/docs/upgrades/57.0.0.md +49 -0
- package/dist/docs/upgrades/57.1.0.md +126 -0
- package/dist/docs/versioned/next/README.md +15 -3
- package/dist/docs/versioned/next/explanation/README.md +3 -0
- package/dist/docs/versioned/next/explanation/admin-consent-migration.md +15 -0
- package/dist/docs/versioned/next/explanation/admin-plugin-frontend.md +12 -0
- package/dist/docs/versioned/next/explanation/announcements/rtk-deprecation.md +29 -0
- package/dist/docs/versioned/next/explanation/authentication.md +51 -58
- package/dist/docs/versioned/next/explanation/local-first-data.md +52 -0
- package/dist/docs/versioned/next/explanation/loop-engineering.md +117 -0
- package/dist/docs/versioned/next/explanation/repository-settings.md +8 -0
- package/dist/docs/versioned/next/explanation/roadmap-process.md +22 -9
- package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +39 -0
- package/dist/docs/versioned/next/how-to/README.md +3 -0
- package/dist/docs/versioned/next/how-to/admin-add-model.md +41 -0
- package/dist/docs/versioned/next/how-to/admin-custom-home.md +31 -0
- package/dist/docs/versioned/next/how-to/admin-custom-widget.md +42 -0
- package/dist/docs/versioned/next/how-to/admin-import-prebuilt.md +25 -0
- package/dist/docs/versioned/next/how-to/circleci.md +173 -0
- package/dist/docs/versioned/next/how-to/configure-better-auth.md +13 -10
- package/dist/docs/versioned/next/how-to/expose-mcp-tools.md +194 -0
- package/dist/docs/versioned/next/how-to/install-agent-skills.md +46 -0
- package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +26 -1
- package/dist/docs/versioned/next/how-to/websocket-integration.md +2 -1
- package/dist/docs/versioned/next/implementationPlans/ModularAPI.md +1 -0
- package/dist/docs/versioned/next/implementationPlans/README.md +31 -17
- package/dist/docs/versioned/next/implementationPlans/admin-improvements-research.md +136 -0
- package/dist/docs/versioned/next/implementationPlans/admin-improvements.md +157 -527
- package/dist/docs/versioned/next/implementationPlans/admin-only-research.md +4 -0
- package/dist/docs/versioned/next/implementationPlans/admin-only.md +3 -1
- package/dist/docs/versioned/next/implementationPlans/admin-script-runner.md +1 -0
- package/dist/docs/versioned/next/implementationPlans/admin-ui-v2-django-parity.md +1 -0
- package/dist/docs/versioned/next/implementationPlans/agentic-sdlc-plugin.md +160 -130
- package/dist/docs/versioned/next/implementationPlans/apierror-standard-error-redesign.md +1 -0
- package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +5 -4
- package/dist/docs/versioned/next/implementationPlans/comms-abstraction-research.md +48 -0
- package/dist/docs/versioned/next/implementationPlans/comms-abstraction.md +152 -62
- package/dist/docs/versioned/next/implementationPlans/comms-adapter-sendgrid.md +9 -9
- package/dist/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +1 -1
- package/dist/docs/versioned/next/implementationPlans/consent-forms.md +4 -0
- package/dist/docs/versioned/next/implementationPlans/design-blend-skill.md +1 -0
- package/dist/docs/versioned/next/implementationPlans/docs-site-and-versioning.md +1 -0
- package/dist/docs/versioned/next/implementationPlans/feature-flags-openfeature.md +1 -0
- package/dist/docs/versioned/next/implementationPlans/feature-flags.md +4 -0
- package/dist/docs/versioned/next/implementationPlans/infra-mcp-research.md +1 -0
- package/dist/docs/versioned/next/implementationPlans/infra-mcp.md +3 -3
- package/dist/docs/versioned/next/implementationPlans/mcp-boost-parity.md +1 -0
- package/dist/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci-research.md +1 -0
- package/dist/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci.md +2 -2
- package/dist/docs/versioned/next/implementationPlans/model-router-actions.md +4 -0
- package/dist/docs/versioned/next/implementationPlans/model-router-mcp.md +5 -4
- package/dist/docs/versioned/next/implementationPlans/offline-mode.md +3 -1
- package/dist/docs/versioned/next/implementationPlans/oss-launch-program.md +11 -2
- package/dist/docs/versioned/next/implementationPlans/pluggable-database-sqlite-research.md +150 -0
- package/dist/docs/versioned/next/implementationPlans/pluggable-database-sqlite.md +247 -0
- package/dist/docs/versioned/next/implementationPlans/positioning-django-rails-universal.md +1 -1
- package/dist/docs/versioned/next/implementationPlans/public-roadmap-github.md +2 -2
- package/dist/docs/versioned/next/implementationPlans/rbac-permissions.md +52 -15
- package/dist/docs/versioned/next/implementationPlans/rtk-to-syncdb-migration-docs.md +13 -12
- package/dist/docs/versioned/next/implementationPlans/syncdb-codegen-research.md +137 -0
- package/dist/docs/versioned/next/implementationPlans/syncdb-codegen.md +63 -59
- package/dist/docs/versioned/next/implementationPlans/syncdb-local-first.md +1 -0
- package/dist/docs/versioned/next/implementationPlans/syncdb-phase-c-design.md +1 -0
- package/dist/docs/versioned/next/implementationPlans/terreno-langfuse-integration.md +1 -0
- package/dist/docs/versioned/next/implementationPlans/terreno-syncdb-2.md +1 -0
- package/dist/docs/versioned/next/implementationPlans/upgrade-banner.md +4 -0
- package/dist/docs/versioned/next/reference/README.md +6 -1
- package/dist/docs/versioned/next/reference/admin-config.md +51 -0
- package/dist/docs/versioned/next/reference/ai.md +15 -1
- package/dist/docs/versioned/next/reference/api.md +18 -0
- package/dist/docs/versioned/next/reference/comms.md +88 -7
- package/dist/docs/versioned/next/reference/environment-variables.md +9 -1
- package/dist/docs/versioned/next/reference/{rtk.md → legacy/rtk.md} +11 -7
- package/dist/docs/versioned/next/reference/lifecycle-plugin.md +54 -0
- package/dist/docs/versioned/next/reference/mcp-server.md +7 -0
- package/dist/docs/versioned/next/reference/syncdb.md +30 -3
- package/dist/docs/versioned/next/tasks/README.md +21 -10
- package/dist/docs/versioned/next/tasks/admin-improvements.md +189 -353
- package/dist/docs/versioned/next/tasks/admin-only.md +2 -0
- package/dist/docs/versioned/next/tasks/agentic-sdlc-plugin.md +54 -160
- package/dist/docs/versioned/next/tasks/comms-abstraction.md +30 -24
- package/dist/docs/versioned/next/tasks/comms-adapter-sendgrid.md +4 -4
- package/dist/docs/versioned/next/tasks/consent-forms.md +2 -0
- package/dist/docs/versioned/next/tasks/feature-flags.md +2 -0
- package/dist/docs/versioned/next/tasks/migrate-cicd-to-circleci.md +14 -10
- package/dist/docs/versioned/next/tasks/model-router-actions.md +2 -0
- package/dist/docs/versioned/next/tasks/model-router-mcp.md +27 -27
- package/dist/docs/versioned/next/tasks/offline-mode.md +2 -0
- package/dist/docs/versioned/next/tasks/pluggable-database-sqlite.md +44 -0
- package/dist/docs/versioned/next/tasks/public-roadmap-github.md +1 -1
- package/dist/docs/versioned/next/tasks/rtk-to-syncdb-migration-docs.md +15 -15
- package/dist/docs/versioned/next/tasks/syncdb-codegen.md +37 -31
- package/dist/docs/versioned/next/tasks/upgrade-banner.md +2 -0
- package/dist/index.js +24 -55
- package/dist/index.js.map +1 -1
- package/dist/local/localServer.d.ts.map +1 -1
- package/dist/local/localServer.js +9 -8
- package/dist/local/localServer.js.map +1 -1
- package/dist/local/localTools.d.ts +1 -1
- package/dist/local/localTools.d.ts.map +1 -1
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +44 -45
- package/dist/prompts.js.map +1 -1
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +9 -2
- package/dist/resources.js.map +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +4 -2
- package/dist/tools.js.map +1 -1
- package/package.json +7 -3
- package/src/docs/guidelines/api/skills/mongoose-schema-safety/SKILL.md +8 -0
- package/src/docs/resources/overview.md +6 -1
- package/src/docs/resources/rtk.md +3 -1
- package/src/docs/resources/syncdb.md +43 -0
- package/src/docs/upgrades/56.0.0-beta.2.md +34 -0
- package/src/docs/upgrades/57.0.0.md +49 -0
- package/src/docs/upgrades/57.1.0.md +126 -0
- package/src/docs/versioned/next/README.md +15 -3
- package/src/docs/versioned/next/explanation/README.md +3 -0
- package/src/docs/versioned/next/explanation/admin-consent-migration.md +15 -0
- package/src/docs/versioned/next/explanation/admin-plugin-frontend.md +12 -0
- package/src/docs/versioned/next/explanation/announcements/rtk-deprecation.md +29 -0
- package/src/docs/versioned/next/explanation/authentication.md +51 -58
- package/src/docs/versioned/next/explanation/local-first-data.md +52 -0
- package/src/docs/versioned/next/explanation/loop-engineering.md +117 -0
- package/src/docs/versioned/next/explanation/repository-settings.md +8 -0
- package/src/docs/versioned/next/explanation/roadmap-process.md +22 -9
- package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +39 -0
- package/src/docs/versioned/next/how-to/README.md +3 -0
- package/src/docs/versioned/next/how-to/admin-add-model.md +41 -0
- package/src/docs/versioned/next/how-to/admin-custom-home.md +31 -0
- package/src/docs/versioned/next/how-to/admin-custom-widget.md +42 -0
- package/src/docs/versioned/next/how-to/admin-import-prebuilt.md +25 -0
- package/src/docs/versioned/next/how-to/circleci.md +173 -0
- package/src/docs/versioned/next/how-to/configure-better-auth.md +13 -10
- package/src/docs/versioned/next/how-to/expose-mcp-tools.md +194 -0
- package/src/docs/versioned/next/how-to/install-agent-skills.md +46 -0
- package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +26 -1
- package/src/docs/versioned/next/how-to/websocket-integration.md +2 -1
- package/src/docs/versioned/next/implementationPlans/ModularAPI.md +1 -0
- package/src/docs/versioned/next/implementationPlans/README.md +31 -17
- package/src/docs/versioned/next/implementationPlans/admin-improvements-research.md +136 -0
- package/src/docs/versioned/next/implementationPlans/admin-improvements.md +157 -527
- package/src/docs/versioned/next/implementationPlans/admin-only-research.md +4 -0
- package/src/docs/versioned/next/implementationPlans/admin-only.md +3 -1
- package/src/docs/versioned/next/implementationPlans/admin-script-runner.md +1 -0
- package/src/docs/versioned/next/implementationPlans/admin-ui-v2-django-parity.md +1 -0
- package/src/docs/versioned/next/implementationPlans/agentic-sdlc-plugin.md +160 -130
- package/src/docs/versioned/next/implementationPlans/apierror-standard-error-redesign.md +1 -0
- package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +5 -4
- package/src/docs/versioned/next/implementationPlans/comms-abstraction-research.md +48 -0
- package/src/docs/versioned/next/implementationPlans/comms-abstraction.md +152 -62
- package/src/docs/versioned/next/implementationPlans/comms-adapter-sendgrid.md +9 -9
- package/src/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +1 -1
- package/src/docs/versioned/next/implementationPlans/consent-forms.md +4 -0
- package/src/docs/versioned/next/implementationPlans/design-blend-skill.md +1 -0
- package/src/docs/versioned/next/implementationPlans/docs-site-and-versioning.md +1 -0
- package/src/docs/versioned/next/implementationPlans/feature-flags-openfeature.md +1 -0
- package/src/docs/versioned/next/implementationPlans/feature-flags.md +4 -0
- package/src/docs/versioned/next/implementationPlans/infra-mcp-research.md +1 -0
- package/src/docs/versioned/next/implementationPlans/infra-mcp.md +3 -3
- package/src/docs/versioned/next/implementationPlans/mcp-boost-parity.md +1 -0
- package/src/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci-research.md +1 -0
- package/src/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci.md +2 -2
- package/src/docs/versioned/next/implementationPlans/model-router-actions.md +4 -0
- package/src/docs/versioned/next/implementationPlans/model-router-mcp.md +5 -4
- package/src/docs/versioned/next/implementationPlans/offline-mode.md +3 -1
- package/src/docs/versioned/next/implementationPlans/oss-launch-program.md +11 -2
- package/src/docs/versioned/next/implementationPlans/pluggable-database-sqlite-research.md +150 -0
- package/src/docs/versioned/next/implementationPlans/pluggable-database-sqlite.md +247 -0
- package/src/docs/versioned/next/implementationPlans/positioning-django-rails-universal.md +1 -1
- package/src/docs/versioned/next/implementationPlans/public-roadmap-github.md +2 -2
- package/src/docs/versioned/next/implementationPlans/rbac-permissions.md +52 -15
- package/src/docs/versioned/next/implementationPlans/rtk-to-syncdb-migration-docs.md +13 -12
- package/src/docs/versioned/next/implementationPlans/syncdb-codegen-research.md +137 -0
- package/src/docs/versioned/next/implementationPlans/syncdb-codegen.md +63 -59
- package/src/docs/versioned/next/implementationPlans/syncdb-local-first.md +1 -0
- package/src/docs/versioned/next/implementationPlans/syncdb-phase-c-design.md +1 -0
- package/src/docs/versioned/next/implementationPlans/terreno-langfuse-integration.md +1 -0
- package/src/docs/versioned/next/implementationPlans/terreno-syncdb-2.md +1 -0
- package/src/docs/versioned/next/implementationPlans/upgrade-banner.md +4 -0
- package/src/docs/versioned/next/reference/README.md +6 -1
- package/src/docs/versioned/next/reference/admin-config.md +51 -0
- package/src/docs/versioned/next/reference/ai.md +15 -1
- package/src/docs/versioned/next/reference/api.md +18 -0
- package/src/docs/versioned/next/reference/comms.md +88 -7
- package/src/docs/versioned/next/reference/environment-variables.md +9 -1
- package/src/docs/versioned/next/reference/{rtk.md → legacy/rtk.md} +11 -7
- package/src/docs/versioned/next/reference/lifecycle-plugin.md +54 -0
- package/src/docs/versioned/next/reference/mcp-server.md +7 -0
- package/src/docs/versioned/next/reference/syncdb.md +30 -3
- package/src/docs/versioned/next/tasks/README.md +21 -10
- package/src/docs/versioned/next/tasks/admin-improvements.md +189 -353
- package/src/docs/versioned/next/tasks/admin-only.md +2 -0
- package/src/docs/versioned/next/tasks/agentic-sdlc-plugin.md +54 -160
- package/src/docs/versioned/next/tasks/comms-abstraction.md +30 -24
- package/src/docs/versioned/next/tasks/comms-adapter-sendgrid.md +4 -4
- package/src/docs/versioned/next/tasks/consent-forms.md +2 -0
- package/src/docs/versioned/next/tasks/feature-flags.md +2 -0
- package/src/docs/versioned/next/tasks/migrate-cicd-to-circleci.md +14 -10
- package/src/docs/versioned/next/tasks/model-router-actions.md +2 -0
- package/src/docs/versioned/next/tasks/model-router-mcp.md +27 -27
- package/src/docs/versioned/next/tasks/offline-mode.md +2 -0
- package/src/docs/versioned/next/tasks/pluggable-database-sqlite.md +44 -0
- package/src/docs/versioned/next/tasks/public-roadmap-github.md +1 -1
- package/src/docs/versioned/next/tasks/rtk-to-syncdb-migration-docs.md +15 -15
- package/src/docs/versioned/next/tasks/syncdb-codegen.md +37 -31
- package/src/docs/versioned/next/tasks/upgrade-banner.md +2 -0
- package/dist/docs/versioned/next/implementationPlans/PLAN_INDEX.md +0 -73
- package/dist/docs/versioned/next/implementationPlans/syncdb-api-inventory.md +0 -303
- package/src/docs/versioned/next/implementationPlans/PLAN_INDEX.md +0 -73
- package/src/docs/versioned/next/implementationPlans/syncdb-api-inventory.md +0 -303
|
@@ -1,577 +1,207 @@
|
|
|
1
|
-
# Implementation Plan: Admin
|
|
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
|
-
|
|
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-
|
|
14
|
+
## Non-Goals
|
|
8
15
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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
|
|
23
|
+
## Decisions
|
|
15
24
|
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
+
Treat as baseline; extend, do not rewrite:
|
|
25
43
|
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
- `
|
|
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
|
-
##
|
|
49
|
+
## Architecture
|
|
38
50
|
|
|
39
51
|
```
|
|
40
52
|
@terreno/api
|
|
41
|
-
modelRouter(
|
|
42
|
-
TerrenoApp.getRegistrations()
|
|
43
|
-
TerrenoPlugin.adminContribution?()
|
|
44
|
-
|
|
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
|
|
48
|
-
1.
|
|
49
|
-
2. plugin adminContribution()
|
|
50
|
-
3. legacy AdminApp
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
+
### Explicitly rejected
|
|
353
101
|
|
|
354
|
-
|
|
102
|
+
- `POST /admin/{resource}/actions/:name` — use `bulk-patch` / `background-tasks` instead.
|
|
355
103
|
|
|
356
|
-
|
|
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
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
529
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
539
|
-
|
|
|
540
|
-
|
|
|
541
|
-
| Filter
|
|
542
|
-
|
|
|
543
|
-
|
|
|
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 |
|