@terreno/mcp 57.3.0 → 57.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/bootstrap.d.ts +0 -18
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +10 -2428
- package/dist/bootstrap.js.map +1 -1
- package/dist/docs/guidelines/admin-frontend/guidelines/core.md +5 -1
- package/dist/docs/guidelines/api/skills/mongoose-schema-safety/SKILL.md +23 -10
- package/dist/docs/resources/syncdb.md +4 -1
- package/dist/docs/upgrades/57.4.0.md +140 -0
- package/dist/docs/versioned/next/README.md +3 -0
- package/dist/docs/versioned/next/explanation/README.md +4 -1
- package/dist/docs/versioned/next/explanation/admin-interface.md +20 -13
- package/dist/docs/versioned/next/explanation/charts.md +29 -0
- package/dist/docs/versioned/next/explanation/deployment-architecture-gcp.md +32 -0
- package/dist/docs/versioned/next/explanation/deployment-baseline.md +1 -1
- package/dist/docs/versioned/next/explanation/example-coverage.md +43 -0
- package/dist/docs/versioned/next/explanation/local-first-data.md +5 -2
- package/dist/docs/versioned/next/explanation/loop-engineering.md +9 -6
- package/dist/docs/versioned/next/explanation/model-router-actions.md +19 -2
- package/dist/docs/versioned/next/explanation/organizations.md +88 -0
- package/dist/docs/versioned/next/explanation/planning-plugins.md +114 -0
- package/dist/docs/versioned/next/explanation/positioning.md +3 -3
- package/dist/docs/versioned/next/explanation/repository-settings.md +6 -5
- package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +44 -34
- package/dist/docs/versioned/next/explanation/source-rules.md +1 -1
- package/dist/docs/versioned/next/explanation/static-analysis.md +76 -16
- package/dist/docs/versioned/next/how-to/README.md +10 -0
- package/dist/docs/versioned/next/how-to/add-gpt-chat-mascot.md +92 -0
- package/dist/docs/versioned/next/how-to/add-organizations.md +204 -0
- package/dist/docs/versioned/next/how-to/admin-add-model.md +9 -1
- package/dist/docs/versioned/next/how-to/admin-import-prebuilt.md +7 -1
- package/dist/docs/versioned/next/how-to/audit-log.md +85 -0
- package/dist/docs/versioned/next/how-to/background-jobs.md +296 -0
- package/dist/docs/versioned/next/how-to/build-admin-screens.md +42 -13
- package/dist/docs/versioned/next/how-to/charts-and-dashboards.md +37 -0
- package/dist/docs/versioned/next/how-to/circleci.md +136 -27
- package/dist/docs/versioned/next/how-to/compare-chart-rendered-snapshots.md +36 -0
- package/dist/docs/versioned/next/how-to/configure-better-auth.md +4 -0
- package/dist/docs/versioned/next/how-to/create-a-terreno-app.md +154 -0
- package/dist/docs/versioned/next/how-to/deploy-backend-to-cloud-run.md +2 -0
- package/dist/docs/versioned/next/how-to/github-actions-ci.md +4 -3
- package/dist/docs/versioned/next/how-to/github-issue-lifecycle.md +12 -8
- package/dist/docs/versioned/next/how-to/in-app-notifications.md +147 -0
- package/dist/docs/versioned/next/how-to/install-agent-skills.md +24 -12
- package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +19 -3
- package/dist/docs/versioned/next/how-to/product-announcements.md +155 -0
- package/dist/docs/versioned/next/how-to/run-mongodb-migrations.md +127 -0
- package/dist/docs/versioned/next/how-to/run-tests-locally.md +102 -0
- package/dist/docs/versioned/next/how-to/seed-a-database.md +7 -2
- package/dist/docs/versioned/next/implementationPlans/README.md +8 -5
- package/dist/docs/versioned/next/implementationPlans/admin-scripts-durable-jobs.md +44 -0
- package/dist/docs/versioned/next/implementationPlans/agent-ui-blocks.md +386 -0
- package/dist/docs/versioned/next/implementationPlans/agentic-sdlc-plugin.md +11 -8
- package/dist/docs/versioned/next/implementationPlans/announcements-surfaces.md +231 -0
- package/dist/docs/versioned/next/implementationPlans/announcements.md +289 -0
- package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +3 -1
- package/dist/docs/versioned/next/implementationPlans/charts-and-dashboards.md +246 -0
- package/dist/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +4 -3
- package/dist/docs/versioned/next/implementationPlans/create-terreno-app.md +177 -0
- package/dist/docs/versioned/next/implementationPlans/datatable-server-side-filtering.md +219 -0
- package/dist/docs/versioned/next/implementationPlans/examples-demo-coverage.md +17 -17
- package/dist/docs/versioned/next/implementationPlans/framework-audit-log.md +247 -0
- package/dist/docs/versioned/next/implementationPlans/gpt-chat-mascot.md +40 -0
- package/dist/docs/versioned/next/implementationPlans/job-queues.md +330 -0
- package/dist/docs/versioned/next/implementationPlans/knip-cleanup.md +136 -0
- package/dist/docs/versioned/next/implementationPlans/migrate-admin-to-syncdb.md +210 -0
- package/dist/docs/versioned/next/implementationPlans/mongodb-migrations-tooling.md +254 -0
- package/dist/docs/versioned/next/implementationPlans/notification-center.md +185 -0
- package/dist/docs/versioned/next/implementationPlans/org-management-ui.md +8 -3
- package/dist/docs/versioned/next/implementationPlans/positioning-django-rails-universal.md +1 -1
- package/dist/docs/versioned/next/reference/README.md +4 -0
- package/dist/docs/versioned/next/reference/admin-backend.md +83 -5
- package/dist/docs/versioned/next/reference/admin-config.md +7 -0
- package/dist/docs/versioned/next/reference/admin-frontend.md +178 -5
- package/dist/docs/versioned/next/reference/admin-spa.md +18 -1
- package/dist/docs/versioned/next/reference/ai.md +2 -0
- package/dist/docs/versioned/next/reference/announcements.md +179 -0
- package/dist/docs/versioned/next/reference/api.md +343 -4
- package/dist/docs/versioned/next/reference/comms.md +33 -0
- package/dist/docs/versioned/next/reference/create-terreno-app.md +44 -0
- package/dist/docs/versioned/next/reference/environment-variables.md +29 -0
- package/dist/docs/versioned/next/reference/jobs.md +319 -0
- package/dist/docs/versioned/next/reference/legacy/rtk.md +5 -1
- package/dist/docs/versioned/next/reference/lifecycle-plugin.md +29 -11
- package/dist/docs/versioned/next/reference/mcp-server.md +2 -43
- package/dist/docs/versioned/next/reference/scan-plugin.md +169 -0
- package/dist/docs/versioned/next/reference/syncdb.md +38 -12
- package/dist/docs/versioned/next/reference/test.md +57 -6
- package/dist/docs/versioned/next/reference/ui-performance.md +2 -1
- package/dist/docs/versioned/next/reference/ui.md +257 -3
- package/dist/docs/versioned/next/tasks/admin-scripts-durable-jobs.md +10 -0
- package/dist/docs/versioned/next/tasks/agent-ui-blocks.md +114 -0
- package/dist/docs/versioned/next/tasks/announcements-surfaces.md +101 -0
- package/dist/docs/versioned/next/tasks/announcements.md +173 -0
- package/dist/docs/versioned/next/tasks/charts-and-dashboards.md +120 -0
- package/dist/docs/versioned/next/tasks/create-terreno-app.md +82 -0
- package/dist/docs/versioned/next/tasks/datatable-server-side-filtering.md +87 -0
- package/dist/docs/versioned/next/tasks/examples-demo-coverage.md +123 -17
- package/dist/docs/versioned/next/tasks/framework-audit-log.md +86 -0
- package/dist/docs/versioned/next/tasks/gpt-chat-mascot.md +13 -0
- package/dist/docs/versioned/next/tasks/job-queues.md +144 -0
- package/dist/docs/versioned/next/tasks/knip-cleanup.md +146 -0
- package/dist/docs/versioned/next/tasks/migrate-admin-to-syncdb.md +209 -0
- package/dist/docs/versioned/next/tasks/mongodb-migrations-tooling.md +107 -0
- package/dist/docs/versioned/next/tasks/notification-center.md +102 -0
- package/dist/docs/versioned/next/tasks/org-management-ui.md +14 -14
- package/dist/docs/versioned/next/tutorials/README.md +1 -1
- package/dist/docs/versioned/next/tutorials/getting-started.md +62 -22
- package/dist/help/excerpt.d.ts +2 -0
- package/dist/help/excerpt.d.ts.map +1 -0
- package/dist/help/excerpt.js +8 -0
- package/dist/help/excerpt.js.map +1 -0
- package/dist/help/updateNotes.d.ts +42 -0
- package/dist/help/updateNotes.d.ts.map +1 -0
- package/dist/help/updateNotes.js +235 -0
- package/dist/help/updateNotes.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/local/tools/databaseQuery.d.ts +1 -0
- package/dist/local/tools/databaseQuery.d.ts.map +1 -1
- package/dist/local/tools/databaseQuery.js +1 -0
- package/dist/local/tools/databaseQuery.js.map +1 -1
- package/dist/packageGuidelines.d.ts +2 -2
- package/dist/packageGuidelines.d.ts.map +1 -1
- package/dist/packageGuidelines.js +2 -8
- package/dist/packageGuidelines.js.map +1 -1
- package/dist/resources.d.ts +3 -0
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +3 -0
- package/dist/resources.js.map +1 -1
- package/dist/search/chunker.d.ts +1 -0
- package/dist/search/chunker.d.ts.map +1 -1
- package/dist/search/chunker.js +1 -0
- package/dist/search/chunker.js.map +1 -1
- package/dist/search/docIndex.d.ts +1 -5
- package/dist/search/docIndex.d.ts.map +1 -1
- package/dist/search/docIndex.js +1 -0
- package/dist/search/docIndex.js.map +1 -1
- package/dist/search/docVersion.d.ts +2 -1
- package/dist/search/docVersion.d.ts.map +1 -1
- package/dist/search/docVersion.js +3 -1
- package/dist/search/docVersion.js.map +1 -1
- package/dist/tools.d.ts +2 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +113 -2
- package/dist/tools.js.map +1 -1
- package/package.json +28 -27
- package/src/docs/guidelines/admin-frontend/guidelines/core.md +5 -1
- package/src/docs/guidelines/api/skills/mongoose-schema-safety/SKILL.md +23 -10
- package/src/docs/resources/syncdb.md +4 -1
- package/src/docs/upgrades/57.4.0.md +140 -0
- package/src/docs/versioned/next/README.md +3 -0
- package/src/docs/versioned/next/explanation/README.md +4 -1
- package/src/docs/versioned/next/explanation/admin-interface.md +20 -13
- package/src/docs/versioned/next/explanation/charts.md +29 -0
- package/src/docs/versioned/next/explanation/deployment-architecture-gcp.md +32 -0
- package/src/docs/versioned/next/explanation/deployment-baseline.md +1 -1
- package/src/docs/versioned/next/explanation/example-coverage.md +43 -0
- package/src/docs/versioned/next/explanation/local-first-data.md +5 -2
- package/src/docs/versioned/next/explanation/loop-engineering.md +9 -6
- package/src/docs/versioned/next/explanation/model-router-actions.md +19 -2
- package/src/docs/versioned/next/explanation/organizations.md +88 -0
- package/src/docs/versioned/next/explanation/planning-plugins.md +114 -0
- package/src/docs/versioned/next/explanation/positioning.md +3 -3
- package/src/docs/versioned/next/explanation/repository-settings.md +6 -5
- package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +44 -34
- package/src/docs/versioned/next/explanation/source-rules.md +1 -1
- package/src/docs/versioned/next/explanation/static-analysis.md +76 -16
- package/src/docs/versioned/next/how-to/README.md +10 -0
- package/src/docs/versioned/next/how-to/add-gpt-chat-mascot.md +92 -0
- package/src/docs/versioned/next/how-to/add-organizations.md +204 -0
- package/src/docs/versioned/next/how-to/admin-add-model.md +9 -1
- package/src/docs/versioned/next/how-to/admin-import-prebuilt.md +7 -1
- package/src/docs/versioned/next/how-to/audit-log.md +85 -0
- package/src/docs/versioned/next/how-to/background-jobs.md +296 -0
- package/src/docs/versioned/next/how-to/build-admin-screens.md +42 -13
- package/src/docs/versioned/next/how-to/charts-and-dashboards.md +37 -0
- package/src/docs/versioned/next/how-to/circleci.md +136 -27
- package/src/docs/versioned/next/how-to/compare-chart-rendered-snapshots.md +36 -0
- package/src/docs/versioned/next/how-to/configure-better-auth.md +4 -0
- package/src/docs/versioned/next/how-to/create-a-terreno-app.md +154 -0
- package/src/docs/versioned/next/how-to/deploy-backend-to-cloud-run.md +2 -0
- package/src/docs/versioned/next/how-to/github-actions-ci.md +4 -3
- package/src/docs/versioned/next/how-to/github-issue-lifecycle.md +12 -8
- package/src/docs/versioned/next/how-to/in-app-notifications.md +147 -0
- package/src/docs/versioned/next/how-to/install-agent-skills.md +24 -12
- package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +19 -3
- package/src/docs/versioned/next/how-to/product-announcements.md +155 -0
- package/src/docs/versioned/next/how-to/run-mongodb-migrations.md +127 -0
- package/src/docs/versioned/next/how-to/run-tests-locally.md +102 -0
- package/src/docs/versioned/next/how-to/seed-a-database.md +7 -2
- package/src/docs/versioned/next/implementationPlans/README.md +8 -5
- package/src/docs/versioned/next/implementationPlans/admin-scripts-durable-jobs.md +44 -0
- package/src/docs/versioned/next/implementationPlans/agent-ui-blocks.md +386 -0
- package/src/docs/versioned/next/implementationPlans/agentic-sdlc-plugin.md +11 -8
- package/src/docs/versioned/next/implementationPlans/announcements-surfaces.md +231 -0
- package/src/docs/versioned/next/implementationPlans/announcements.md +289 -0
- package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +3 -1
- package/src/docs/versioned/next/implementationPlans/charts-and-dashboards.md +246 -0
- package/src/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +4 -3
- package/src/docs/versioned/next/implementationPlans/create-terreno-app.md +177 -0
- package/src/docs/versioned/next/implementationPlans/datatable-server-side-filtering.md +219 -0
- package/src/docs/versioned/next/implementationPlans/examples-demo-coverage.md +17 -17
- package/src/docs/versioned/next/implementationPlans/framework-audit-log.md +247 -0
- package/src/docs/versioned/next/implementationPlans/gpt-chat-mascot.md +40 -0
- package/src/docs/versioned/next/implementationPlans/job-queues.md +330 -0
- package/src/docs/versioned/next/implementationPlans/knip-cleanup.md +136 -0
- package/src/docs/versioned/next/implementationPlans/migrate-admin-to-syncdb.md +210 -0
- package/src/docs/versioned/next/implementationPlans/mongodb-migrations-tooling.md +254 -0
- package/src/docs/versioned/next/implementationPlans/notification-center.md +185 -0
- package/src/docs/versioned/next/implementationPlans/org-management-ui.md +8 -3
- package/src/docs/versioned/next/implementationPlans/positioning-django-rails-universal.md +1 -1
- package/src/docs/versioned/next/reference/README.md +4 -0
- package/src/docs/versioned/next/reference/admin-backend.md +83 -5
- package/src/docs/versioned/next/reference/admin-config.md +7 -0
- package/src/docs/versioned/next/reference/admin-frontend.md +178 -5
- package/src/docs/versioned/next/reference/admin-spa.md +18 -1
- package/src/docs/versioned/next/reference/ai.md +2 -0
- package/src/docs/versioned/next/reference/announcements.md +179 -0
- package/src/docs/versioned/next/reference/api.md +343 -4
- package/src/docs/versioned/next/reference/comms.md +33 -0
- package/src/docs/versioned/next/reference/create-terreno-app.md +44 -0
- package/src/docs/versioned/next/reference/environment-variables.md +29 -0
- package/src/docs/versioned/next/reference/jobs.md +319 -0
- package/src/docs/versioned/next/reference/legacy/rtk.md +5 -1
- package/src/docs/versioned/next/reference/lifecycle-plugin.md +29 -11
- package/src/docs/versioned/next/reference/mcp-server.md +2 -43
- package/src/docs/versioned/next/reference/scan-plugin.md +169 -0
- package/src/docs/versioned/next/reference/syncdb.md +38 -12
- package/src/docs/versioned/next/reference/test.md +57 -6
- package/src/docs/versioned/next/reference/ui-performance.md +2 -1
- package/src/docs/versioned/next/reference/ui.md +257 -3
- package/src/docs/versioned/next/tasks/admin-scripts-durable-jobs.md +10 -0
- package/src/docs/versioned/next/tasks/agent-ui-blocks.md +114 -0
- package/src/docs/versioned/next/tasks/announcements-surfaces.md +101 -0
- package/src/docs/versioned/next/tasks/announcements.md +173 -0
- package/src/docs/versioned/next/tasks/charts-and-dashboards.md +120 -0
- package/src/docs/versioned/next/tasks/create-terreno-app.md +82 -0
- package/src/docs/versioned/next/tasks/datatable-server-side-filtering.md +87 -0
- package/src/docs/versioned/next/tasks/examples-demo-coverage.md +123 -17
- package/src/docs/versioned/next/tasks/framework-audit-log.md +86 -0
- package/src/docs/versioned/next/tasks/gpt-chat-mascot.md +13 -0
- package/src/docs/versioned/next/tasks/job-queues.md +144 -0
- package/src/docs/versioned/next/tasks/knip-cleanup.md +146 -0
- package/src/docs/versioned/next/tasks/migrate-admin-to-syncdb.md +209 -0
- package/src/docs/versioned/next/tasks/mongodb-migrations-tooling.md +107 -0
- package/src/docs/versioned/next/tasks/notification-center.md +102 -0
- package/src/docs/versioned/next/tasks/org-management-ui.md +14 -14
- package/src/docs/versioned/next/tutorials/README.md +1 -1
- package/src/docs/versioned/next/tutorials/getting-started.md +62 -22
|
@@ -86,15 +86,15 @@ The repo states that `example-frontend` and `example-backend` serve as documenta
|
|
|
86
86
|
| Admin panel (embedded) | yes | yes | — |
|
|
87
87
|
| Admin SPA | yes | n/a | — |
|
|
88
88
|
| AI streaming chat | yes | yes | — |
|
|
89
|
-
| AI structured output |
|
|
89
|
+
| AI structured output | no | no | **gap** — chat streaming only; see [example-coverage.md](../explanation/example-coverage.md) |
|
|
90
90
|
| Feature flags + live updates | yes | yes | — |
|
|
91
91
|
| Websockets / realtime | yes | yes | — |
|
|
92
92
|
| Consent forms | yes | yes | — |
|
|
93
93
|
| File upload / GCS | yes | yes | — |
|
|
94
|
-
| Better Auth |
|
|
95
|
-
| syncdb local-first |
|
|
96
|
-
| RBAC |
|
|
97
|
-
| Background jobs | no | no |
|
|
94
|
+
| Better Auth | yes | yes | — |
|
|
95
|
+
| syncdb local-first | yes | yes | shipped in examples; #869 closed |
|
|
96
|
+
| RBAC | yes | yes | shipped; see [rbac-permissions.md](rbac-permissions.md) |
|
|
97
|
+
| Background jobs | no | no | **gap** — no generic queue example |
|
|
98
98
|
|
|
99
99
|
Every `?` must be resolved by reading the example source, and every real gap either filled or recorded as a known gap.
|
|
100
100
|
|
|
@@ -149,15 +149,15 @@ See [`docs/tasks/examples-demo-coverage.md`](../tasks/examples-demo-coverage.md)
|
|
|
149
149
|
|
|
150
150
|
## Acceptance Criteria
|
|
151
151
|
|
|
152
|
-
- [
|
|
153
|
-
- [
|
|
154
|
-
- [
|
|
155
|
-
- [
|
|
156
|
-
- [
|
|
157
|
-
- [
|
|
158
|
-
- [
|
|
159
|
-
- [
|
|
160
|
-
- [
|
|
161
|
-
- [
|
|
162
|
-
- [
|
|
163
|
-
- [
|
|
152
|
+
- [x] Every component exported from `ui/src/index.tsx` has a demo story or an allowlist entry with a stated reason.
|
|
153
|
+
- [x] `bun run scripts/check-demo-coverage.ts` fails when a newly exported component has no story and no allowlist entry, and runs in `ui-demo-ci.yml`.
|
|
154
|
+
- [x] `demo/package.json`'s `test:ci` runs something real.
|
|
155
|
+
- [x] Stories exist for all P0 components, each showing multiple states (default, loading, error, disabled) where applicable.
|
|
156
|
+
- [x] Every published package's CI runs `scripts/check-coverage.ts` at the 95% threshold.
|
|
157
|
+
- [x] Packages currently below 95% have a tracked catch-up plan; no package is permanently exempt.
|
|
158
|
+
- [x] Every published package is covered by CI running its tests and coverage check, either through a dedicated workflow or the matrix job.
|
|
159
|
+
- [x] Codecov receives uploads from every package's CI and reports per-PR deltas.
|
|
160
|
+
- [x] `README.md` shows a coverage badge that reflects reality.
|
|
161
|
+
- [x] `docs/explanation/example-coverage.md` resolves every `?` in the capability matrix, and each real gap is either filled with an example or recorded as a known gap with an issue link.
|
|
162
|
+
- [x] `CONTRIBUTING.md` states the 95% coverage expectation and how to run coverage locally per package.
|
|
163
|
+
- [x] `bun run lint`, `bun run compile`, and the full test suite pass.
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Implementation Plan: Framework-level audit log
|
|
2
|
+
|
|
3
|
+
**Status:** Approved
|
|
4
|
+
**Branch:** `cursor/framework-audit-log-grow-0d6c`
|
|
5
|
+
**Owner:** —
|
|
6
|
+
**Created:** 2026-09-10
|
|
7
|
+
**Roadmap issue:** https://github.com/FlourishHealth/terreno/issues/1186 (implementation PRs use `Fixes #1186`)
|
|
8
|
+
**Task list:** [framework-audit-log.md](../tasks/framework-audit-log.md)
|
|
9
|
+
**Program:** [B2B platform](b2b-platform-program.md)
|
|
10
|
+
**Depends on:** — (soft: `organizationId` now; org-admin list filter waits on [org-management-ui](org-management-ui.md))
|
|
11
|
+
**RTK deprecation flag:** None — admin viewer uses generated OpenAPI / `useAdminApi`, not syncdb
|
|
12
|
+
|
|
13
|
+
## Goal
|
|
14
|
+
|
|
15
|
+
Opt-in `AuditApp` in `@terreno/api` persists append-only `AuditEvent` rows for successful mutations: who changed which record, with changed-field before/after diffs. The existing admin Platform **Audit Log** lists them. Default retention is forever. This is the B2B compliance log; it generalizes example `AdminAuditLog` without replacing consent `ConsentResponse` rows.
|
|
16
|
+
|
|
17
|
+
## Non-Goals
|
|
18
|
+
|
|
19
|
+
- Waiting on `org-management-ui` to start (no `OrgsApp` in tree today).
|
|
20
|
+
- Default-on `modelRouter` product CRUD.
|
|
21
|
+
- Consent signing history (`ConsentResponse` + `GET /consents/audit/:userId` stay).
|
|
22
|
+
- Login, failed auth, permission denials (except existing `RbacAudit` `denied: true` fanned through `auditSink`).
|
|
23
|
+
- Sync mutate / outbox / socket writes (`@terreno/syncdb`). HTTP `modelRouter` only.
|
|
24
|
+
- Admin hard-delete of events; SIEM export; legal hold.
|
|
25
|
+
- Full-document snapshots.
|
|
26
|
+
- Per-org-admin filtered lists (store `organizationId` now; filter when org context ships).
|
|
27
|
+
|
|
28
|
+
## Decisions
|
|
29
|
+
|
|
30
|
+
| ID | Question | Choice |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| Q1 | Org dependency | Ship now. Optional `organizationId` from the mutated document or `req.organization` when present. |
|
|
33
|
+
| Q2 | Default on | Off. `AuditApp` plugin + per-router `audit: true`. **AdminApp writes `AuditEvent` automatically when `AuditApp` is registered.** |
|
|
34
|
+
| Q3 | Events | Successful create/update/delete and array mutations on audited routers; AdminApp → same collection; RBAC via existing `auditSink`; consent stays domain-specific. |
|
|
35
|
+
| Q4 | Mutability / retention | HTTP list+read only. Create only via framework writers. No PATCH/DELETE. **Default retain forever.** Optional `retentionDays > 0` Mongo TTL. |
|
|
36
|
+
| Q5 | Diffs | Changed fields only. Auto-redact `password`, `hash`, `salt`, `token`, `secret`, `refreshToken` (case-insensitive last path segment) plus per-router `redact`. Never log full request bodies. |
|
|
37
|
+
|
|
38
|
+
**Recorded assumptions (not grilled):** No default-connection singleton (same as `RbacAudit`: `createAuditEventModel(connection)`). Example-backend **replaces** `AdminAuditLog` with `AuditEvent`. `onAdminAudit` remains an extra sink. Writes are best-effort (`logger.error`, mutation still succeeds). Persist is fire-and-forget on the request path. Optional Cloud Tasks enqueue (`GCP_TASKS_AUDIT_QUEUE` + `AUDIT_TASKS_URL`) writes Mongo on a worker via `POST /internal/audit-events`. `AuditEvent` is never audited. `audit: true` without `AuditApp` skips the write and logs an error once per process. Array mutations use `verb: "updated"` and `operation` `arrayPush` / `arrayUpdate` / `arrayRemove`. `organizationId` is stored as string. Admin list uses `Permissions.IsAdmin` until org RBAC lands.
|
|
39
|
+
|
|
40
|
+
## Architecture
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
AuditApp({retentionDays?})
|
|
44
|
+
→ createAuditEventModel(connection)
|
|
45
|
+
→ set process recorder
|
|
46
|
+
→ modelRouter("/audit-events", AuditEvent, {create/update/delete: [], list/read: IsAdmin})
|
|
47
|
+
→ adminContribution() for Platform Audit Log
|
|
48
|
+
|
|
49
|
+
modelRouter(..., {audit: true | {redact?: string[]}})
|
|
50
|
+
→ after successful create/update/delete/array *
|
|
51
|
+
→ recordAuditEvent({actorId, modelName, recordId, recordLabel?, organizationId?, verb, operation, before, after, source: "modelRouter"})
|
|
52
|
+
|
|
53
|
+
AdminApp
|
|
54
|
+
→ after successful admin CRUD, if recorder present
|
|
55
|
+
→ recordAuditEvent({..., source: "admin"})
|
|
56
|
+
→ then onAdminAudit? (unchanged extra sink)
|
|
57
|
+
|
|
58
|
+
createAccess({auditSink: persistRbacAuditToAuditEvent})
|
|
59
|
+
→ source: "rbac"; before/after from permissionDelta when present
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`AuditApp` implements `TerrenoPlugin` (+ `adminContribution` like FeatureFlags). Importing `@terreno/api` does **not** register `AuditEvent` on `mongoose.connection`.
|
|
63
|
+
|
|
64
|
+
### Config shape
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
interface AuditAppOptions {
|
|
68
|
+
enqueue?: (write: AuditEventWrite) => Promise<void>;
|
|
69
|
+
processQueuePath?: string;
|
|
70
|
+
processQueueSecret?: string;
|
|
71
|
+
/** Mongo TTL in days. Omit or 0 = forever (no TTL index). */
|
|
72
|
+
retentionDays?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface ModelRouterAuditOptions {
|
|
76
|
+
redact?: string[]; // extra field names; merged with defaults
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// modelRouter options:
|
|
80
|
+
audit?: boolean | ModelRouterAuditOptions;
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`audit: true` equals `{redact: []}`.
|
|
84
|
+
|
|
85
|
+
### Writer contract
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
interface AuditEventWrite {
|
|
89
|
+
actorId?: string;
|
|
90
|
+
after?: Record<string, unknown>;
|
|
91
|
+
before?: Record<string, unknown>;
|
|
92
|
+
modelName: string;
|
|
93
|
+
operation: "create" | "update" | "delete" | "arrayPush" | "arrayUpdate" | "arrayRemove";
|
|
94
|
+
organizationId?: string;
|
|
95
|
+
recordId?: string;
|
|
96
|
+
recordLabel?: string;
|
|
97
|
+
source: "admin" | "modelRouter" | "rbac";
|
|
98
|
+
verb: "created" | "deleted" | "updated";
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Diff helper: shallow (plus one-level nested objects) comparison of previous JSON vs next JSON after redaction. Unchanged keys omitted. Create: `before` omitted, `after` is redacted new fields. Delete: `after` omitted, `before` is redacted last state.
|
|
103
|
+
|
|
104
|
+
`organizationId` resolution order: `req.organization?.id` / `_id`, else string of `doc.organizationId` when present.
|
|
105
|
+
|
|
106
|
+
## Models
|
|
107
|
+
|
|
108
|
+
`AuditEvent` (collection `auditevents`):
|
|
109
|
+
|
|
110
|
+
| Field | Type | Notes |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| `actorId` | ObjectId, ref User, indexed | Optional (system / missing user) |
|
|
113
|
+
| `modelName` | string, required, indexed | |
|
|
114
|
+
| `recordId` | string, indexed | |
|
|
115
|
+
| `recordLabel` | string | Short label (admin list fields / `title` / `name` / `email`) |
|
|
116
|
+
| `organizationId` | string, indexed | Optional |
|
|
117
|
+
| `verb` | `created` \| `updated` \| `deleted`, indexed | Widget-compatible |
|
|
118
|
+
| `operation` | string, required | Fine-grained verb |
|
|
119
|
+
| `source` | `admin` \| `modelRouter` \| `rbac`, indexed | |
|
|
120
|
+
| `before` / `after` | Mixed | Changed fields only, redacted |
|
|
121
|
+
| `created` / `updated` | dates | `createdUpdatedPlugin`; no `isDeletedPlugin` (append-only) |
|
|
122
|
+
|
|
123
|
+
Indexes: `{created: -1}`, `{modelName: 1, recordId: 1, created: -1}`. Optional TTL on `created` when `retentionDays > 0`. Every field has `description`. Five-type pattern co-located with the schema. `strict: "throw"`.
|
|
124
|
+
|
|
125
|
+
HTTP: `create`/`update`/`delete` empty permission arrays. No public POST/PATCH/DELETE even for admins. Denied list/create (including non-admin GET list) returns **405**, matching `permissionMiddleware`; instance-level denials after a loaded document remain **403**.
|
|
126
|
+
|
|
127
|
+
## APIs
|
|
128
|
+
|
|
129
|
+
| Method | Path | Who | Behavior |
|
|
130
|
+
| --- | --- | --- | --- |
|
|
131
|
+
| GET | `/audit-events` | IsAdmin | Paginated list, default sort `-created`, `queryFields`: `modelName`, `recordId`, `actorId`, `verb`, `source`, `organizationId` |
|
|
132
|
+
| GET | `/audit-events/:id` | IsAdmin | Read one |
|
|
133
|
+
| POST/PATCH/DELETE | `/audit-events` | — | Disabled |
|
|
134
|
+
|
|
135
|
+
Admin mounts the same model via `adminContribution` (or example registers it once — do **not** double-mount). Route path includes `audit-log` **or** model name `AuditEvent` so `AdminShell.isAuditLogModel` lifts it into Platform.
|
|
136
|
+
|
|
137
|
+
RBAC helper `persistRbacAuditToAuditEvent(model)` returns an `RbacAuditSink`. Built-in `RbacAudit` collection unchanged.
|
|
138
|
+
|
|
139
|
+
## Notifications
|
|
140
|
+
|
|
141
|
+
None.
|
|
142
|
+
|
|
143
|
+
## UI
|
|
144
|
+
|
|
145
|
+
Reuse Platform Audit Log + `RecentActivityWidget`. Update `isAuditLogModel` to match `AuditEvent` (keep `AdminAuditLog` and `audit-log` path for leftover apps). List columns: `created`, `verb`, `modelName`, `recordLabel`, `actorId`. Detail form is read-only; show `before`/`after` as JSON (existing object field rendering is enough). No new custom screen.
|
|
146
|
+
|
|
147
|
+
example-backend: register `AuditApp`, enable `audit: true` on `Todo`, drop `AdminAuditLog` model and `onAdminAudit` persistence (optional `onAdminAudit` may remain no-op or be removed). Point home `recentActivity` at `AuditEvent`. RBAC `auditSink` uses the helper. Scripts that count/wipe `AdminAuditLog` switch to `AuditEvent` (wipe only in test/dev scripts — production HTTP still cannot delete).
|
|
148
|
+
|
|
149
|
+
## Docs in this slice
|
|
150
|
+
|
|
151
|
+
| Page | Change |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| `docs/how-to/audit-log.md` | **New.** Enable `AuditApp`, per-router `audit`, AdminApp auto-write, redaction, TTL, append-only, org field. |
|
|
154
|
+
| `docs/reference/api.md` | `AuditApp`, `audit` option, `persistRbacAuditToAuditEvent`, no default-connection model. |
|
|
155
|
+
| `docs/explanation/admin-interface.md` | Platform Audit Log is `AuditEvent` when `AuditApp` is registered. |
|
|
156
|
+
| `docs/reference/admin-backend.md` | `onAdminAudit` is extra; framework write when `AuditApp` present. |
|
|
157
|
+
| `docs/explanation/roadmap-seed-issues.md` | IP + task URLs; soft org depend. |
|
|
158
|
+
| `docs/implementationPlans/b2b-platform-program.md` | Row for this IP. |
|
|
159
|
+
| `.rulesync/rules/api/00-api.mdc` | AuditApp + `audit` option. |
|
|
160
|
+
| `CHANGELOG.md` | Added opt-in framework audit log. |
|
|
161
|
+
|
|
162
|
+
## Testing
|
|
163
|
+
|
|
164
|
+
Tracer: `TerrenoApp` + `AuditApp` + test model with `audit: true` → authenticated PATCH changes `title` → `GET /audit-events` as admin returns one row with `verb: "updated"`, `before.title` / `after.title`, no `password` if present.
|
|
165
|
+
|
|
166
|
+
Cases:
|
|
167
|
+
|
|
168
|
+
- Plugin omitted: `audit: true` does not throw on CRUD; no collection on default connection; error logged once.
|
|
169
|
+
- Create/update/delete each write one event; actorId matches `req.user`.
|
|
170
|
+
- Unchanged fields absent from `before`/`after`.
|
|
171
|
+
- Default redact list strips secrets even if not in `redact`.
|
|
172
|
+
- Per-router `redact: ["ssn"]` strips `ssn`.
|
|
173
|
+
- Array push writes `operation: "arrayPush"`, `verb: "updated"`.
|
|
174
|
+
- Admin POST writes `source: "admin"` without `onAdminAudit`.
|
|
175
|
+
- `onAdminAudit` still fires when both configured; mutation 201 if either sink throws.
|
|
176
|
+
- AuditEvent HTTP POST is 404/not registered; PATCH/DELETE disabled.
|
|
177
|
+
- `organizationId` copied from document; from `req.organization` when set.
|
|
178
|
+
- `retentionDays` omitted: no TTL index. `retentionDays: 1`: TTL index present (assert index spec; do not sleep a day).
|
|
179
|
+
- Example RBAC role change fans into `AuditEvent` with `source: "rbac"`.
|
|
180
|
+
- Existing `onAdminAudit` tests stay green (best-effort extra sink).
|
|
181
|
+
- Admin frontend: `isAuditLogModel` true for `AuditEvent`; Recent Activity still skips when no audit model.
|
|
182
|
+
|
|
183
|
+
Skills: `terreno-backend-api`, `mongoose-schema-safety`, `backend-test-env` (if tests touch env), `building-admin-interfaces`, `verify-ui-changes` (example `/admin` audit log after Todo/admin mutation), `update-docs`, `generate-sdk` if example OpenAPI admin SDK lists the new route.
|
|
184
|
+
|
|
185
|
+
## Risks
|
|
186
|
+
|
|
187
|
+
| Risk | Mitigation |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| Write volume / PII | Opt-in per router; redaction; append-only HTTP. |
|
|
190
|
+
| Missing plugin | No-op + once-per-process error; docs. |
|
|
191
|
+
| Example `AdminAuditLog` break | Replace in the same slice; shell still matches old name. |
|
|
192
|
+
| TTL index change later | How-to: drop old TTL index when changing `retentionDays`. |
|
|
193
|
+
| Sync bypass | Documented non-goal; follow-up IP. |
|
|
194
|
+
| Org filter incomplete | Field stored; list filter is `org-management-ui`. |
|
|
195
|
+
|
|
196
|
+
## Phases
|
|
197
|
+
|
|
198
|
+
1. Tracer: model + `AuditApp` + `audit: true` create/update/delete + list/read API + redaction.
|
|
199
|
+
2. Array mutations + `organizationId`.
|
|
200
|
+
3. AdminApp auto-write + example migration + shell matcher + RBAC helper.
|
|
201
|
+
4. Optional TTL.
|
|
202
|
+
5. Docs, changelog, rulesync, seed, B2B table.
|
|
203
|
+
|
|
204
|
+
## Feature Flags & Migrations
|
|
205
|
+
|
|
206
|
+
No feature flag. New collection. example-backend drops `AdminAuditLog` (example-only; no published schema). Apps that copied `AdminAuditLog` keep it until they register `AuditApp`.
|
|
207
|
+
|
|
208
|
+
## Activity Log & User Updates
|
|
209
|
+
|
|
210
|
+
This IP **is** the activity log. No `UserUpdate` spam.
|
|
211
|
+
|
|
212
|
+
## Not Included / Future Work
|
|
213
|
+
|
|
214
|
+
- Org-admin scoped list (`org-management-ui`).
|
|
215
|
+
- Sync-path audit.
|
|
216
|
+
- Auth / login events.
|
|
217
|
+
- SIEM export, legal hold, immutable object storage.
|
|
218
|
+
- Custom diff UI beyond JSON fields.
|
|
219
|
+
|
|
220
|
+
## Files to Create / Modify
|
|
221
|
+
|
|
222
|
+
| File | Role |
|
|
223
|
+
| --- | --- |
|
|
224
|
+
| `api/src/audit/*` | Model, recorder, diff/redact, `AuditApp`, tests |
|
|
225
|
+
| `api/src/api.ts` | `audit` option + post hooks |
|
|
226
|
+
| `api/src/index.ts` | Exports |
|
|
227
|
+
| `admin-backend/src/adminApp.ts` | Auto-write when recorder present |
|
|
228
|
+
| `admin-frontend/src/AdminShell.tsx`, `AdminHome.tsx` | `isAuditLogModel` |
|
|
229
|
+
| `example-backend/src/server.ts`, `access.ts`, models, scripts | `AuditApp`; drop `AdminAuditLog` |
|
|
230
|
+
| Docs listed above | Same slice |
|
|
231
|
+
|
|
232
|
+
## Task List
|
|
233
|
+
|
|
234
|
+
[docs/tasks/framework-audit-log.md](../tasks/framework-audit-log.md)
|
|
235
|
+
|
|
236
|
+
## Acceptance Criteria
|
|
237
|
+
|
|
238
|
+
- [ ] `AuditApp` omitted: no `AuditEvent` on the default connection; product CRUD unchanged.
|
|
239
|
+
- [ ] `audit: true` + `AuditApp`: create/update/delete persist one event with actor, model, record, changed-field diff, redacted secrets.
|
|
240
|
+
- [ ] HTTP `/audit-events` is list+read for admin only; create/update/delete disabled.
|
|
241
|
+
- [ ] Admin mutations write `AuditEvent` when the plugin is registered, without requiring `onAdminAudit`.
|
|
242
|
+
- [ ] `onAdminAudit` still runs as an extra best-effort sink.
|
|
243
|
+
- [ ] example-backend Platform Audit Log and Recent Activity read `AuditEvent`; `AdminAuditLog` removed.
|
|
244
|
+
- [ ] RBAC `auditSink` helper writes `source: "rbac"` without dropping `RbacAudit`.
|
|
245
|
+
- [ ] Default: no TTL. `retentionDays > 0` adds TTL on `created`.
|
|
246
|
+
- [ ] How-to + api reference + admin explanation match the shipped design.
|
|
247
|
+
- [ ] Merging the implementation PR closes https://github.com/FlourishHealth/terreno/issues/1186 (`Fixes #1186`).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# GPTChat optional mascot
|
|
2
|
+
|
|
3
|
+
**Status:** Complete
|
|
4
|
+
**Branch:** `cursor/gpt-chat-mascot-d39f`
|
|
5
|
+
**Owner:** cloud agent
|
|
6
|
+
**Created:** 2026-09-15
|
|
7
|
+
|
|
8
|
+
## Goal
|
|
9
|
+
|
|
10
|
+
Let consuming apps brand the GPT screen with their own mascot. Terreno does not ship a default character.
|
|
11
|
+
|
|
12
|
+
## Non-Goals
|
|
13
|
+
|
|
14
|
+
- Bundling artwork, Lottie files, or a Terreno fox in `@terreno/ui`
|
|
15
|
+
- Animating the mascot from `isStreaming` inside GPTChat (consumers wrap their own node)
|
|
16
|
+
- Showing the mascot beside assistant bubbles
|
|
17
|
+
|
|
18
|
+
## Decisions
|
|
19
|
+
|
|
20
|
+
| Question | Decision |
|
|
21
|
+
|----------|----------|
|
|
22
|
+
| Who owns the asset? | Consumer. `mascot?: React.ReactNode` |
|
|
23
|
+
| When is it visible? | Only while `currentMessages.length === 0` |
|
|
24
|
+
| Default? | Omitted — existing empty chat unchanged |
|
|
25
|
+
|
|
26
|
+
## UI
|
|
27
|
+
|
|
28
|
+
`GPTChat` empty-state hero: optional mascot slot, then existing suggested prompts.
|
|
29
|
+
|
|
30
|
+
## Task List
|
|
31
|
+
|
|
32
|
+
[docs/tasks/gpt-chat-mascot.md](../tasks/gpt-chat-mascot.md)
|
|
33
|
+
|
|
34
|
+
## Acceptance Criteria
|
|
35
|
+
|
|
36
|
+
- [ ] Omitting `mascot` leaves the empty chat unchanged
|
|
37
|
+
- [ ] Passing `mascot` shows it on an empty chat, including with suggested prompts
|
|
38
|
+
- [ ] Messages hide the mascot
|
|
39
|
+
- [ ] Demo story and example app show a consumer node
|
|
40
|
+
- [ ] Docs describe the optional slot
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# Implementation Plan: Durable background jobs (`@terreno/jobs`)
|
|
2
|
+
|
|
3
|
+
**Status:** Complete — 2026-09-10
|
|
4
|
+
**Branch:** `cursor/durable-background-jobs-803d`
|
|
5
|
+
**Owner:** —
|
|
6
|
+
**Created:** 2026-09-10
|
|
7
|
+
**Approved:** 2026-09-10
|
|
8
|
+
**Roadmap issue:** https://github.com/FlourishHealth/terreno/issues/1188 (this feature **closes** that issue; implementation PRs use `Fixes #1188`)
|
|
9
|
+
**Task list:** [job-queues.md](../tasks/job-queues.md)
|
|
10
|
+
**Depends on:** —
|
|
11
|
+
**RTK deprecation flag:** Partial — admin screens use generated SDK / `useAdminApi`
|
|
12
|
+
**Program:** [B2B platform](b2b-platform-program.md)
|
|
13
|
+
|
|
14
|
+
## Goal
|
|
15
|
+
|
|
16
|
+
Terreno apps can enqueue named jobs that survive process restarts: retries with
|
|
17
|
+
backoff, delayed and recurring schedules, dead-lettering, and an admin operations
|
|
18
|
+
UI. Persistence and the default worker are **Mongo**. Execution is a pluggable
|
|
19
|
+
`JobRunner` so the same job definitions run in-process, in a standalone worker,
|
|
20
|
+
on **GCP Cloud Tasks**, on **Vercel Queues**, or on a custom runner. Comms,
|
|
21
|
+
webhooks, and billing stay on their current paths; this IP ships the queue they
|
|
22
|
+
will enqueue onto later.
|
|
23
|
+
|
|
24
|
+
## Non-Goals
|
|
25
|
+
|
|
26
|
+
- BullMQ, Redis, or any Redis-backed queue.
|
|
27
|
+
- Vercel Workflows (`"use workflow"` / `"use step"`).
|
|
28
|
+
- Migrating `@terreno/comms` sends, Expo receipt polling, `WebhooksApp`
|
|
29
|
+
200-then-queue dispatch, or `@terreno/billing` sync onto the queue.
|
|
30
|
+
- Replacing or merging `BackgroundTask` / admin **scripts**.
|
|
31
|
+
- Changing in-process `cronjob()` behavior (hard-coded `America/Chicago`).
|
|
32
|
+
- Live GCP or Vercel integration tests in CI.
|
|
33
|
+
- Generic `modelRouter` CRUD as the operator UI.
|
|
34
|
+
|
|
35
|
+
## Decisions
|
|
36
|
+
|
|
37
|
+
| Question | Decision |
|
|
38
|
+
|----------|----------|
|
|
39
|
+
| Package | New workspace package **`@terreno/jobs`**. `JobsApp` implements `TerrenoPlugin`. |
|
|
40
|
+
| Persistence | Mongo collections owned by this package. Default runner claims work from Mongo. |
|
|
41
|
+
| `BackgroundTask` | Unchanged. Scripts stay in-process bookkeeping. Jobs are a separate `Job` model. |
|
|
42
|
+
| v1 consumers | Framework + example only. Public `enqueue` is the seam later IPs call. |
|
|
43
|
+
| Workers | **Both:** `jobs.startWorker()` in the API process **or** a dedicated worker process. Explicit opt-in; registering `JobsApp` does not start a poll loop. |
|
|
44
|
+
| Schedules | Delayed `runAt` and recurring cron. Timezone is an **IANA** string on each schedule (default **`UTC`**). |
|
|
45
|
+
| Cloud dispatch | **GCP Cloud Tasks** and **Vercel Queues** adapters (mocked-SDK tests). Custom `JobRunner` is a first-class type. |
|
|
46
|
+
| Vercel product | **Queues** only. Job catalog and admin stay in Terreno. |
|
|
47
|
+
| Execute HTTP | `POST {basePath}/execute` for GCP/custom HTTP push (GCP: OIDC). Vercel uses private `handleCallback` consumers → `executeJobById`. Mongo runner does **not** mount execute unless a cloud/custom runner needs it. |
|
|
48
|
+
| Admin UI | Custom screen `jobs` (comms pattern): list/filter, detail/attempts, retry, DLQ requeue, cancel. Home widget. example-frontend + admin-spa. |
|
|
49
|
+
| Existing `cronjob()` | Leave it. Docs call it process-local. Durable work uses `JobsApp`. |
|
|
50
|
+
| Delivery | At-least-once. Handlers must be idempotent. Optional `idempotencyKey` unique with `name`. |
|
|
51
|
+
| Payload | JSON-serializable only. Admin redacts by default (`redactPayload`). |
|
|
52
|
+
| Recurring overlap | One in-flight instance per schedule `name`; skip the tick if still running. |
|
|
53
|
+
|
|
54
|
+
## Architecture
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
@terreno/jobs
|
|
58
|
+
JobsApp (TerrenoPlugin)
|
|
59
|
+
define(name, {handler, retry, schedule?})
|
|
60
|
+
enqueue({name, payload, runAt?, idempotencyKey?})
|
|
61
|
+
startWorker() // Mongo poll / runner.start
|
|
62
|
+
adminContribution() → screen "jobs"
|
|
63
|
+
optional POST /jobs/execute // cloud/custom HTTP
|
|
64
|
+
|
|
65
|
+
JobRunner
|
|
66
|
+
enqueue(job) → dispatch
|
|
67
|
+
start?(signal) → local poll (Mongo)
|
|
68
|
+
stop?()
|
|
69
|
+
|
|
70
|
+
MongoJobRunner — claim via findOneAndUpdate; lock TTL
|
|
71
|
+
GcpCloudTasksRunner — create HTTP task → POST /jobs/execute (OIDC)
|
|
72
|
+
VercelQueuesRunner — send(topic,{jobId}) → private handleCallback consumer → executeJobById
|
|
73
|
+
Custom — app-supplied JobRunner
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import {JobsApp, MongoJobRunner, getJobsService} from "@terreno/jobs";
|
|
78
|
+
|
|
79
|
+
const jobs = new JobsApp({
|
|
80
|
+
runner: new MongoJobRunner(),
|
|
81
|
+
timezone: "UTC",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
jobs.define("send-welcome-email", {
|
|
85
|
+
handler: async (payload, ctx) => {
|
|
86
|
+
ctx.log.info("sending");
|
|
87
|
+
// work
|
|
88
|
+
},
|
|
89
|
+
retry: {maxAttempts: 5, backoffMs: 1000},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
new TerrenoApp({userModel: User}).register(jobs);
|
|
93
|
+
|
|
94
|
+
// Explicit — API process or a worker entrypoint
|
|
95
|
+
await jobs.startWorker();
|
|
96
|
+
|
|
97
|
+
await getJobsService().enqueue({
|
|
98
|
+
name: "send-welcome-email",
|
|
99
|
+
payload: {userId: "..."},
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Standalone worker (example-backend): same `JobsApp` definitions, `build()` without
|
|
104
|
+
`listen`, then `startWorker()`. Do not start two poll loops against the same
|
|
105
|
+
queue unless lock TTL and claim are in place (they are).
|
|
106
|
+
|
|
107
|
+
### Claim (Mongo)
|
|
108
|
+
|
|
109
|
+
`findOneAndUpdate` where `status` is `pending` or `scheduled`, `runAt <= now`,
|
|
110
|
+
and `lockedAt` is missing or older than `lockTtlMs` (default 15 minutes). Set
|
|
111
|
+
`status: "running"`, `lockedAt`, `lockedBy` (hostname + pid). On success, wrap
|
|
112
|
+
`runWithRequestContext({jobId})`. On handler success → `completed`. On throw →
|
|
113
|
+
increment `attemptCount`, set `lastError`, either `pending` with `runAt` backoff
|
|
114
|
+
or `dead` when `attemptCount >= maxAttempts`.
|
|
115
|
+
|
|
116
|
+
### Retry
|
|
117
|
+
|
|
118
|
+
Exponential backoff from `retry.backoffMs` (default 1000) with jitter, cap
|
|
119
|
+
`retry.maxBackoffMs` (default 15 minutes). `maxAttempts` default 5. Dead-letter
|
|
120
|
+
status is **`dead`**. Admin **Retry** enqueues a new `Job` (`retriedFromId`);
|
|
121
|
+
**Requeue DLQ** resets the same row to `pending` with `runAt: now` and clears
|
|
122
|
+
the lock.
|
|
123
|
+
|
|
124
|
+
### Recurring
|
|
125
|
+
|
|
126
|
+
`define(name, {schedule: {cron, timezone?}})` upserts a `JobSchedule` row.
|
|
127
|
+
The worker ticks due schedules and `enqueue`s a child `Job` with
|
|
128
|
+
`scheduleId`. Timezone defaults to `JobsApp` `timezone` then `UTC`. Invalid cron
|
|
129
|
+
fails `define` / startup, not silently.
|
|
130
|
+
|
|
131
|
+
### `JobRunner`
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
interface JobRunner {
|
|
135
|
+
readonly id: string;
|
|
136
|
+
enqueue(job: JobDocument): Promise<void>;
|
|
137
|
+
start?(options: {jobs: JobsApp; signal: AbortSignal}): Promise<void>;
|
|
138
|
+
stop?(): Promise<void>;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Mongo `enqueue` only persists (already saved) then no-ops dispatch — the poller
|
|
143
|
+
picks it up. Cloud runners persist then create a platform task whose HTTP body
|
|
144
|
+
is `{jobId}` (not the payload). Execute loads the Mongo row, claims if needed,
|
|
145
|
+
runs the handler.
|
|
146
|
+
|
|
147
|
+
GCP and Vercel SDKs are **optional peers** on subpath exports
|
|
148
|
+
(`@terreno/jobs/runners/gcpCloudTasks`, `@terreno/jobs/runners/vercelQueues`).
|
|
149
|
+
Core `@terreno/jobs` has zero cloud SDKs. Tests inject fakes.
|
|
150
|
+
|
|
151
|
+
### Execute HTTP
|
|
152
|
+
|
|
153
|
+
Mounted only when `JobsApp` `mountExecuteRoute: true` or the runner requests it
|
|
154
|
+
(GCP default true; Vercel uses private `handleCallback` consumers; Mongo default false).
|
|
155
|
+
|
|
156
|
+
| Status | When |
|
|
157
|
+
|--------|------|
|
|
158
|
+
| 200 | Handler finished (success or recorded failure/dead) |
|
|
159
|
+
| 401 | Missing/invalid OIDC or platform signature |
|
|
160
|
+
| 404 | Unknown `jobId` |
|
|
161
|
+
| 409 | Job already `completed` / `cancelled` (idempotent no-op 200 preferred for provider retries — **200** if terminal success; **409** only if still `running` under a live lock) |
|
|
162
|
+
|
|
163
|
+
Do not skip rate limiting on this path. Do not add it to generated SDK consumer
|
|
164
|
+
hooks (admin uses dedicated jobs routes). OpenAPI: document execute as
|
|
165
|
+
internal/operator; omit from public SDK codegen if tagged internal.
|
|
166
|
+
|
|
167
|
+
### Logging
|
|
168
|
+
|
|
169
|
+
`createScopedLogger({prefix: "[Job]", labels: {jobName, jobId}})`. Never log
|
|
170
|
+
full payloads or secrets. Correlation: `runWithRequestContext({jobId})`.
|
|
171
|
+
|
|
172
|
+
## Models
|
|
173
|
+
|
|
174
|
+
Five-type pattern, field `description`s, `createdUpdatedPlugin`. **No**
|
|
175
|
+
`isDeletedPlugin` on live queue rows (use `cancelled`). Not `modelRouter`
|
|
176
|
+
models.
|
|
177
|
+
|
|
178
|
+
**Job** — `name` (string, indexed), `payload` (Mixed), `status` (`pending` \|
|
|
179
|
+
`scheduled` \| `running` \| `completed` \| `failed` \| `cancelled` \| `dead`),
|
|
180
|
+
`runAt` (date, indexed), `attemptCount` (number), `maxAttempts` (number),
|
|
181
|
+
`lockedAt` / `lockedBy`, `lastError`, `attempts[]` (`{at, error, errorClass}`),
|
|
182
|
+
`scheduleId` (optional ref), `idempotencyKey` (optional), `retriedFromId` /
|
|
183
|
+
`retriedById`, `payloadRedacted` (boolean). Unique sparse index
|
|
184
|
+
`(name, idempotencyKey)`. Compound index `(status, runAt)`.
|
|
185
|
+
|
|
186
|
+
**JobSchedule** — `name` (unique), `cron`, `timezone`, `nextRunAt`, `enabled`,
|
|
187
|
+
`handlerName` (= `name`).
|
|
188
|
+
|
|
189
|
+
## APIs
|
|
190
|
+
|
|
191
|
+
Default `basePath` `/jobs`. Admin routes require `admin:access` plus
|
|
192
|
+
`admin:jobs` when `accessControl` is configured; legacy `user.admin` when not.
|
|
193
|
+
|
|
194
|
+
| Method | Path | Purpose |
|
|
195
|
+
|--------|------|---------|
|
|
196
|
+
| GET | `/jobs` | Paginated list: `name`, `status`, `scheduleId`, date range, `q` |
|
|
197
|
+
| GET | `/jobs/:id` | Detail: attempts, redacted payload, lock |
|
|
198
|
+
| POST | `/jobs/:id/retry` | New job from payload; original `retriedById` |
|
|
199
|
+
| POST | `/jobs/:id/requeue` | DLQ/`failed`/`cancelled` → `pending` now |
|
|
200
|
+
| POST | `/jobs/:id/cancel` | `pending`/`scheduled`/`running` → `cancelled` (handler `ctx.signal`) |
|
|
201
|
+
| GET | `/jobs/schedules` | Defined schedules + next run |
|
|
202
|
+
| POST | `/jobs/schedules/:name/pause` | `enabled: false` |
|
|
203
|
+
| POST | `/jobs/schedules/:name/resume` | `enabled: true` |
|
|
204
|
+
| POST | `/jobs/execute` | Cloud/custom HTTP execute (not admin session) |
|
|
205
|
+
|
|
206
|
+
Enqueue from app code only (`getJobsService().enqueue`). No public
|
|
207
|
+
`POST /jobs` create for arbitrary names from the browser.
|
|
208
|
+
|
|
209
|
+
## Notifications
|
|
210
|
+
|
|
211
|
+
None. Failures are `Job` rows + `logger`. Downstream comms can `enqueue` later.
|
|
212
|
+
|
|
213
|
+
## UI
|
|
214
|
+
|
|
215
|
+
`JobsApp.adminContribution()`: custom screen `name: "jobs"`, display **Jobs**,
|
|
216
|
+
icon `clock`. Built-in widget in `@terreno/admin-frontend` (`JOBS_ADMIN_WIDGETS`)
|
|
217
|
+
so hosts do not re-map (comms pattern).
|
|
218
|
+
|
|
219
|
+
| Piece | Role |
|
|
220
|
+
|-------|------|
|
|
221
|
+
| `JobsDashboardScreen` | Filters + table + pause/resume schedules |
|
|
222
|
+
| `JobsJobDetail` | Attempts, error, retry / requeue / cancel |
|
|
223
|
+
| Home widget `jobs` | Counts of `dead` + `running`; link to screen |
|
|
224
|
+
|
|
225
|
+
example-frontend: `/admin/jobs`, `/admin/jobs/[id]`.
|
|
226
|
+
admin-spa: `/jobs`, `/jobs/[id]`.
|
|
227
|
+
`verify-ui-changes` on the example admin.
|
|
228
|
+
|
|
229
|
+
RBAC: add `jobs` to `terrenoStatements.admin` (alongside `runScripts`).
|
|
230
|
+
|
|
231
|
+
## Phases
|
|
232
|
+
|
|
233
|
+
1. **Tracer:** package, `define`/`enqueue`, Mongo claim, `startWorker`, one example job.
|
|
234
|
+
2. **Reliability:** retries, DLQ, lock TTL, cancel signal, delayed `runAt`, cron schedules.
|
|
235
|
+
3. **Runners:** `JobRunner`, custom fake, GCP Cloud Tasks, Vercel Queues, execute HTTP.
|
|
236
|
+
4. **Admin:** routes + UI + example + spa.
|
|
237
|
+
5. **Docs + publish:** Diátaxis, changelog, CI, `publish-on-tag`, seed/positioning.
|
|
238
|
+
|
|
239
|
+
## Feature Flags & Migrations
|
|
240
|
+
|
|
241
|
+
New collections only. No user-data migration. Opt-in: apps that never register
|
|
242
|
+
`JobsApp` are unchanged.
|
|
243
|
+
|
|
244
|
+
## Activity Log & User Updates
|
|
245
|
+
|
|
246
|
+
Each attempt is on `Job.attempts`. No end-user notifications.
|
|
247
|
+
|
|
248
|
+
## Not Included / Future Work
|
|
249
|
+
|
|
250
|
+
- Comms automatic scheduled retries (`comms-abstraction` / admin dashboard).
|
|
251
|
+
- Webhook 200-then-queue (`inbound-webhooks`).
|
|
252
|
+
- Billing Stripe handler offload (`billing-stripe`).
|
|
253
|
+
- Vercel Workflows runner.
|
|
254
|
+
- Redis/BullMQ runner.
|
|
255
|
+
- Per-queue concurrency caps / rate limits beyond claim locks.
|
|
256
|
+
- Streaming logs over sockets (poll admin GET).
|
|
257
|
+
|
|
258
|
+
## Docs in this slice
|
|
259
|
+
|
|
260
|
+
| Page | Change |
|
|
261
|
+
|------|--------|
|
|
262
|
+
| `docs/how-to/background-jobs.md` | **New.** Define, enqueue, worker vs API, Mongo vs GCP vs Vercel, execute auth, idempotency. |
|
|
263
|
+
| `docs/reference/jobs.md` | **New.** Options, models, routes, runners, env. |
|
|
264
|
+
| `docs/how-to/README.md` | Link the how-to. |
|
|
265
|
+
| `docs/reference/api.md` | Contrast `cronjob()` / `wrapScript` as process-local. |
|
|
266
|
+
| `docs/reference/environment-variables.md` | Jobs / Cloud Tasks / Vercel / execute audience. |
|
|
267
|
+
| `docs/how-to/admin-import-prebuilt.md` | `JobsApp` screen `jobs`. |
|
|
268
|
+
| `docs/explanation/roadmap-seed-issues.md` | IP + task URLs; `IP=job-queues`. |
|
|
269
|
+
| `docs/explanation/positioning.md` | Celery/ActiveJob row → shipped via `@terreno/jobs` when Complete. |
|
|
270
|
+
| `changelog/unreleased/job-queues.md` | Added. |
|
|
271
|
+
| `.rulesync/rules/jobs/00-jobs.md` | Package rule; `bun run rules` / `skills:sync`. |
|
|
272
|
+
| `.rulesync/skills/terreno-backend-api/SKILL.md` | Jobs vs scripts vs cron. |
|
|
273
|
+
|
|
274
|
+
## Testing
|
|
275
|
+
|
|
276
|
+
Bun + `@terreno/test` Mongo. Do not mock `@terreno/api` or models. Cloud adapters:
|
|
277
|
+
inject fake Task/Queue clients. Execute route: signed vs unsigned. Claim: two
|
|
278
|
+
workers cannot run the same job. Restart: `running` with expired lock is
|
|
279
|
+
reclaimed. Recurring: timezone `America/New_York` vs `UTC` tick. Admin: 403
|
|
280
|
+
without `admin:jobs`. UI: `verify-ui-changes`.
|
|
281
|
+
|
|
282
|
+
## Files to Create / Modify
|
|
283
|
+
|
|
284
|
+
- `jobs/` — new package (models, service, runners, routes, tests)
|
|
285
|
+
- Root `package.json` workspace + `jobs:*` scripts
|
|
286
|
+
- `api/src/rbac/statements.ts` — `admin: ["access", "runScripts", "viewBackgroundTasks", "jobs"]`
|
|
287
|
+
- `admin-frontend` — jobs screens + widgets + tests
|
|
288
|
+
- `admin-spa` — routes
|
|
289
|
+
- `example-backend` — `JobsApp` + worker entry + demo job
|
|
290
|
+
- `example-frontend` — admin routes + SDK regen if jobs admin paths are OpenAPI
|
|
291
|
+
- `.github/workflows/jobs-ci.yml`, `publish-on-tag.yml`, `cd.yml` path filters
|
|
292
|
+
- Docs/rules/changelog as in the table
|
|
293
|
+
|
|
294
|
+
## Task List
|
|
295
|
+
|
|
296
|
+
See [docs/tasks/job-queues.md](../tasks/job-queues.md).
|
|
297
|
+
|
|
298
|
+
## Acceptance Criteria
|
|
299
|
+
|
|
300
|
+
- [x] `enqueue` then kill the Node process; a new `startWorker()` runs the job.
|
|
301
|
+
Verified by bun test that simulates crash by leaving `pending` and starting
|
|
302
|
+
a second worker.
|
|
303
|
+
- [x] Handler throw retries until `maxAttempts` then `status: "dead"`. Admin
|
|
304
|
+
requeue returns it to `pending` and the worker runs it. Verified by bun tests
|
|
305
|
+
+ admin UI verification.
|
|
306
|
+
- [x] Delayed `runAt` in the future is not claimed early; cron in a named IANA
|
|
307
|
+
timezone fires `nextRunAt` correctly. Verified by bun tests with frozen clocks
|
|
308
|
+
(Luxon).
|
|
309
|
+
- [x] Two overlapping `startWorker()` loops claim a job once. Verified by bun test.
|
|
310
|
+
- [x] Custom `JobRunner` receives `enqueue`; GCP and Vercel adapters create a
|
|
311
|
+
mocked platform task and `POST /jobs/execute` with valid auth runs the
|
|
312
|
+
handler, invalid auth 401. Verified by bun tests (no live cloud).
|
|
313
|
+
- [x] Admin Jobs screen lists jobs, opens detail, retries, requeues DLQ, cancels.
|
|
314
|
+
Verified by `verify-ui-changes` artifacts.
|
|
315
|
+
- [x] Apps without `JobsApp` behave as today (`cronjob`, scripts). Verified by
|
|
316
|
+
existing api/admin-backend tests remaining green.
|
|
317
|
+
- [x] How-to + jobs reference match the shipped API. Verified in Roast;
|
|
318
|
+
`bun run website:build` on the docs task.
|
|
319
|
+
|
|
320
|
+
## Named assumptions
|
|
321
|
+
|
|
322
|
+
1. Handlers are registered in every process that executes jobs (API and worker).
|
|
323
|
+
2. JSON payloads only; no Buffer/stream jobs in v1.
|
|
324
|
+
3. Unique `(name, idempotencyKey)` makes duplicate enqueue a no-op returning the
|
|
325
|
+
existing row.
|
|
326
|
+
4. `failed` is a transient row state between attempts if we surface it; terminal
|
|
327
|
+
exhaustion is `dead`. Pick may use `pending` between retries only — then omit
|
|
328
|
+
`failed` as terminal. Prefer **pending-with-future-`runAt`** between attempts
|
|
329
|
+
and **`dead`** at exhaustion so the admin filter is simple.
|
|
330
|
+
5. Positioning Celery row updates when the IP is **Complete**, not while Draft.
|