From 254c27a13b067fc7b368a5903779e8dcf595ffc8 Mon Sep 17 00:00:00 2001 From: mira-2026 Date: Fri, 7 Aug 2026 12:49:20 +0200 Subject: [PATCH 1/3] feat(rewrite): add Phase 3 task domain --- greenfield/bun.lock | 10 - .../greenfield-rewrite/progress.md | 29 +- .../docs/generated/packages-and-runtime.md | 2 - greenfield/docs/generated/procedures.md | 13 +- ...Security.createPrincipal.input.schema.json | 6 +- ...ecurity.createPrincipal.output.schema.json | 12 +- ...curity.disablePrincipal.output.schema.json | 12 +- ...Security.listPrincipals.output.schema.json | 12 +- ...rity.replaceCapabilities.input.schema.json | 6 +- ...ity.replaceCapabilities.output.schema.json | 12 +- .../schemas/events.stream.input.schema.json | 3 +- .../schemas/events.stream.output.schema.json | 56 + ...ecurityAudit.listEvents.output.schema.json | 12 +- .../schemas/tasks.addUpdate.input.schema.json | 30 + .../tasks.addUpdate.output.schema.json | 101 ++ .../schemas/tasks.assign.input.schema.json | 39 + .../schemas/tasks.assign.output.schema.json | 204 +++ .../schemas/tasks.create.input.schema.json | 173 ++ .../schemas/tasks.create.output.schema.json | 204 +++ .../schemas/tasks.delete.input.schema.json | 24 + .../schemas/tasks.delete.output.schema.json | 24 + .../tasks.deleteProgress.input.schema.json | 32 + .../tasks.deleteProgress.output.schema.json | 32 + .../schemas/tasks.get.input.schema.json | 18 + .../schemas/tasks.get.output.schema.json | 204 +++ .../schemas/tasks.list.input.schema.json | 129 ++ .../schemas/tasks.list.output.schema.json | 230 +++ .../tasks.listUpdates.input.schema.json | 46 + .../tasks.listUpdates.output.schema.json | 138 ++ .../schemas/tasks.move.input.schema.json | 34 + .../schemas/tasks.move.output.schema.json | 204 +++ .../schemas/tasks.update.input.schema.json | 193 +++ .../schemas/tasks.update.output.schema.json | 204 +++ .../tasks.updateProgress.input.schema.json | 44 + .../tasks.updateProgress.output.schema.json | 101 ++ .../migration.sql | 159 ++ .../snapshot.json | 1419 ++++++++++++++--- greenfield/package.json | 2 - .../scripts/documentation/artifacts.test.ts | 40 +- .../scripts/documentation/jsonSchema.test.ts | 47 +- .../scripts/documentation/jsonSchema.ts | 73 +- greenfield/src/app/dashboardServer.ts | 11 + greenfield/src/app/server.ts | 3 + greenfield/src/app/trpcHttpHandler.ts | 3 + .../src/browser/api/realtimeClient.test.ts | 107 ++ greenfield/src/browser/api/realtimeClient.ts | 124 ++ .../src/browser/api/realtimeContext.tsx | 28 + .../src/browser/api/realtimeContextValue.ts | 20 + .../src/browser/api/realtimeHub.test.ts | 149 ++ greenfield/src/browser/api/realtimeHub.ts | 163 ++ greenfield/src/browser/api/trpcClient.ts | 4 + greenfield/src/browser/application.test.tsx | 2 + greenfield/src/browser/application.tsx | 23 +- .../auth/AuthenticationBoundary.test.tsx | 2 + .../src/browser/auth/LoginRoute.test.tsx | 2 + .../src/browser/layout/DashboardShell.tsx | 6 +- greenfield/src/browser/lib/dashboardRoutes.ts | 12 + greenfield/src/browser/router.tsx | 5 + greenfield/src/browser/routes/tasks.lazy.tsx | 14 + .../security/AccountSecurityRoute.test.tsx | 2 + greenfield/src/browser/tasks/NewTaskModal.tsx | 24 + .../browser/tasks/TaskAutomationFields.tsx | 175 ++ .../src/browser/tasks/TaskBoard.test.ts | 37 + greenfield/src/browser/tasks/TaskBoard.tsx | 73 + .../src/browser/tasks/TaskBoardRoute.test.tsx | 340 ++++ .../src/browser/tasks/TaskBoardRoute.tsx | 180 +++ .../src/browser/tasks/TaskBoardToolbar.tsx | 86 + greenfield/src/browser/tasks/TaskCard.tsx | 117 ++ greenfield/src/browser/tasks/TaskColumn.tsx | 67 + .../src/browser/tasks/TaskDetailModal.tsx | 150 ++ .../src/browser/tasks/TaskEditorFields.tsx | 162 ++ .../src/browser/tasks/TaskEditorForm.tsx | 66 + .../browser/tasks/TaskMetadataControls.tsx | 81 + .../src/browser/tasks/TaskProgressForm.tsx | 105 ++ .../src/browser/tasks/TaskProgressSection.tsx | 207 +++ greenfield/src/browser/tasks/taskBoardDrop.ts | 29 + .../src/browser/tasks/taskEditorForm.test.ts | 71 + .../src/browser/tasks/taskEditorForm.ts | 166 ++ greenfield/src/browser/tasks/taskMutations.ts | 63 + .../src/browser/tasks/taskPresentation.ts | 76 + .../src/browser/tasks/taskQueries.test.ts | 52 + greenfield/src/browser/tasks/taskQueries.ts | 113 ++ .../browser/tasks/useTaskEditorController.ts | 60 + .../useTaskRealtimeInvalidation.test.tsx | 157 ++ .../tasks/useTaskRealtimeInvalidation.ts | 53 + greenfield/src/browser/test/realtime.ts | 13 + greenfield/src/browser/ui/ActionLink.tsx | 3 +- .../src/browser/ui/ExternalLink.test.tsx | 39 + greenfield/src/browser/ui/ExternalLink.tsx | 52 + greenfield/src/browser/ui/Form.tsx | 4 +- greenfield/src/browser/ui/Markdown.tsx | 29 + greenfield/src/browser/ui/NavigationLink.tsx | 3 +- greenfield/src/browser/ui/SearchInput.tsx | 61 + greenfield/src/browser/ui/Select.test.tsx | 45 + greenfield/src/browser/ui/Select.tsx | 108 ++ greenfield/src/browser/ui/Textarea.tsx | 42 + greenfield/src/contracts/contractRegistry.ts | 2 + greenfield/src/contracts/events.test.ts | 22 +- greenfield/src/contracts/events.ts | 40 +- greenfield/src/contracts/security.ts | 26 +- greenfield/src/contracts/taskModel.test.ts | 99 ++ greenfield/src/contracts/taskModel.ts | 267 ++++ greenfield/src/contracts/taskRealtime.ts | 40 + greenfield/src/contracts/tasks.test.ts | 157 ++ greenfield/src/contracts/tasks.ts | 519 ++++++ .../migrations/migrationGraph.test.ts | 17 + .../database/migrations/taskSchema.test.ts | 187 +++ .../schema/automationPrincipalCapabilities.ts | 2 +- .../server/database/schema/drizzleSchema.ts | 6 + .../database/schema/taskAutomationProfiles.ts | 55 + .../src/server/database/schema/taskEvents.ts | 58 + .../src/server/database/schema/taskLabels.ts | 28 + .../database/schema/taskNotificationOutbox.ts | 65 + .../src/server/database/schema/taskUpdates.ts | 53 + .../src/server/database/schema/tasks.ts | 76 + .../validation/taskAutomationProfiles.ts | 38 + .../server/database/validation/taskEvents.ts | 95 ++ .../server/database/validation/taskLabels.ts | 22 + .../validation/taskNotificationOutbox.ts | 48 + .../database/validation/taskRows.test.ts | 151 ++ .../server/database/validation/taskUpdates.ts | 116 ++ .../src/server/database/validation/tasks.ts | 88 + .../src/server/domains/monitoring/service.ts | 3 +- .../src/server/domains/realtime/retention.ts | 4 + .../requestAuthenticationSession.test.ts | 7 +- greenfield/src/server/domains/tasks/errors.ts | 26 + .../server/domains/tasks/mutationSupport.ts | 185 +++ .../server/domains/tasks/procedures.test.ts | 130 ++ .../src/server/domains/tasks/procedures.ts | 8 + .../src/server/domains/tasks/queries.ts | 85 + .../src/server/domains/tasks/repository.ts | 64 + .../server/domains/tasks/repositoryReader.ts | 227 +++ .../server/domains/tasks/repositoryRecords.ts | 30 + .../server/domains/tasks/repositoryTypes.ts | 114 ++ .../domains/tasks/repositoryUnitOfWork.ts | 248 +++ greenfield/src/server/domains/tasks/routes.ts | 145 ++ .../src/server/domains/tasks/service.test.ts | 410 +++++ .../src/server/domains/tasks/service.ts | 214 +++ .../src/server/domains/tasks/serviceEvents.ts | 91 ++ .../server/domains/tasks/serviceRecords.ts | 134 ++ .../src/server/domains/tasks/taskMutations.ts | 383 +++++ .../domains/tasks/taskNotification.test.ts | 119 ++ .../server/domains/tasks/taskNotification.ts | 100 ++ .../tasks/taskNotificationQueue.test.ts | 189 +++ .../domains/tasks/taskNotificationQueue.ts | 163 ++ .../domains/tasks/taskProgressMutations.ts | 225 +++ .../domains/tasks/testSupport/service.ts | 31 + .../domains/tasks/testSupport/taskService.ts | 73 + .../platform/runtime/applicationRuntime.ts | 9 + .../src/server/test/support/requestContext.ts | 7 + greenfield/src/server/trpc/appRouter.ts | 3 + greenfield/src/server/trpc/context.test.ts | 4 + greenfield/src/server/trpc/context.ts | 4 + .../src/server/trpc/procedureErrorPolicy.ts | 53 + .../src/shared/databaseMigrationManifest.ts | 4 +- greenfield/src/shared/taskNotifications.ts | 82 + greenfield/src/shared/validation.test.ts | 11 + greenfield/src/shared/validation.ts | 37 + .../test/parity/fixtures/frontend-routes.json | 4 +- .../parity/fixtures/greenfield-contracts.json | 44 + .../parity/fixtures/legacy-endpoints.json | 22 +- .../src/test/parity/parityInventory.test.ts | 25 +- .../src/test/parity/parityInventorySchemas.ts | 2 +- .../test/parity/reviewedParityInventory.ts | 33 +- .../src/worker/taskNotifications.test.ts | 206 +++ greenfield/src/worker/taskNotifications.ts | 211 +++ 166 files changed, 14205 insertions(+), 363 deletions(-) create mode 100644 greenfield/docs/generated/schemas/tasks.addUpdate.input.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.addUpdate.output.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.assign.input.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.assign.output.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.create.input.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.create.output.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.delete.input.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.delete.output.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.deleteProgress.input.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.deleteProgress.output.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.get.input.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.get.output.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.list.input.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.list.output.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.listUpdates.input.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.listUpdates.output.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.move.input.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.move.output.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.update.input.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.update.output.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.updateProgress.input.schema.json create mode 100644 greenfield/docs/generated/schemas/tasks.updateProgress.output.schema.json create mode 100644 greenfield/src/browser/api/realtimeClient.test.ts create mode 100644 greenfield/src/browser/api/realtimeClient.ts create mode 100644 greenfield/src/browser/api/realtimeContext.tsx create mode 100644 greenfield/src/browser/api/realtimeContextValue.ts create mode 100644 greenfield/src/browser/api/realtimeHub.test.ts create mode 100644 greenfield/src/browser/api/realtimeHub.ts create mode 100644 greenfield/src/browser/lib/dashboardRoutes.ts create mode 100644 greenfield/src/browser/routes/tasks.lazy.tsx create mode 100644 greenfield/src/browser/tasks/NewTaskModal.tsx create mode 100644 greenfield/src/browser/tasks/TaskAutomationFields.tsx create mode 100644 greenfield/src/browser/tasks/TaskBoard.test.ts create mode 100644 greenfield/src/browser/tasks/TaskBoard.tsx create mode 100644 greenfield/src/browser/tasks/TaskBoardRoute.test.tsx create mode 100644 greenfield/src/browser/tasks/TaskBoardRoute.tsx create mode 100644 greenfield/src/browser/tasks/TaskBoardToolbar.tsx create mode 100644 greenfield/src/browser/tasks/TaskCard.tsx create mode 100644 greenfield/src/browser/tasks/TaskColumn.tsx create mode 100644 greenfield/src/browser/tasks/TaskDetailModal.tsx create mode 100644 greenfield/src/browser/tasks/TaskEditorFields.tsx create mode 100644 greenfield/src/browser/tasks/TaskEditorForm.tsx create mode 100644 greenfield/src/browser/tasks/TaskMetadataControls.tsx create mode 100644 greenfield/src/browser/tasks/TaskProgressForm.tsx create mode 100644 greenfield/src/browser/tasks/TaskProgressSection.tsx create mode 100644 greenfield/src/browser/tasks/taskBoardDrop.ts create mode 100644 greenfield/src/browser/tasks/taskEditorForm.test.ts create mode 100644 greenfield/src/browser/tasks/taskEditorForm.ts create mode 100644 greenfield/src/browser/tasks/taskMutations.ts create mode 100644 greenfield/src/browser/tasks/taskPresentation.ts create mode 100644 greenfield/src/browser/tasks/taskQueries.test.ts create mode 100644 greenfield/src/browser/tasks/taskQueries.ts create mode 100644 greenfield/src/browser/tasks/useTaskEditorController.ts create mode 100644 greenfield/src/browser/tasks/useTaskRealtimeInvalidation.test.tsx create mode 100644 greenfield/src/browser/tasks/useTaskRealtimeInvalidation.ts create mode 100644 greenfield/src/browser/test/realtime.ts create mode 100644 greenfield/src/browser/ui/ExternalLink.test.tsx create mode 100644 greenfield/src/browser/ui/ExternalLink.tsx create mode 100644 greenfield/src/browser/ui/Markdown.tsx create mode 100644 greenfield/src/browser/ui/SearchInput.tsx create mode 100644 greenfield/src/browser/ui/Select.test.tsx create mode 100644 greenfield/src/browser/ui/Select.tsx create mode 100644 greenfield/src/browser/ui/Textarea.tsx create mode 100644 greenfield/src/contracts/taskModel.test.ts create mode 100644 greenfield/src/contracts/taskModel.ts create mode 100644 greenfield/src/contracts/taskRealtime.ts create mode 100644 greenfield/src/contracts/tasks.test.ts create mode 100644 greenfield/src/contracts/tasks.ts create mode 100644 greenfield/src/server/database/migrations/taskSchema.test.ts create mode 100644 greenfield/src/server/database/schema/taskAutomationProfiles.ts create mode 100644 greenfield/src/server/database/schema/taskEvents.ts create mode 100644 greenfield/src/server/database/schema/taskLabels.ts create mode 100644 greenfield/src/server/database/schema/taskNotificationOutbox.ts create mode 100644 greenfield/src/server/database/schema/taskUpdates.ts create mode 100644 greenfield/src/server/database/schema/tasks.ts create mode 100644 greenfield/src/server/database/validation/taskAutomationProfiles.ts create mode 100644 greenfield/src/server/database/validation/taskEvents.ts create mode 100644 greenfield/src/server/database/validation/taskLabels.ts create mode 100644 greenfield/src/server/database/validation/taskNotificationOutbox.ts create mode 100644 greenfield/src/server/database/validation/taskRows.test.ts create mode 100644 greenfield/src/server/database/validation/taskUpdates.ts create mode 100644 greenfield/src/server/database/validation/tasks.ts create mode 100644 greenfield/src/server/domains/realtime/retention.ts create mode 100644 greenfield/src/server/domains/tasks/errors.ts create mode 100644 greenfield/src/server/domains/tasks/mutationSupport.ts create mode 100644 greenfield/src/server/domains/tasks/procedures.test.ts create mode 100644 greenfield/src/server/domains/tasks/procedures.ts create mode 100644 greenfield/src/server/domains/tasks/queries.ts create mode 100644 greenfield/src/server/domains/tasks/repository.ts create mode 100644 greenfield/src/server/domains/tasks/repositoryReader.ts create mode 100644 greenfield/src/server/domains/tasks/repositoryRecords.ts create mode 100644 greenfield/src/server/domains/tasks/repositoryTypes.ts create mode 100644 greenfield/src/server/domains/tasks/repositoryUnitOfWork.ts create mode 100644 greenfield/src/server/domains/tasks/routes.ts create mode 100644 greenfield/src/server/domains/tasks/service.test.ts create mode 100644 greenfield/src/server/domains/tasks/service.ts create mode 100644 greenfield/src/server/domains/tasks/serviceEvents.ts create mode 100644 greenfield/src/server/domains/tasks/serviceRecords.ts create mode 100644 greenfield/src/server/domains/tasks/taskMutations.ts create mode 100644 greenfield/src/server/domains/tasks/taskNotification.test.ts create mode 100644 greenfield/src/server/domains/tasks/taskNotification.ts create mode 100644 greenfield/src/server/domains/tasks/taskNotificationQueue.test.ts create mode 100644 greenfield/src/server/domains/tasks/taskNotificationQueue.ts create mode 100644 greenfield/src/server/domains/tasks/taskProgressMutations.ts create mode 100644 greenfield/src/server/domains/tasks/testSupport/service.ts create mode 100644 greenfield/src/server/domains/tasks/testSupport/taskService.ts create mode 100644 greenfield/src/shared/taskNotifications.ts create mode 100644 greenfield/src/worker/taskNotifications.test.ts create mode 100644 greenfield/src/worker/taskNotifications.ts diff --git a/greenfield/bun.lock b/greenfield/bun.lock index 9284e602a..bb9eef1f7 100644 --- a/greenfield/bun.lock +++ b/greenfield/bun.lock @@ -6,9 +6,7 @@ "name": "mira-dashboard", "dependencies": { "@daypicker/react": "10.0.1", - "@dnd-kit/core": "^6.3.1", "@dnd-kit/react": "^0.5.0", - "@dnd-kit/sortable": "^10.0.0", "@headlessui/react": "^2.2.10", "@microlink/react-json-view": "^1.31.28", "@simplewebauthn/browser": "13.3.0", @@ -124,24 +122,16 @@ "@dnd-kit/abstract": ["@dnd-kit/abstract@0.5.0", "", { "dependencies": { "@dnd-kit/geometry": "^0.5.0", "@dnd-kit/state": "^0.5.0", "tslib": "^2.6.2" } }, "sha512-hi13iMJgjPX/KDYVKg5VeDIhmYiV6buc9bAX+tCLYf4QdyYjPbsXjn2sPo6m7fQ6SGJBEFgHJ2PemeKDUbwBaA=="], - "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], - "@dnd-kit/collision": ["@dnd-kit/collision@0.5.0", "", { "dependencies": { "@dnd-kit/abstract": "^0.5.0", "@dnd-kit/geometry": "^0.5.0", "tslib": "^2.6.2" } }, "sha512-xUqRn3lS7oqLkT0AnnHS/STh/Czvwe1UapZFYiLbsUGxopMsQd4teaPCzPouOThoMdGEe+dHWjfqJl6t9iG4mQ=="], - "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="], - "@dnd-kit/dom": ["@dnd-kit/dom@0.5.0", "", { "dependencies": { "@dnd-kit/abstract": "^0.5.0", "@dnd-kit/collision": "^0.5.0", "@dnd-kit/geometry": "^0.5.0", "@dnd-kit/state": "^0.5.0", "tslib": "^2.6.2" } }, "sha512-f2xFJp5SYQ8EW/Fbtaa8iBb66hpkWc7qa8vU826KW11/tb44sH+AisZnGtwOOTWTQ0GraqBDr5ixTErww+eKXw=="], "@dnd-kit/geometry": ["@dnd-kit/geometry@0.5.0", "", { "dependencies": { "@dnd-kit/state": "^0.5.0", "tslib": "^2.6.2" } }, "sha512-ubHQS1CiSDH8ssYH2xG5BnpwPSFP1tStXXjug7/Ba6qnQdu/EUH47l6QXKIksQnnanfVfDf0aGeevRxgZlj28A=="], "@dnd-kit/react": ["@dnd-kit/react@0.5.0", "", { "dependencies": { "@dnd-kit/abstract": "^0.5.0", "@dnd-kit/dom": "^0.5.0", "@dnd-kit/state": "^0.5.0", "tslib": "^2.6.2" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-abQPLI8lmfVE+v/n+pqy5WFxrw6T2Yg0UQZsL78dp5DKci7dKTVDjvLWqvass+XTFtzJmsZEjk1NdqE6xG8Jiw=="], - "@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="], - "@dnd-kit/state": ["@dnd-kit/state@0.5.0", "", { "dependencies": { "@preact/signals-core": "^1.10.0", "tslib": "^2.6.2" } }, "sha512-y7XbabQqjF58Lk8YmDQuR8l6QjN+Kh4qlGEjUvHuIeasLk1QP+9L5diXS98VMxQIivyMmUtX2//f+3N7qPJX4w=="], - "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], - "@drizzle-team/brocli": ["@drizzle-team/brocli@0.12.0", "", {}, "sha512-mlUE+rZ8CatQekLhnaiN91Iemdd+e2gFKooGlnRB3oPTL3VghLfX24dx7HrzMNeC1JrIB/0kpsfyty3f5HNfxQ=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], diff --git a/greenfield/docs/architecture/greenfield-rewrite/progress.md b/greenfield/docs/architecture/greenfield-rewrite/progress.md index 602545087..4f86c5adf 100644 --- a/greenfield/docs/architecture/greenfield-rewrite/progress.md +++ b/greenfield/docs/architecture/greenfield-rewrite/progress.md @@ -12,7 +12,7 @@ closes a phase; dated entries below provide the evidence, not a second status so | 0 — Evidence and qualification | Complete | All eight mandatory spikes pass on exact Bun revision `17d6843606d76620cb55d31424d7fb0aed51c367`: build, transport, cross-process SQLite/outbox, Drizzle/Bun SQLite, browser data, chat batching, shutdown, and capped resources. Source-derived parity and the OpenClaw source audit pass as additional evidence. | | 1 — Foundation | Complete | The self-contained future root builds immutable browser/web/worker artifacts, protects project-local production state, installs exact Bun and systemd artifacts, migrates a database copy, atomically promotes the release/database pair, serves readiness/browser assets, writes project-local logs, and proves crash-safe rollback and shutdown in a disposable lifecycle. | | 2 — Trust and transport | Complete for the stated server scope | Authentication, MFA, WebAuthn, automation credentials, audit, authenticated renewable SSE, one-shot native Gateway bootstrap verification, and the consolidated [threat model](../../security/greenfield-phase-two-threat-model.md) have executable evidence. Browser UI and production cutover remain later gates. | -| 3 — Core operator domains | Started | Monitoring transaction/schema foundations exist; task, agent, report, incident, notification, schedule/job, cache/metrics procedures and browser parity are not complete. | +| 3 — Core operator domains | Started | The task domain and `/tasks` parity slice are implemented with durable history, realtime invalidation, and browser workflows. Agent, report, incident, notification, schedule/job, cache/metrics, overview, and worker-domain parity remain open. | | 4 — Gateway and chat | Not started | The Phase 2 verifier is one-shot only. Persistent native Gateway lifecycle, current-protocol re-audit, sessions, chat journal/recovery, attachments, and frontend remain open. | | 5 — Privileged and external domains | Not started | Worker-owned file/media, Docker, database, OpenClaw, GitHub, deployment, backup, and other privileged adapters remain open. | | 6 — Parity, hardening, and cutover | Not started | Full UI parity, generated `/docs`, load/resource/restore evidence, cutover rehearsal, fresh production database, and legacy removal remain open. | @@ -782,3 +782,30 @@ closes a phase; dated entries below provide the evidence, not a second status so This completes Phase 1 only. Phase 3–6 domains, persistent Gateway/chat, privileged adapters, full-browser parity, production rehearsal, cutover, and legacy deletion remain open. + +### 2026-08-07 — Phase 3 task domain and browser slice + +- A normalized task aggregate now owns tasks, canonical labels, optional automation profiles, + progress updates, and an append-only task-event history. Immediate admitted transactions keep + every aggregate mutation, audit record, and realtime outbox event atomic; version checks reject + stale edits and status movement without replaying a started callback. +- Eleven typed task procedures cover list/detail, create/update/delete, assign/move, and progress + add/update/delete/list. Effect services preserve typed domain failures, Valibot validates every + boundary and persisted record, capability policy separates task reads from writes, and task + mutations publish bounded realtime invalidations. +- Mira-relevant task events also create one redacted `task_notification_outbox` intent in the same + transaction. The queue preserves legacy create/update/assignment/movement/progress/deletion + semantics, suppresses `openclaw-task-tracking` self-notifications, hides task titles from other + automations, labels retained task fields as untrusted data, and uses the task-event ID as the + stable Gateway idempotency key. The Effect worker claims one delivery per lease, aborts a stalled + send before its lease can expire, and only acknowledges or releases work while it still owns a + live lease. Production sending remains gated on the Phase 4 persistent authenticated Gateway + client rather than reusing the one-shot bootstrap verifier. +- `/tasks` provides the reviewed four-column operator layout, server-side search and + assignee/recurring filters, accessible create/edit/detail/progress dialogs, and status movement + through `@dnd-kit/react`. Shared Headless UI controls, TanStack Form, TanStack Query, and the + existing reusable Dashboard presentation components own browser behavior rather than local + control implementations. +- The reviewed parity inventory now marks the 11 task operations and `/tasks` route implemented. + This closes only the task portion of Phase 3. Agent, report, incident, notification, job, + monitoring API, overview, cache/metrics, and real worker execution remain explicit gates. diff --git a/greenfield/docs/generated/packages-and-runtime.md b/greenfield/docs/generated/packages-and-runtime.md index 0c3172588..f87a9bce9 100644 --- a/greenfield/docs/generated/packages-and-runtime.md +++ b/greenfield/docs/generated/packages-and-runtime.md @@ -15,9 +15,7 @@ | Package | Declared | Resolved | Scope | | --- | --- | --- | --- | | `@daypicker/react` | `10.0.1` | `10.0.1` | runtime | -| `@dnd-kit/core` | `^6.3.1` | `6.3.1` | runtime | | `@dnd-kit/react` | `^0.5.0` | `0.5.0` | runtime | -| `@dnd-kit/sortable` | `^10.0.0` | `10.0.0` | runtime | | `@headlessui/react` | `^2.2.10` | `2.2.10` | runtime | | `@microlink/react-json-view` | `^1.31.28` | `1.31.28` | runtime | | `@simplewebauthn/browser` | `13.3.0` | `13.3.0` | runtime | diff --git a/greenfield/docs/generated/procedures.md b/greenfield/docs/generated/procedures.md index 1a7909978..dcf473a33 100644 --- a/greenfield/docs/generated/procedures.md +++ b/greenfield/docs/generated/procedures.md @@ -40,6 +40,17 @@ | `automationSecurity.replaceCapabilities` | mutation | automation-security | MFA enrollment required; recent MFA when enabled | [input](./schemas/automationSecurity.replaceCapabilities.input.schema.json) | [output](./schemas/automationSecurity.replaceCapabilities.output.schema.json) | `CONFLICT`, `FORBIDDEN`, `NOT_FOUND`, `SERVICE_UNAVAILABLE`, `UNAUTHORIZED` | `mfa_enrollment_required`, `step_up_required` | Atomically replaces a principal's least-privilege capability set. | | `automationSecurity.revokeCredential` | mutation | automation-security | MFA enrollment required; recent MFA when enabled | [input](./schemas/automationSecurity.revokeCredential.input.schema.json) | [output](./schemas/automationSecurity.revokeCredential.output.schema.json) | `CONFLICT`, `FORBIDDEN`, `NOT_FOUND`, `SERVICE_UNAVAILABLE`, `UNAUTHORIZED` | `mfa_enrollment_required`, `step_up_required` | Explicitly revokes one automation credential after client cutover. | | `automationSecurity.rotateCredential` | mutation | automation-security | MFA enrollment required; recent MFA when enabled | [input](./schemas/automationSecurity.rotateCredential.input.schema.json) | [output](./schemas/automationSecurity.rotateCredential.output.schema.json) | `CONFLICT`, `FORBIDDEN`, `NOT_FOUND`, `PRECONDITION_FAILED`, `SERVICE_UNAVAILABLE`, `UNAUTHORIZED` | `mfa_enrollment_required`, `step_up_required` | Stages a linked replacement credential without revoking its predecessor. | -| `events.stream` | subscription | events | Authenticated; per-topic: notifications:read, reports:read | [input](./schemas/events.stream.input.schema.json) | [output](./schemas/events.stream.output.schema.json) | `BAD_REQUEST`, `FORBIDDEN`, `SERVICE_UNAVAILABLE`, `TOO_MANY_REQUESTS`, `UNAUTHORIZED` | None | Streams authorized durable changes with tracked resume cursors. | +| `events.stream` | subscription | events | Authenticated; per-topic: notifications:read, reports:read, tasks:read | [input](./schemas/events.stream.input.schema.json) | [output](./schemas/events.stream.output.schema.json) | `BAD_REQUEST`, `FORBIDDEN`, `SERVICE_UNAVAILABLE`, `TOO_MANY_REQUESTS`, `UNAUTHORIZED` | None | Streams authorized durable changes with tracked resume cursors. | | `securityAudit.listEvents` | query | securityAudit | Authenticated browser session | [input](./schemas/securityAudit.listEvents.input.schema.json) | [output](./schemas/securityAudit.listEvents.output.schema.json) | `FORBIDDEN`, `UNAUTHORIZED` | None | Lists redacted immutable security events in stable newest-first order. | | `system.runtimeIdentity` | query | system | Public | [input](./schemas/system.runtimeIdentity.input.schema.json) | [output](./schemas/system.runtimeIdentity.output.schema.json) | None | None | Returns the Bun runtime identity of the serving process. | +| `tasks.addUpdate` | mutation | tasks | Authenticated: tasks:write | [input](./schemas/tasks.addUpdate.input.schema.json) | [output](./schemas/tasks.addUpdate.output.schema.json) | `CONFLICT`, `FORBIDDEN`, `NOT_FOUND`, `SERVICE_UNAVAILABLE`, `UNAUTHORIZED` | None | Appends one authenticated progress update to a task. | +| `tasks.assign` | mutation | tasks | Authenticated: tasks:write | [input](./schemas/tasks.assign.input.schema.json) | [output](./schemas/tasks.assign.output.schema.json) | `CONFLICT`, `FORBIDDEN`, `NOT_FOUND`, `SERVICE_UNAVAILABLE`, `UNAUTHORIZED` | None | Assigns or unassigns a task under optimistic concurrency control. | +| `tasks.create` | mutation | tasks | Authenticated: tasks:write | [input](./schemas/tasks.create.input.schema.json) | [output](./schemas/tasks.create.output.schema.json) | `CONFLICT`, `FORBIDDEN`, `SERVICE_UNAVAILABLE`, `UNAUTHORIZED` | None | Creates one task with server-owned identity and audit history. | +| `tasks.delete` | mutation | tasks | Authenticated: tasks:write | [input](./schemas/tasks.delete.input.schema.json) | [output](./schemas/tasks.delete.output.schema.json) | `CONFLICT`, `FORBIDDEN`, `NOT_FOUND`, `SERVICE_UNAVAILABLE`, `UNAUTHORIZED` | None | Deletes a version-matched task and mutable relationships while retaining append-only events. | +| `tasks.deleteProgress` | mutation | tasks | Authenticated: tasks:write | [input](./schemas/tasks.deleteProgress.input.schema.json) | [output](./schemas/tasks.deleteProgress.output.schema.json) | `CONFLICT`, `FORBIDDEN`, `NOT_FOUND`, `SERVICE_UNAVAILABLE`, `UNAUTHORIZED` | None | Deletes one version-matched task progress entry. | +| `tasks.get` | query | tasks | Authenticated: tasks:read | [input](./schemas/tasks.get.input.schema.json) | [output](./schemas/tasks.get.output.schema.json) | `FORBIDDEN`, `NOT_FOUND`, `UNAUTHORIZED` | None | Loads one complete task for detail editing. | +| `tasks.list` | query | tasks | Authenticated: tasks:read | [input](./schemas/tasks.list.input.schema.json) | [output](./schemas/tasks.list.output.schema.json) | `FORBIDDEN`, `UNAUTHORIZED` | None | Lists one stable filtered page of task-board rows. | +| `tasks.listUpdates` | query | tasks | Authenticated: tasks:read | [input](./schemas/tasks.listUpdates.input.schema.json) | [output](./schemas/tasks.listUpdates.output.schema.json) | `FORBIDDEN`, `NOT_FOUND`, `UNAUTHORIZED` | None | Lists one stable newest-first page of task progress entries. | +| `tasks.move` | mutation | tasks | Authenticated: tasks:write | [input](./schemas/tasks.move.input.schema.json) | [output](./schemas/tasks.move.output.schema.json) | `CONFLICT`, `FORBIDDEN`, `NOT_FOUND`, `SERVICE_UNAVAILABLE`, `UNAUTHORIZED` | None | Moves a task between board columns under optimistic concurrency control. | +| `tasks.update` | mutation | tasks | Authenticated: tasks:write | [input](./schemas/tasks.update.input.schema.json) | [output](./schemas/tasks.update.output.schema.json) | `CONFLICT`, `FORBIDDEN`, `NOT_FOUND`, `SERVICE_UNAVAILABLE`, `UNAUTHORIZED` | None | Updates task content under optimistic concurrency control. | +| `tasks.updateProgress` | mutation | tasks | Authenticated: tasks:write | [input](./schemas/tasks.updateProgress.input.schema.json) | [output](./schemas/tasks.updateProgress.output.schema.json) | `CONFLICT`, `FORBIDDEN`, `NOT_FOUND`, `SERVICE_UNAVAILABLE`, `UNAUTHORIZED` | None | Edits one task progress entry under optimistic concurrency control. | diff --git a/greenfield/docs/generated/schemas/automationSecurity.createPrincipal.input.schema.json b/greenfield/docs/generated/schemas/automationSecurity.createPrincipal.input.schema.json index 60d5ef3f8..18281d19f 100644 --- a/greenfield/docs/generated/schemas/automationSecurity.createPrincipal.input.schema.json +++ b/greenfield/docs/generated/schemas/automationSecurity.createPrincipal.input.schema.json @@ -7,11 +7,13 @@ "items": { "enum": [ "notifications:read", - "reports:read" + "reports:read", + "tasks:read", + "tasks:write" ], "type": "string" }, - "maxItems": 2, + "maxItems": 4, "uniqueItems": true }, "id": { diff --git a/greenfield/docs/generated/schemas/automationSecurity.createPrincipal.output.schema.json b/greenfield/docs/generated/schemas/automationSecurity.createPrincipal.output.schema.json index 1ce7d511d..ec1802fe0 100644 --- a/greenfield/docs/generated/schemas/automationSecurity.createPrincipal.output.schema.json +++ b/greenfield/docs/generated/schemas/automationSecurity.createPrincipal.output.schema.json @@ -76,11 +76,13 @@ "items": { "enum": [ "notifications:read", - "reports:read" + "reports:read", + "tasks:read", + "tasks:write" ], "type": "string" }, - "maxItems": 2, + "maxItems": 4, "uniqueItems": true }, "createdAtMs": { @@ -145,11 +147,13 @@ "items": { "enum": [ "notifications:read", - "reports:read" + "reports:read", + "tasks:read", + "tasks:write" ], "type": "string" }, - "maxItems": 2, + "maxItems": 4, "uniqueItems": true }, "createdAtMs": { diff --git a/greenfield/docs/generated/schemas/automationSecurity.disablePrincipal.output.schema.json b/greenfield/docs/generated/schemas/automationSecurity.disablePrincipal.output.schema.json index 93134f927..f109aefb8 100644 --- a/greenfield/docs/generated/schemas/automationSecurity.disablePrincipal.output.schema.json +++ b/greenfield/docs/generated/schemas/automationSecurity.disablePrincipal.output.schema.json @@ -25,11 +25,13 @@ "items": { "enum": [ "notifications:read", - "reports:read" + "reports:read", + "tasks:read", + "tasks:write" ], "type": "string" }, - "maxItems": 2, + "maxItems": 4, "uniqueItems": true }, "createdAtMs": { @@ -94,11 +96,13 @@ "items": { "enum": [ "notifications:read", - "reports:read" + "reports:read", + "tasks:read", + "tasks:write" ], "type": "string" }, - "maxItems": 2, + "maxItems": 4, "uniqueItems": true }, "createdAtMs": { diff --git a/greenfield/docs/generated/schemas/automationSecurity.listPrincipals.output.schema.json b/greenfield/docs/generated/schemas/automationSecurity.listPrincipals.output.schema.json index 53eee1003..08b454687 100644 --- a/greenfield/docs/generated/schemas/automationSecurity.listPrincipals.output.schema.json +++ b/greenfield/docs/generated/schemas/automationSecurity.listPrincipals.output.schema.json @@ -50,11 +50,13 @@ "items": { "enum": [ "notifications:read", - "reports:read" + "reports:read", + "tasks:read", + "tasks:write" ], "type": "string" }, - "maxItems": 2, + "maxItems": 4, "uniqueItems": true }, "createdAtMs": { @@ -119,11 +121,13 @@ "items": { "enum": [ "notifications:read", - "reports:read" + "reports:read", + "tasks:read", + "tasks:write" ], "type": "string" }, - "maxItems": 2, + "maxItems": 4, "uniqueItems": true }, "createdAtMs": { diff --git a/greenfield/docs/generated/schemas/automationSecurity.replaceCapabilities.input.schema.json b/greenfield/docs/generated/schemas/automationSecurity.replaceCapabilities.input.schema.json index e3c7d73b8..d9a694b00 100644 --- a/greenfield/docs/generated/schemas/automationSecurity.replaceCapabilities.input.schema.json +++ b/greenfield/docs/generated/schemas/automationSecurity.replaceCapabilities.input.schema.json @@ -18,11 +18,13 @@ "items": { "enum": [ "notifications:read", - "reports:read" + "reports:read", + "tasks:read", + "tasks:write" ], "type": "string" }, - "maxItems": 2, + "maxItems": 4, "uniqueItems": true } }, diff --git a/greenfield/docs/generated/schemas/automationSecurity.replaceCapabilities.output.schema.json b/greenfield/docs/generated/schemas/automationSecurity.replaceCapabilities.output.schema.json index 4dd40892c..aeb8308ed 100644 --- a/greenfield/docs/generated/schemas/automationSecurity.replaceCapabilities.output.schema.json +++ b/greenfield/docs/generated/schemas/automationSecurity.replaceCapabilities.output.schema.json @@ -25,11 +25,13 @@ "items": { "enum": [ "notifications:read", - "reports:read" + "reports:read", + "tasks:read", + "tasks:write" ], "type": "string" }, - "maxItems": 2, + "maxItems": 4, "uniqueItems": true }, "createdAtMs": { @@ -94,11 +96,13 @@ "items": { "enum": [ "notifications:read", - "reports:read" + "reports:read", + "tasks:read", + "tasks:write" ], "type": "string" }, - "maxItems": 2, + "maxItems": 4, "uniqueItems": true }, "createdAtMs": { diff --git a/greenfield/docs/generated/schemas/events.stream.input.schema.json b/greenfield/docs/generated/schemas/events.stream.input.schema.json index 7bcd06437..2cc44b3bf 100644 --- a/greenfield/docs/generated/schemas/events.stream.input.schema.json +++ b/greenfield/docs/generated/schemas/events.stream.input.schema.json @@ -14,7 +14,8 @@ "enum": [ "monitoring.incidents", "monitoring.notifications", - "monitoring.reports" + "monitoring.reports", + "tasks.records" ], "type": "string" }, diff --git a/greenfield/docs/generated/schemas/events.stream.output.schema.json b/greenfield/docs/generated/schemas/events.stream.output.schema.json index 01d8572e0..848e71d2e 100644 --- a/greenfield/docs/generated/schemas/events.stream.output.schema.json +++ b/greenfield/docs/generated/schemas/events.stream.output.schema.json @@ -157,6 +157,62 @@ "topic" ], "additionalProperties": false + }, + { + "type": "object", + "properties": { + "entityId": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "entityType": { + "const": "task" + }, + "occurredAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "operation": { + "enum": [ + "created", + "deleted", + "updated" + ], + "type": "string" + }, + "payload": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "topic": { + "const": "tasks.records" + } + }, + "required": [ + "entityId", + "entityType", + "occurredAtMs", + "operation", + "payload", + "topic" + ], + "additionalProperties": false } ] }, diff --git a/greenfield/docs/generated/schemas/securityAudit.listEvents.output.schema.json b/greenfield/docs/generated/schemas/securityAudit.listEvents.output.schema.json index bb2071f53..6fd0f79cb 100644 --- a/greenfield/docs/generated/schemas/securityAudit.listEvents.output.schema.json +++ b/greenfield/docs/generated/schemas/securityAudit.listEvents.output.schema.json @@ -134,11 +134,13 @@ "items": { "enum": [ "notifications:read", - "reports:read" + "reports:read", + "tasks:read", + "tasks:write" ], "type": "string" }, - "maxItems": 2, + "maxItems": 4, "uniqueItems": true }, "method": { @@ -182,11 +184,13 @@ "items": { "enum": [ "notifications:read", - "reports:read" + "reports:read", + "tasks:read", + "tasks:write" ], "type": "string" }, - "maxItems": 2, + "maxItems": 4, "uniqueItems": true }, "replacementCredentialId": { diff --git a/greenfield/docs/generated/schemas/tasks.addUpdate.input.schema.json b/greenfield/docs/generated/schemas/tasks.addUpdate.input.schema.json new file mode 100644 index 000000000..e51defbff --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.addUpdate.input.schema.json @@ -0,0 +1,30 @@ +{ + "$id": "urn:mira-dashboard:tasks.addUpdate.input", + "type": "object", + "properties": { + "messageMarkdown": { + "type": "string", + "maxLength": 20000, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + } + ] + }, + "taskId": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + } + }, + "required": [ + "messageMarkdown", + "taskId" + ], + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.addUpdate.output.schema.json b/greenfield/docs/generated/schemas/tasks.addUpdate.output.schema.json new file mode 100644 index 000000000..fee946a9a --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.addUpdate.output.schema.json @@ -0,0 +1,101 @@ +{ + "$id": "urn:mira-dashboard:tasks.addUpdate.output", + "type": "object", + "properties": { + "author": { + "oneOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9._-]*$" + }, + "kind": { + "const": "automation" + } + }, + "required": [ + "id", + "kind" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "kind": { + "const": "user" + } + }, + "required": [ + "id", + "kind" + ], + "additionalProperties": false + } + ] + }, + "createdAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "messageMarkdown": { + "type": "string", + "maxLength": 20000, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + } + ] + }, + "taskId": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "updatedAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "version": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "author", + "createdAtMs", + "id", + "messageMarkdown", + "taskId", + "updatedAtMs", + "version" + ], + "additionalProperties": false, + "$comment": "Live Valibot validation additionally requires progress update timestamps not to precede creation.", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.assign.input.schema.json b/greenfield/docs/generated/schemas/tasks.assign.input.schema.json new file mode 100644 index 000000000..df1b5e82b --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.assign.input.schema.json @@ -0,0 +1,39 @@ +{ + "$id": "urn:mira-dashboard:tasks.assign.input", + "type": "object", + "properties": { + "expectedVersion": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "assignee": { + "anyOf": [ + { + "enum": [ + "mira-2026", + "rajohan" + ], + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "expectedVersion", + "id", + "assignee" + ], + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.assign.output.schema.json b/greenfield/docs/generated/schemas/tasks.assign.output.schema.json new file mode 100644 index 000000000..b5b55cc82 --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.assign.output.schema.json @@ -0,0 +1,204 @@ +{ + "$id": "urn:mira-dashboard:tasks.assign.output", + "type": "object", + "properties": { + "assignee": { + "enum": [ + "mira-2026", + "rajohan" + ], + "type": "string" + }, + "automation": { + "type": "object", + "properties": { + "cronJobId": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "kind": { + "const": "openclaw-cron" + }, + "model": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "scheduleSummary": { + "type": "string", + "maxLength": 500, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "sessionTarget": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "thinking": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "recurring": { + "type": "boolean" + } + }, + "required": [ + "cronJobId", + "kind", + "recurring" + ], + "additionalProperties": false + }, + "createdAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "labels": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "maxItems": 20, + "uniqueItems": true, + "$comment": "Live Valibot validation additionally requires task strings to use canonical code-unit order." + }, + "priority": { + "enum": [ + "low", + "medium", + "high" + ], + "type": "string" + }, + "status": { + "enum": [ + "todo", + "in-progress", + "blocked", + "done" + ], + "type": "string" + }, + "title": { + "type": "string", + "maxLength": 240, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "updatedAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "version": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "bodyMarkdown": { + "type": "string", + "maxLength": 100000, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + } + ] + } + }, + "required": [ + "createdAtMs", + "id", + "labels", + "priority", + "status", + "title", + "updatedAtMs", + "version" + ], + "additionalProperties": false, + "$comment": "Live Valibot validation additionally requires task update timestamps not to precede creation.", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.create.input.schema.json b/greenfield/docs/generated/schemas/tasks.create.input.schema.json new file mode 100644 index 000000000..763181986 --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.create.input.schema.json @@ -0,0 +1,173 @@ +{ + "$id": "urn:mira-dashboard:tasks.create.input", + "type": "object", + "properties": { + "assignee": { + "enum": [ + "mira-2026", + "rajohan" + ], + "type": "string" + }, + "automation": { + "type": "object", + "properties": { + "cronJobId": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "kind": { + "const": "openclaw-cron" + }, + "model": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "scheduleSummary": { + "type": "string", + "maxLength": 500, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "sessionTarget": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "thinking": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "recurring": { + "type": "boolean", + "default": true + } + }, + "required": [ + "cronJobId", + "kind" + ], + "additionalProperties": false + }, + "bodyMarkdown": { + "type": "string", + "maxLength": 100000, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + } + ] + }, + "labels": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "maxItems": 20, + "uniqueItems": true + }, + "priority": { + "enum": [ + "low", + "medium", + "high" + ], + "type": "string" + }, + "status": { + "enum": [ + "todo", + "in-progress", + "blocked", + "done" + ], + "type": "string" + }, + "title": { + "type": "string", + "maxLength": 240, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + } + }, + "required": [ + "title" + ], + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.create.output.schema.json b/greenfield/docs/generated/schemas/tasks.create.output.schema.json new file mode 100644 index 000000000..9855eb9b5 --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.create.output.schema.json @@ -0,0 +1,204 @@ +{ + "$id": "urn:mira-dashboard:tasks.create.output", + "type": "object", + "properties": { + "assignee": { + "enum": [ + "mira-2026", + "rajohan" + ], + "type": "string" + }, + "automation": { + "type": "object", + "properties": { + "cronJobId": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "kind": { + "const": "openclaw-cron" + }, + "model": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "scheduleSummary": { + "type": "string", + "maxLength": 500, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "sessionTarget": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "thinking": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "recurring": { + "type": "boolean" + } + }, + "required": [ + "cronJobId", + "kind", + "recurring" + ], + "additionalProperties": false + }, + "createdAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "labels": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "maxItems": 20, + "uniqueItems": true, + "$comment": "Live Valibot validation additionally requires task strings to use canonical code-unit order." + }, + "priority": { + "enum": [ + "low", + "medium", + "high" + ], + "type": "string" + }, + "status": { + "enum": [ + "todo", + "in-progress", + "blocked", + "done" + ], + "type": "string" + }, + "title": { + "type": "string", + "maxLength": 240, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "updatedAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "version": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "bodyMarkdown": { + "type": "string", + "maxLength": 100000, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + } + ] + } + }, + "required": [ + "createdAtMs", + "id", + "labels", + "priority", + "status", + "title", + "updatedAtMs", + "version" + ], + "additionalProperties": false, + "$comment": "Live Valibot validation additionally requires task update timestamps not to precede creation.", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.delete.input.schema.json b/greenfield/docs/generated/schemas/tasks.delete.input.schema.json new file mode 100644 index 000000000..7bb83040b --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.delete.input.schema.json @@ -0,0 +1,24 @@ +{ + "$id": "urn:mira-dashboard:tasks.delete.input", + "type": "object", + "properties": { + "expectedVersion": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + } + }, + "required": [ + "expectedVersion", + "id" + ], + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.delete.output.schema.json b/greenfield/docs/generated/schemas/tasks.delete.output.schema.json new file mode 100644 index 000000000..d622684ba --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.delete.output.schema.json @@ -0,0 +1,24 @@ +{ + "$id": "urn:mira-dashboard:tasks.delete.output", + "type": "object", + "properties": { + "deletedAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + } + }, + "required": [ + "deletedAtMs", + "id" + ], + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.deleteProgress.input.schema.json b/greenfield/docs/generated/schemas/tasks.deleteProgress.input.schema.json new file mode 100644 index 000000000..27c557f03 --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.deleteProgress.input.schema.json @@ -0,0 +1,32 @@ +{ + "$id": "urn:mira-dashboard:tasks.deleteProgress.input", + "type": "object", + "properties": { + "expectedVersion": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "taskId": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "updateId": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + } + }, + "required": [ + "expectedVersion", + "taskId", + "updateId" + ], + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.deleteProgress.output.schema.json b/greenfield/docs/generated/schemas/tasks.deleteProgress.output.schema.json new file mode 100644 index 000000000..1b399e63a --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.deleteProgress.output.schema.json @@ -0,0 +1,32 @@ +{ + "$id": "urn:mira-dashboard:tasks.deleteProgress.output", + "type": "object", + "properties": { + "deletedAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "taskId": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "updateId": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + } + }, + "required": [ + "deletedAtMs", + "taskId", + "updateId" + ], + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.get.input.schema.json b/greenfield/docs/generated/schemas/tasks.get.input.schema.json new file mode 100644 index 000000000..6c4610f26 --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.get.input.schema.json @@ -0,0 +1,18 @@ +{ + "$id": "urn:mira-dashboard:tasks.get.input", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + } + }, + "required": [ + "id" + ], + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.get.output.schema.json b/greenfield/docs/generated/schemas/tasks.get.output.schema.json new file mode 100644 index 000000000..7fd381d61 --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.get.output.schema.json @@ -0,0 +1,204 @@ +{ + "$id": "urn:mira-dashboard:tasks.get.output", + "type": "object", + "properties": { + "assignee": { + "enum": [ + "mira-2026", + "rajohan" + ], + "type": "string" + }, + "automation": { + "type": "object", + "properties": { + "cronJobId": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "kind": { + "const": "openclaw-cron" + }, + "model": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "scheduleSummary": { + "type": "string", + "maxLength": 500, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "sessionTarget": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "thinking": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "recurring": { + "type": "boolean" + } + }, + "required": [ + "cronJobId", + "kind", + "recurring" + ], + "additionalProperties": false + }, + "createdAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "labels": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "maxItems": 20, + "uniqueItems": true, + "$comment": "Live Valibot validation additionally requires task strings to use canonical code-unit order." + }, + "priority": { + "enum": [ + "low", + "medium", + "high" + ], + "type": "string" + }, + "status": { + "enum": [ + "todo", + "in-progress", + "blocked", + "done" + ], + "type": "string" + }, + "title": { + "type": "string", + "maxLength": 240, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "updatedAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "version": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "bodyMarkdown": { + "type": "string", + "maxLength": 100000, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + } + ] + } + }, + "required": [ + "createdAtMs", + "id", + "labels", + "priority", + "status", + "title", + "updatedAtMs", + "version" + ], + "additionalProperties": false, + "$comment": "Live Valibot validation additionally requires task update timestamps not to precede creation.", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.list.input.schema.json b/greenfield/docs/generated/schemas/tasks.list.input.schema.json new file mode 100644 index 000000000..4fa819b6d --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.list.input.schema.json @@ -0,0 +1,129 @@ +{ + "$id": "urn:mira-dashboard:tasks.list.input", + "type": "object", + "properties": { + "cursor": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "updatedAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + } + }, + "required": [ + "id", + "updatedAtMs" + ], + "additionalProperties": false + }, + "filters": { + "type": "object", + "properties": { + "assignees": { + "type": "array", + "items": { + "enum": [ + "mira-2026", + "rajohan" + ], + "type": "string" + }, + "minItems": 1, + "maxItems": 2, + "uniqueItems": true + }, + "automation": { + "enum": [ + "recurring", + "manual" + ], + "type": "string" + }, + "labels": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "maxItems": 20, + "uniqueItems": true, + "minItems": 1 + }, + "priorities": { + "type": "array", + "items": { + "enum": [ + "low", + "medium", + "high" + ], + "type": "string" + }, + "minItems": 1, + "maxItems": 3, + "uniqueItems": true + }, + "search": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "statuses": { + "type": "array", + "items": { + "enum": [ + "todo", + "in-progress", + "blocked", + "done" + ], + "type": "string" + }, + "minItems": 1, + "maxItems": 4, + "uniqueItems": true + } + }, + "required": [], + "additionalProperties": false + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + } + }, + "required": [], + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.list.output.schema.json b/greenfield/docs/generated/schemas/tasks.list.output.schema.json new file mode 100644 index 000000000..26a3c560a --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.list.output.schema.json @@ -0,0 +1,230 @@ +{ + "$id": "urn:mira-dashboard:tasks.list.output", + "type": "object", + "properties": { + "nextCursor": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "updatedAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + } + }, + "required": [ + "id", + "updatedAtMs" + ], + "additionalProperties": false + }, + "tasks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "assignee": { + "enum": [ + "mira-2026", + "rajohan" + ], + "type": "string" + }, + "automation": { + "type": "object", + "properties": { + "cronJobId": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "kind": { + "const": "openclaw-cron" + }, + "model": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "scheduleSummary": { + "type": "string", + "maxLength": 500, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "sessionTarget": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "thinking": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "recurring": { + "type": "boolean" + } + }, + "required": [ + "cronJobId", + "kind", + "recurring" + ], + "additionalProperties": false + }, + "createdAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "labels": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "maxItems": 20, + "uniqueItems": true, + "$comment": "Live Valibot validation additionally requires task strings to use canonical code-unit order." + }, + "priority": { + "enum": [ + "low", + "medium", + "high" + ], + "type": "string" + }, + "status": { + "enum": [ + "todo", + "in-progress", + "blocked", + "done" + ], + "type": "string" + }, + "title": { + "type": "string", + "maxLength": 240, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "updatedAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "version": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "createdAtMs", + "id", + "labels", + "priority", + "status", + "title", + "updatedAtMs", + "version" + ], + "additionalProperties": false, + "$comment": "Live Valibot validation additionally requires task update timestamps not to precede creation." + }, + "maxItems": 100, + "$comment": "Live Valibot validation additionally requires strict newest-first task ordering by update timestamp and ID." + } + }, + "required": [ + "tasks" + ], + "additionalProperties": false, + "$comment": "Live Valibot validation additionally requires a task continuation cursor to identify the returned last row.", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.listUpdates.input.schema.json b/greenfield/docs/generated/schemas/tasks.listUpdates.input.schema.json new file mode 100644 index 000000000..505d2aa4e --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.listUpdates.input.schema.json @@ -0,0 +1,46 @@ +{ + "$id": "urn:mira-dashboard:tasks.listUpdates.input", + "type": "object", + "properties": { + "cursor": { + "type": "object", + "properties": { + "createdAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + } + }, + "required": [ + "createdAtMs", + "id" + ], + "additionalProperties": false + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "default": 20 + }, + "taskId": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.listUpdates.output.schema.json b/greenfield/docs/generated/schemas/tasks.listUpdates.output.schema.json new file mode 100644 index 000000000..48f913a5b --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.listUpdates.output.schema.json @@ -0,0 +1,138 @@ +{ + "$id": "urn:mira-dashboard:tasks.listUpdates.output", + "type": "object", + "properties": { + "nextCursor": { + "type": "object", + "properties": { + "createdAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + } + }, + "required": [ + "createdAtMs", + "id" + ], + "additionalProperties": false + }, + "updates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "author": { + "oneOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9._-]*$" + }, + "kind": { + "const": "automation" + } + }, + "required": [ + "id", + "kind" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "kind": { + "const": "user" + } + }, + "required": [ + "id", + "kind" + ], + "additionalProperties": false + } + ] + }, + "createdAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "messageMarkdown": { + "type": "string", + "maxLength": 20000, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + } + ] + }, + "taskId": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "updatedAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "version": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "author", + "createdAtMs", + "id", + "messageMarkdown", + "taskId", + "updatedAtMs", + "version" + ], + "additionalProperties": false, + "$comment": "Live Valibot validation additionally requires progress update timestamps not to precede creation." + }, + "maxItems": 50, + "$comment": "Live Valibot validation additionally requires strict newest-first progress ordering by creation timestamp and ID." + } + }, + "required": [ + "updates" + ], + "additionalProperties": false, + "$comment": "Live Valibot validation additionally requires a progress continuation cursor to identify the returned last row.", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.move.input.schema.json b/greenfield/docs/generated/schemas/tasks.move.input.schema.json new file mode 100644 index 000000000..722f17fa1 --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.move.input.schema.json @@ -0,0 +1,34 @@ +{ + "$id": "urn:mira-dashboard:tasks.move.input", + "type": "object", + "properties": { + "expectedVersion": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "status": { + "enum": [ + "todo", + "in-progress", + "blocked", + "done" + ], + "type": "string" + } + }, + "required": [ + "expectedVersion", + "id", + "status" + ], + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.move.output.schema.json b/greenfield/docs/generated/schemas/tasks.move.output.schema.json new file mode 100644 index 000000000..d3b1ff110 --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.move.output.schema.json @@ -0,0 +1,204 @@ +{ + "$id": "urn:mira-dashboard:tasks.move.output", + "type": "object", + "properties": { + "assignee": { + "enum": [ + "mira-2026", + "rajohan" + ], + "type": "string" + }, + "automation": { + "type": "object", + "properties": { + "cronJobId": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "kind": { + "const": "openclaw-cron" + }, + "model": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "scheduleSummary": { + "type": "string", + "maxLength": 500, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "sessionTarget": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "thinking": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "recurring": { + "type": "boolean" + } + }, + "required": [ + "cronJobId", + "kind", + "recurring" + ], + "additionalProperties": false + }, + "createdAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "labels": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "maxItems": 20, + "uniqueItems": true, + "$comment": "Live Valibot validation additionally requires task strings to use canonical code-unit order." + }, + "priority": { + "enum": [ + "low", + "medium", + "high" + ], + "type": "string" + }, + "status": { + "enum": [ + "todo", + "in-progress", + "blocked", + "done" + ], + "type": "string" + }, + "title": { + "type": "string", + "maxLength": 240, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "updatedAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "version": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "bodyMarkdown": { + "type": "string", + "maxLength": 100000, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + } + ] + } + }, + "required": [ + "createdAtMs", + "id", + "labels", + "priority", + "status", + "title", + "updatedAtMs", + "version" + ], + "additionalProperties": false, + "$comment": "Live Valibot validation additionally requires task update timestamps not to precede creation.", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.update.input.schema.json b/greenfield/docs/generated/schemas/tasks.update.input.schema.json new file mode 100644 index 000000000..387dc2f5f --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.update.input.schema.json @@ -0,0 +1,193 @@ +{ + "$id": "urn:mira-dashboard:tasks.update.input", + "type": "object", + "properties": { + "expectedVersion": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "patch": { + "type": "object", + "properties": { + "automation": { + "anyOf": [ + { + "type": "object", + "properties": { + "cronJobId": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "kind": { + "const": "openclaw-cron" + }, + "model": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "scheduleSummary": { + "type": "string", + "maxLength": 500, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "sessionTarget": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "thinking": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "recurring": { + "type": "boolean", + "default": true + } + }, + "required": [ + "cronJobId", + "kind" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "bodyMarkdown": { + "anyOf": [ + { + "type": "string", + "maxLength": 100000, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + } + ] + }, + { + "type": "null" + } + ] + }, + "labels": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "maxItems": 20, + "uniqueItems": true + }, + "priority": { + "enum": [ + "low", + "medium", + "high" + ], + "type": "string" + }, + "title": { + "type": "string", + "maxLength": 240, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + } + }, + "required": [], + "additionalProperties": false, + "minProperties": 1 + } + }, + "required": [ + "expectedVersion", + "id", + "patch" + ], + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.update.output.schema.json b/greenfield/docs/generated/schemas/tasks.update.output.schema.json new file mode 100644 index 000000000..db75dd964 --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.update.output.schema.json @@ -0,0 +1,204 @@ +{ + "$id": "urn:mira-dashboard:tasks.update.output", + "type": "object", + "properties": { + "assignee": { + "enum": [ + "mira-2026", + "rajohan" + ], + "type": "string" + }, + "automation": { + "type": "object", + "properties": { + "cronJobId": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "kind": { + "const": "openclaw-cron" + }, + "model": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "scheduleSummary": { + "type": "string", + "maxLength": 500, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "sessionTarget": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "thinking": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "recurring": { + "type": "boolean" + } + }, + "required": [ + "cronJobId", + "kind", + "recurring" + ], + "additionalProperties": false + }, + "createdAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "labels": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "maxItems": 20, + "uniqueItems": true, + "$comment": "Live Valibot validation additionally requires task strings to use canonical code-unit order." + }, + "priority": { + "enum": [ + "low", + "medium", + "high" + ], + "type": "string" + }, + "status": { + "enum": [ + "todo", + "in-progress", + "blocked", + "done" + ], + "type": "string" + }, + "title": { + "type": "string", + "maxLength": 240, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + }, + { + "pattern": "^(?![\\s\\S]*(?:[\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u0600-\\u0605\\u061C\\u06DD\\u070F\\u0890-\\u0891\\u08E2\\u180E\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF\\uFFF9-\\uFFFB]|\\uD804[\\uDCBD\\uDCCD]|\\uD80D[\\uDC30-\\uDC3F]|\\uD82F[\\uDCA0-\\uDCA3]|\\uD834[\\uDD73-\\uDD7A]|\\uDB40(?:\\uDC01|[\\uDC20-\\uDC7F])))[\\s\\S]*$" + } + ], + "$comment": "Live Valibot validation additionally requires compact task text to have canonical outer whitespace." + }, + "updatedAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "version": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "bodyMarkdown": { + "type": "string", + "maxLength": 100000, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + } + ] + } + }, + "required": [ + "createdAtMs", + "id", + "labels", + "priority", + "status", + "title", + "updatedAtMs", + "version" + ], + "additionalProperties": false, + "$comment": "Live Valibot validation additionally requires task update timestamps not to precede creation.", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.updateProgress.input.schema.json b/greenfield/docs/generated/schemas/tasks.updateProgress.input.schema.json new file mode 100644 index 000000000..4293cb488 --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.updateProgress.input.schema.json @@ -0,0 +1,44 @@ +{ + "$id": "urn:mira-dashboard:tasks.updateProgress.input", + "type": "object", + "properties": { + "expectedVersion": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "messageMarkdown": { + "type": "string", + "maxLength": 20000, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + } + ] + }, + "taskId": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "updateId": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + } + }, + "required": [ + "expectedVersion", + "messageMarkdown", + "taskId", + "updateId" + ], + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/docs/generated/schemas/tasks.updateProgress.output.schema.json b/greenfield/docs/generated/schemas/tasks.updateProgress.output.schema.json new file mode 100644 index 000000000..24958db8b --- /dev/null +++ b/greenfield/docs/generated/schemas/tasks.updateProgress.output.schema.json @@ -0,0 +1,101 @@ +{ + "$id": "urn:mira-dashboard:tasks.updateProgress.output", + "type": "object", + "properties": { + "author": { + "oneOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9._-]*$" + }, + "kind": { + "const": "automation" + } + }, + "required": [ + "id", + "kind" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "kind": { + "const": "user" + } + }, + "required": [ + "id", + "kind" + ], + "additionalProperties": false + } + ] + }, + "createdAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "id": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "messageMarkdown": { + "type": "string", + "maxLength": 20000, + "minLength": 1, + "pattern": "\\S", + "allOf": [ + { + "pattern": "^[^\\u0000]*$" + } + ] + }, + "taskId": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "format": "uuid", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "updatedAtMs": { + "type": "integer", + "minimum": 0, + "maximum": 8640000000000000 + }, + "version": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "author", + "createdAtMs", + "id", + "messageMarkdown", + "taskId", + "updatedAtMs", + "version" + ], + "additionalProperties": false, + "$comment": "Live Valibot validation additionally requires progress update timestamps not to precede creation.", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} diff --git a/greenfield/migrations/20260804022252_dashboard-foundation/migration.sql b/greenfield/migrations/20260804022252_dashboard-foundation/migration.sql index 3edc47571..20415a40b 100644 --- a/greenfield/migrations/20260804022252_dashboard-foundation/migration.sql +++ b/greenfield/migrations/20260804022252_dashboard-foundation/migration.sql @@ -372,6 +372,116 @@ CREATE UNIQUE INDEX `automation_credentials_prefix_unique` ON `automation_creden CREATE UNIQUE INDEX `automation_credentials_validator_unique` ON `automation_credentials` (`validator_version`,`validator_hash`);--> statement-breakpoint CREATE INDEX `automation_principals_created_id_idx` ON `automation_principals` (`created_at`,`id`);--> statement-breakpoint CREATE INDEX `automation_principals_active_created_id_idx` ON `automation_principals` (`created_at`,`id`) WHERE "automation_principals"."disabled_at" IS NULL;--> statement-breakpoint +CREATE TABLE `task_automation_profiles` ( + `cron_job_id` text NOT NULL, + `kind` text NOT NULL, + `model` text, + `recurring` integer NOT NULL, + `schedule_summary` text, + `session_target` text, + `task_id` text PRIMARY KEY NOT NULL, + `thinking` text, + CONSTRAINT `fk_task_automation_profiles_task_id_tasks_id_fk` FOREIGN KEY (`task_id`) REFERENCES `tasks`(`id`) ON DELETE CASCADE, + CONSTRAINT "task_automation_profiles_cron_job_id_check" CHECK(length("cron_job_id") BETWEEN 1 AND 200 AND instr("cron_job_id", char(0)) = 0 AND length(trim("cron_job_id", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0 AND "cron_job_id" NOT GLOB ('*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || char(173) || char(1536) || '-' || char(1541) || char(1564) || char(1757) || char(1807) || char(2192) || '-' || char(2193) || char(2274) || char(6158) || char(8203) || '-' || char(8207) || char(8234) || '-' || char(8238) || char(8288) || '-' || char(8292) || char(8294) || '-' || char(8303) || char(65279) || char(65529) || '-' || char(65531) || char(69821) || char(69837) || char(78896) || '-' || char(78911) || char(113824) || '-' || char(113827) || char(119155) || '-' || char(119162) || char(917505) || char(917536) || '-' || char(917631) || ']*')), + CONSTRAINT "task_automation_profiles_kind_check" CHECK("kind" = 'openclaw-cron'), + CONSTRAINT "task_automation_profiles_model_check" CHECK("model" IS NULL OR (length("model") BETWEEN 1 AND 200 AND instr("model", char(0)) = 0 AND length(trim("model", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0 AND "model" NOT GLOB ('*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || char(173) || char(1536) || '-' || char(1541) || char(1564) || char(1757) || char(1807) || char(2192) || '-' || char(2193) || char(2274) || char(6158) || char(8203) || '-' || char(8207) || char(8234) || '-' || char(8238) || char(8288) || '-' || char(8292) || char(8294) || '-' || char(8303) || char(65279) || char(65529) || '-' || char(65531) || char(69821) || char(69837) || char(78896) || '-' || char(78911) || char(113824) || '-' || char(113827) || char(119155) || '-' || char(119162) || char(917505) || char(917536) || '-' || char(917631) || ']*'))), + CONSTRAINT "task_automation_profiles_recurring_check" CHECK("recurring" IN (0, 1)), + CONSTRAINT "task_automation_profiles_schedule_summary_check" CHECK("schedule_summary" IS NULL OR (length("schedule_summary") BETWEEN 1 AND 500 AND instr("schedule_summary", char(0)) = 0 AND length(trim("schedule_summary", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0 AND "schedule_summary" NOT GLOB ('*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || char(173) || char(1536) || '-' || char(1541) || char(1564) || char(1757) || char(1807) || char(2192) || '-' || char(2193) || char(2274) || char(6158) || char(8203) || '-' || char(8207) || char(8234) || '-' || char(8238) || char(8288) || '-' || char(8292) || char(8294) || '-' || char(8303) || char(65279) || char(65529) || '-' || char(65531) || char(69821) || char(69837) || char(78896) || '-' || char(78911) || char(113824) || '-' || char(113827) || char(119155) || '-' || char(119162) || char(917505) || char(917536) || '-' || char(917631) || ']*'))), + CONSTRAINT "task_automation_profiles_session_target_check" CHECK("session_target" IS NULL OR (length("session_target") BETWEEN 1 AND 200 AND instr("session_target", char(0)) = 0 AND length(trim("session_target", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0 AND "session_target" NOT GLOB ('*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || char(173) || char(1536) || '-' || char(1541) || char(1564) || char(1757) || char(1807) || char(2192) || '-' || char(2193) || char(2274) || char(6158) || char(8203) || '-' || char(8207) || char(8234) || '-' || char(8238) || char(8288) || '-' || char(8292) || char(8294) || '-' || char(8303) || char(65279) || char(65529) || '-' || char(65531) || char(69821) || char(69837) || char(78896) || '-' || char(78911) || char(113824) || '-' || char(113827) || char(119155) || '-' || char(119162) || char(917505) || char(917536) || '-' || char(917631) || ']*'))), + CONSTRAINT "task_automation_profiles_task_id_check" CHECK(length("task_id") = 36 AND instr("task_id", char(0)) = 0 AND length(replace("task_id", '-', '')) = 32 AND replace("task_id", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr("task_id", 9, 1) = '-' AND substr("task_id", 14, 1) = '-' AND substr("task_id", 15, 1) = '7' AND substr("task_id", 19, 1) = '-' AND substr("task_id", 20, 1) GLOB '[89ab]' AND substr("task_id", 24, 1) = '-'), + CONSTRAINT "task_automation_profiles_thinking_check" CHECK("thinking" IS NULL OR (length("thinking") BETWEEN 1 AND 200 AND instr("thinking", char(0)) = 0 AND length(trim("thinking", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0 AND "thinking" NOT GLOB ('*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || char(173) || char(1536) || '-' || char(1541) || char(1564) || char(1757) || char(1807) || char(2192) || '-' || char(2193) || char(2274) || char(6158) || char(8203) || '-' || char(8207) || char(8234) || '-' || char(8238) || char(8288) || '-' || char(8292) || char(8294) || '-' || char(8303) || char(65279) || char(65529) || '-' || char(65531) || char(69821) || char(69837) || char(78896) || '-' || char(78911) || char(113824) || '-' || char(113827) || char(119155) || '-' || char(119162) || char(917505) || char(917536) || '-' || char(917631) || ']*'))) +) STRICT; +--> statement-breakpoint +CREATE TABLE `task_events` ( + `actor_id` text NOT NULL, + `actor_kind` text NOT NULL, + `created_at` integer NOT NULL, + `event_type` text NOT NULL, + `id` text PRIMARY KEY NOT NULL, + `payload_json` text DEFAULT '{}' NOT NULL, + `task_id` text NOT NULL, + CONSTRAINT "task_events_actor_check" CHECK(("actor_kind" = 'user' AND length("actor_id") = 36 AND instr("actor_id", char(0)) = 0 AND length(replace("actor_id", '-', '')) = 32 AND replace("actor_id", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr("actor_id", 9, 1) = '-' AND substr("actor_id", 14, 1) = '-' AND substr("actor_id", 15, 1) = '7' AND substr("actor_id", 19, 1) = '-' AND substr("actor_id", 20, 1) GLOB '[89ab]' AND substr("actor_id", 24, 1) = '-') OR ("actor_kind" = 'automation' AND length("actor_id") BETWEEN 1 AND 64 AND instr("actor_id", char(0)) = 0 AND "actor_id" = lower("actor_id") AND substr("actor_id", 1, 1) GLOB '[a-z0-9]' AND "actor_id" NOT GLOB '*[^a-z0-9._-]*')), + CONSTRAINT "task_events_created_at_check" CHECK("created_at" BETWEEN 0 AND 8640000000000000), + CONSTRAINT "task_events_event_type_check" CHECK("event_type" IN ('assigned', 'created', 'deleted', 'moved', 'progress-added', 'progress-deleted', 'progress-updated', 'updated')), + CONSTRAINT "task_events_id_check" CHECK(length("id") = 36 AND instr("id", char(0)) = 0 AND length(replace("id", '-', '')) = 32 AND replace("id", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr("id", 9, 1) = '-' AND substr("id", 14, 1) = '-' AND substr("id", 15, 1) = '7' AND substr("id", 19, 1) = '-' AND substr("id", 20, 1) GLOB '[89ab]' AND substr("id", 24, 1) = '-'), + CONSTRAINT "task_events_payload_json_check" CHECK(length(CAST("payload_json" AS BLOB)) <= 4096 AND CASE WHEN json_valid("payload_json") THEN json_type("payload_json") = 'object' ELSE 0 END), + CONSTRAINT "task_events_task_id_check" CHECK(length("task_id") = 36 AND instr("task_id", char(0)) = 0 AND length(replace("task_id", '-', '')) = 32 AND replace("task_id", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr("task_id", 9, 1) = '-' AND substr("task_id", 14, 1) = '-' AND substr("task_id", 15, 1) = '7' AND substr("task_id", 19, 1) = '-' AND substr("task_id", 20, 1) GLOB '[89ab]' AND substr("task_id", 24, 1) = '-') +) STRICT, WITHOUT ROWID; +--> statement-breakpoint +CREATE TABLE `task_notification_outbox` ( + `attempt_count` integer DEFAULT 0 NOT NULL, + `available_at` integer NOT NULL, + `created_at` integer NOT NULL, + `delivered_at` integer, + `event_id` text PRIMARY KEY NOT NULL, + `lease_expires_at` integer, + `lease_owner` text, + `message` text NOT NULL, + CONSTRAINT `fk_task_notification_outbox_event_id_task_events_id_fk` FOREIGN KEY (`event_id`) REFERENCES `task_events`(`id`) ON UPDATE RESTRICT ON DELETE RESTRICT, + CONSTRAINT "task_notification_outbox_attempt_count_check" CHECK("attempt_count" BETWEEN 0 AND 9007199254740991), + CONSTRAINT "task_notification_outbox_available_at_check" CHECK("available_at" BETWEEN 0 AND 8640000000000000 AND "available_at" >= "created_at"), + CONSTRAINT "task_notification_outbox_created_at_check" CHECK("created_at" BETWEEN 0 AND 8640000000000000), + CONSTRAINT "task_notification_outbox_delivered_at_check" CHECK("delivered_at" IS NULL OR ("delivered_at" BETWEEN 0 AND 8640000000000000 AND "delivered_at" >= "created_at")), + CONSTRAINT "task_notification_outbox_event_id_check" CHECK(length("event_id") = 36 AND instr("event_id", char(0)) = 0 AND length(replace("event_id", '-', '')) = 32 AND replace("event_id", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr("event_id", 9, 1) = '-' AND substr("event_id", 14, 1) = '-' AND substr("event_id", 15, 1) = '7' AND substr("event_id", 19, 1) = '-' AND substr("event_id", 20, 1) GLOB '[89ab]' AND substr("event_id", 24, 1) = '-'), + CONSTRAINT "task_notification_outbox_lease_check" CHECK(("lease_owner" IS NULL AND "lease_expires_at" IS NULL) OR ("delivered_at" IS NULL AND "lease_owner" IS NOT NULL AND length("lease_owner") = 36 AND instr("lease_owner", char(0)) = 0 AND length(replace("lease_owner", '-', '')) = 32 AND replace("lease_owner", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr("lease_owner", 9, 1) = '-' AND substr("lease_owner", 14, 1) = '-' AND substr("lease_owner", 15, 1) = '7' AND substr("lease_owner", 19, 1) = '-' AND substr("lease_owner", 20, 1) GLOB '[89ab]' AND substr("lease_owner", 24, 1) = '-' AND "lease_expires_at" IS NOT NULL AND "lease_expires_at" BETWEEN 0 AND 8640000000000000)), + CONSTRAINT "task_notification_outbox_message_check" CHECK(length("message") BETWEEN 1 AND 2048 AND instr("message", char(0)) = 0 AND length(trim("message", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0 AND "message" NOT GLOB ('*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || char(173) || char(1536) || '-' || char(1541) || char(1564) || char(1757) || char(1807) || char(2192) || '-' || char(2193) || char(2274) || char(6158) || char(8203) || '-' || char(8207) || char(8234) || '-' || char(8238) || char(8288) || '-' || char(8292) || char(8294) || '-' || char(8303) || char(65279) || char(65529) || '-' || char(65531) || char(69821) || char(69837) || char(78896) || '-' || char(78911) || char(113824) || '-' || char(113827) || char(119155) || '-' || char(119162) || char(917505) || char(917536) || '-' || char(917631) || ']*') AND length(CAST("message" AS BLOB)) <= 2048) +) STRICT, WITHOUT ROWID; +--> statement-breakpoint +CREATE TABLE `task_labels` ( + `label` text NOT NULL, + `task_id` text NOT NULL, + CONSTRAINT `task_labels_pk` PRIMARY KEY(`task_id`, `label`), + CONSTRAINT `fk_task_labels_task_id_tasks_id_fk` FOREIGN KEY (`task_id`) REFERENCES `tasks`(`id`) ON DELETE CASCADE, + CONSTRAINT "task_labels_label_check" CHECK(length("label") BETWEEN 1 AND 64 AND instr("label", char(0)) = 0 AND length(trim("label", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0 AND "label" NOT GLOB ('*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || char(173) || char(1536) || '-' || char(1541) || char(1564) || char(1757) || char(1807) || char(2192) || '-' || char(2193) || char(2274) || char(6158) || char(8203) || '-' || char(8207) || char(8234) || '-' || char(8238) || char(8288) || '-' || char(8292) || char(8294) || '-' || char(8303) || char(65279) || char(65529) || '-' || char(65531) || char(69821) || char(69837) || char(78896) || '-' || char(78911) || char(113824) || '-' || char(113827) || char(119155) || '-' || char(119162) || char(917505) || char(917536) || '-' || char(917631) || ']*')), + CONSTRAINT "task_labels_task_id_check" CHECK(length("task_id") = 36 AND instr("task_id", char(0)) = 0 AND length(replace("task_id", '-', '')) = 32 AND replace("task_id", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr("task_id", 9, 1) = '-' AND substr("task_id", 14, 1) = '-' AND substr("task_id", 15, 1) = '7' AND substr("task_id", 19, 1) = '-' AND substr("task_id", 20, 1) GLOB '[89ab]' AND substr("task_id", 24, 1) = '-') +) STRICT; +--> statement-breakpoint +CREATE TABLE `task_updates` ( + `author_id` text NOT NULL, + `author_kind` text NOT NULL, + `created_at` integer NOT NULL, + `id` text PRIMARY KEY NOT NULL, + `message_markdown` text NOT NULL, + `task_id` text NOT NULL, + `updated_at` integer NOT NULL, + `version` integer DEFAULT 1 NOT NULL, + CONSTRAINT `fk_task_updates_task_id_tasks_id_fk` FOREIGN KEY (`task_id`) REFERENCES `tasks`(`id`) ON DELETE CASCADE, + CONSTRAINT "task_updates_author_check" CHECK(("author_kind" = 'user' AND length("author_id") = 36 AND instr("author_id", char(0)) = 0 AND length(replace("author_id", '-', '')) = 32 AND replace("author_id", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr("author_id", 9, 1) = '-' AND substr("author_id", 14, 1) = '-' AND substr("author_id", 15, 1) = '7' AND substr("author_id", 19, 1) = '-' AND substr("author_id", 20, 1) GLOB '[89ab]' AND substr("author_id", 24, 1) = '-') OR ("author_kind" = 'automation' AND length("author_id") BETWEEN 1 AND 64 AND instr("author_id", char(0)) = 0 AND "author_id" = lower("author_id") AND substr("author_id", 1, 1) GLOB '[a-z0-9]' AND "author_id" NOT GLOB '*[^a-z0-9._-]*')), + CONSTRAINT "task_updates_id_check" CHECK(length("id") = 36 AND instr("id", char(0)) = 0 AND length(replace("id", '-', '')) = 32 AND replace("id", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr("id", 9, 1) = '-' AND substr("id", 14, 1) = '-' AND substr("id", 15, 1) = '7' AND substr("id", 19, 1) = '-' AND substr("id", 20, 1) GLOB '[89ab]' AND substr("id", 24, 1) = '-'), + CONSTRAINT "task_updates_message_markdown_check" CHECK(length("message_markdown") BETWEEN 1 AND 20000 AND instr("message_markdown", char(0)) = 0 AND length(trim("message_markdown", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0), + CONSTRAINT "task_updates_task_id_check" CHECK(length("task_id") = 36 AND instr("task_id", char(0)) = 0 AND length(replace("task_id", '-', '')) = 32 AND replace("task_id", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr("task_id", 9, 1) = '-' AND substr("task_id", 14, 1) = '-' AND substr("task_id", 15, 1) = '7' AND substr("task_id", 19, 1) = '-' AND substr("task_id", 20, 1) GLOB '[89ab]' AND substr("task_id", 24, 1) = '-'), + CONSTRAINT "task_updates_time_check" CHECK("created_at" BETWEEN 0 AND 8640000000000000 AND "updated_at" BETWEEN 0 AND 8640000000000000 AND "updated_at" >= "created_at"), + CONSTRAINT "task_updates_version_check" CHECK("version" BETWEEN 1 AND 9007199254740991) +) STRICT; +--> statement-breakpoint +CREATE TABLE `tasks` ( + `assignee` text, + `body_markdown` text, + `created_at` integer NOT NULL, + `id` text PRIMARY KEY NOT NULL, + `priority` text NOT NULL, + `status` text NOT NULL, + `title` text NOT NULL, + `updated_at` integer NOT NULL, + `version` integer DEFAULT 1 NOT NULL, + CONSTRAINT "tasks_assignee_check" CHECK("assignee" IS NULL OR "assignee" IN ('mira-2026', 'rajohan')), + CONSTRAINT "tasks_body_markdown_check" CHECK("body_markdown" IS NULL OR (length("body_markdown") BETWEEN 1 AND 100000 AND instr("body_markdown", char(0)) = 0 AND length(trim("body_markdown", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0)), + CONSTRAINT "tasks_id_check" CHECK(length("id") = 36 AND instr("id", char(0)) = 0 AND length(replace("id", '-', '')) = 32 AND replace("id", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr("id", 9, 1) = '-' AND substr("id", 14, 1) = '-' AND substr("id", 15, 1) = '7' AND substr("id", 19, 1) = '-' AND substr("id", 20, 1) GLOB '[89ab]' AND substr("id", 24, 1) = '-'), + CONSTRAINT "tasks_priority_check" CHECK("priority" IN ('low', 'medium', 'high')), + CONSTRAINT "tasks_status_check" CHECK("status" IN ('todo', 'in-progress', 'blocked', 'done')), + CONSTRAINT "tasks_title_check" CHECK(length("title") BETWEEN 1 AND 240 AND instr("title", char(0)) = 0 AND length(trim("title", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0 AND "title" NOT GLOB ('*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || char(173) || char(1536) || '-' || char(1541) || char(1564) || char(1757) || char(1807) || char(2192) || '-' || char(2193) || char(2274) || char(6158) || char(8203) || '-' || char(8207) || char(8234) || '-' || char(8238) || char(8288) || '-' || char(8292) || char(8294) || '-' || char(8303) || char(65279) || char(65529) || '-' || char(65531) || char(69821) || char(69837) || char(78896) || '-' || char(78911) || char(113824) || '-' || char(113827) || char(119155) || '-' || char(119162) || char(917505) || char(917536) || '-' || char(917631) || ']*')), + CONSTRAINT "tasks_time_check" CHECK("created_at" BETWEEN 0 AND 8640000000000000 AND "updated_at" BETWEEN 0 AND 8640000000000000 AND "updated_at" >= "created_at"), + CONSTRAINT "tasks_version_check" CHECK("version" BETWEEN 1 AND 9007199254740991) +) STRICT; +--> statement-breakpoint +CREATE UNIQUE INDEX `task_automation_profiles_cron_job_id_unique` ON `task_automation_profiles` (`cron_job_id`);--> statement-breakpoint +CREATE INDEX `task_events_task_created_id_idx` ON `task_events` (`task_id`,`created_at`,`id`);--> statement-breakpoint +CREATE INDEX `task_notification_outbox_eligible_idx` ON `task_notification_outbox` (`delivered_at`,`available_at`,`lease_expires_at`,`created_at`,`event_id`);--> statement-breakpoint +CREATE INDEX `task_labels_label_task_idx` ON `task_labels` (`label`,`task_id`);--> statement-breakpoint +CREATE INDEX `task_updates_task_created_id_idx` ON `task_updates` (`task_id`,`created_at`,`id`);--> statement-breakpoint +CREATE INDEX `tasks_updated_id_idx` ON `tasks` (`updated_at`,`id`);--> statement-breakpoint +CREATE INDEX `tasks_status_priority_updated_id_idx` ON `tasks` (`status`,`priority`,`updated_at`,`id`);--> statement-breakpoint +CREATE INDEX `tasks_assignee_status_updated_id_idx` ON `tasks` (`assignee`,`status`,`updated_at`,`id`);--> statement-breakpoint CREATE TRIGGER automation_credentials_validate_replacement_insert BEFORE INSERT ON automation_credentials WHEN NEW.replaces_credential_id IS NOT NULL @@ -637,3 +747,52 @@ BEFORE DELETE ON schema_migrations BEGIN SELECT RAISE(ABORT, 'schema_migrations is append-only'); END; +--> statement-breakpoint +CREATE TRIGGER task_events_validate_payload +BEFORE INSERT ON task_events +WHEN EXISTS ( + WITH RECURSIVE + payload_tree(id, parent, key, type, atom) AS ( + SELECT id, parent, key, type, atom FROM json_tree(NEW.payload_json) + ), + payload_depth(id, type, atom, depth) AS ( + SELECT id, type, atom, 0 FROM payload_tree WHERE parent IS NULL + UNION ALL + SELECT child.id, child.type, child.atom, parent.depth + 1 + FROM payload_tree AS child + JOIN payload_depth AS parent ON child.parent = parent.id + ) + SELECT 1 + FROM payload_depth + WHERE (type IN ('object', 'array') AND depth > 12) + OR (type IN ('integer', 'real') AND (atom > 9007199254740991 OR atom < -9007199254740991)) + UNION ALL + SELECT 1 + FROM payload_tree AS member + JOIN payload_tree AS parent ON parent.id = member.parent + WHERE parent.type = 'object' + GROUP BY member.parent, member.key + HAVING count(*) > 1 +) +BEGIN + SELECT RAISE(ABORT, 'task_events payload must be a bounded JSON object'); +END; +--> statement-breakpoint +CREATE TRIGGER task_events_reject_replace +BEFORE INSERT ON task_events +WHEN EXISTS (SELECT 1 FROM task_events WHERE id = NEW.id) +BEGIN + SELECT RAISE(ABORT, 'task_events is append-only'); +END; +--> statement-breakpoint +CREATE TRIGGER task_events_reject_update +BEFORE UPDATE ON task_events +BEGIN + SELECT RAISE(ABORT, 'task_events is append-only'); +END; +--> statement-breakpoint +CREATE TRIGGER task_events_reject_delete +BEFORE DELETE ON task_events +BEGIN + SELECT RAISE(ABORT, 'task_events is append-only'); +END; diff --git a/greenfield/migrations/20260804022252_dashboard-foundation/snapshot.json b/greenfield/migrations/20260804022252_dashboard-foundation/snapshot.json index 9cc323c76..73dfc934d 100644 --- a/greenfield/migrations/20260804022252_dashboard-foundation/snapshot.json +++ b/greenfield/migrations/20260804022252_dashboard-foundation/snapshot.json @@ -1,10 +1,8 @@ { "version": "7", "dialect": "sqlite", - "id": "7c296db8-4e4b-4ecd-a214-72e9e88165c3", - "prevIds": [ - "00000000-0000-0000-0000-000000000000" - ], + "id": "7f6021f7-3f3d-4d7f-ab30-5ccdf723711c", + "prevIds": [], "ddl": [ { "name": "audit_events", @@ -66,6 +64,30 @@ "name": "schema_migrations", "entityType": "tables" }, + { + "name": "task_automation_profiles", + "entityType": "tables" + }, + { + "name": "task_events", + "entityType": "tables" + }, + { + "name": "task_labels", + "entityType": "tables" + }, + { + "name": "task_notification_outbox", + "entityType": "tables" + }, + { + "name": "task_updates", + "entityType": "tables" + }, + { + "name": "tasks", + "entityType": "tables" + }, { "name": "user_recovery_codes", "entityType": "tables" @@ -1413,84 +1435,94 @@ "table": "schema_migrations" }, { - "type": "integer", + "type": "text", "notNull": true, "autoincrement": false, "default": null, "generated": null, - "name": "created_at", + "name": "cron_job_id", "entityType": "columns", - "table": "user_recovery_codes" + "table": "task_automation_profiles" }, { "type": "text", - "notNull": false, + "notNull": true, "autoincrement": false, "default": null, "generated": null, - "name": "id", + "name": "kind", "entityType": "columns", - "table": "user_recovery_codes" + "table": "task_automation_profiles" }, { "type": "text", - "notNull": true, + "notNull": false, "autoincrement": false, "default": null, "generated": null, - "name": "selector", + "name": "model", "entityType": "columns", - "table": "user_recovery_codes" + "table": "task_automation_profiles" }, { "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "recurring", + "entityType": "columns", + "table": "task_automation_profiles" + }, + { + "type": "text", "notNull": false, "autoincrement": false, "default": null, "generated": null, - "name": "used_at", + "name": "schedule_summary", "entityType": "columns", - "table": "user_recovery_codes" + "table": "task_automation_profiles" }, { "type": "text", - "notNull": true, + "notNull": false, "autoincrement": false, "default": null, "generated": null, - "name": "user_id", + "name": "session_target", "entityType": "columns", - "table": "user_recovery_codes" + "table": "task_automation_profiles" }, { "type": "text", - "notNull": true, + "notNull": false, "autoincrement": false, "default": null, "generated": null, - "name": "validator_hash", + "name": "task_id", "entityType": "columns", - "table": "user_recovery_codes" + "table": "task_automation_profiles" }, { - "type": "integer", + "type": "text", "notNull": false, "autoincrement": false, "default": null, "generated": null, - "name": "confirmed_at", + "name": "thinking", "entityType": "columns", - "table": "user_totp_factors" + "table": "task_automation_profiles" }, { - "type": "integer", + "type": "text", "notNull": true, "autoincrement": false, "default": null, "generated": null, - "name": "created_at", + "name": "actor_id", "entityType": "columns", - "table": "user_totp_factors" + "table": "task_events" }, { "type": "text", @@ -1498,9 +1530,9 @@ "autoincrement": false, "default": null, "generated": null, - "name": "encrypted_secret", + "name": "actor_kind", "entityType": "columns", - "table": "user_totp_factors" + "table": "task_events" }, { "type": "integer", @@ -1508,9 +1540,19 @@ "autoincrement": false, "default": null, "generated": null, - "name": "enrollment_expires_at", + "name": "created_at", "entityType": "columns", - "table": "user_totp_factors" + "table": "task_events" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "event_type", + "entityType": "columns", + "table": "task_events" }, { "type": "text", @@ -1520,27 +1562,27 @@ "generated": null, "name": "id", "entityType": "columns", - "table": "user_totp_factors" + "table": "task_events" }, { "type": "text", "notNull": true, "autoincrement": false, - "default": null, + "default": "'{}'", "generated": null, - "name": "label", + "name": "payload_json", "entityType": "columns", - "table": "user_totp_factors" + "table": "task_events" }, { - "type": "integer", - "notNull": false, + "type": "text", + "notNull": true, "autoincrement": false, "default": null, "generated": null, - "name": "last_used_step", + "name": "task_id", "entityType": "columns", - "table": "user_totp_factors" + "table": "task_events" }, { "type": "text", @@ -1548,9 +1590,9 @@ "autoincrement": false, "default": null, "generated": null, - "name": "secret_key_id", + "name": "label", "entityType": "columns", - "table": "user_totp_factors" + "table": "task_labels" }, { "type": "text", @@ -1558,19 +1600,19 @@ "autoincrement": false, "default": null, "generated": null, - "name": "user_id", + "name": "task_id", "entityType": "columns", - "table": "user_totp_factors" + "table": "task_labels" }, { "type": "integer", "notNull": true, "autoincrement": false, - "default": null, + "default": "0", "generated": null, - "name": "algorithm", + "name": "attempt_count", "entityType": "columns", - "table": "user_webauthn_credentials" + "table": "task_notification_outbox" }, { "type": "integer", @@ -1578,9 +1620,9 @@ "autoincrement": false, "default": null, "generated": null, - "name": "backed_up", + "name": "available_at", "entityType": "columns", - "table": "user_webauthn_credentials" + "table": "task_notification_outbox" }, { "type": "integer", @@ -1588,39 +1630,39 @@ "autoincrement": false, "default": null, "generated": null, - "name": "counter", + "name": "created_at", "entityType": "columns", - "table": "user_webauthn_credentials" + "table": "task_notification_outbox" }, { "type": "integer", - "notNull": true, + "notNull": false, "autoincrement": false, "default": null, "generated": null, - "name": "created_at", + "name": "delivered_at", "entityType": "columns", - "table": "user_webauthn_credentials" + "table": "task_notification_outbox" }, { "type": "text", - "notNull": true, + "notNull": false, "autoincrement": false, "default": null, "generated": null, - "name": "credential_id", + "name": "event_id", "entityType": "columns", - "table": "user_webauthn_credentials" + "table": "task_notification_outbox" }, { - "type": "text", - "notNull": true, + "type": "integer", + "notNull": false, "autoincrement": false, "default": null, "generated": null, - "name": "device_type", + "name": "lease_expires_at", "entityType": "columns", - "table": "user_webauthn_credentials" + "table": "task_notification_outbox" }, { "type": "text", @@ -1628,9 +1670,9 @@ "autoincrement": false, "default": null, "generated": null, - "name": "id", + "name": "lease_owner", "entityType": "columns", - "table": "user_webauthn_credentials" + "table": "task_notification_outbox" }, { "type": "text", @@ -1638,49 +1680,49 @@ "autoincrement": false, "default": null, "generated": null, - "name": "label", + "name": "message", "entityType": "columns", - "table": "user_webauthn_credentials" + "table": "task_notification_outbox" }, { - "type": "integer", - "notNull": false, + "type": "text", + "notNull": true, "autoincrement": false, "default": null, "generated": null, - "name": "last_used_at", + "name": "author_id", "entityType": "columns", - "table": "user_webauthn_credentials" + "table": "task_updates" }, { - "type": "blob", + "type": "text", "notNull": true, "autoincrement": false, "default": null, "generated": null, - "name": "public_key", + "name": "author_kind", "entityType": "columns", - "table": "user_webauthn_credentials" + "table": "task_updates" }, { - "type": "text", + "type": "integer", "notNull": true, "autoincrement": false, "default": null, "generated": null, - "name": "rp_id", + "name": "created_at", "entityType": "columns", - "table": "user_webauthn_credentials" + "table": "task_updates" }, { - "type": "integer", - "notNull": true, + "type": "text", + "notNull": false, "autoincrement": false, "default": null, "generated": null, - "name": "transport_mask", + "name": "id", "entityType": "columns", - "table": "user_webauthn_credentials" + "table": "task_updates" }, { "type": "text", @@ -1688,19 +1730,19 @@ "autoincrement": false, "default": null, "generated": null, - "name": "user_id", + "name": "message_markdown", "entityType": "columns", - "table": "user_webauthn_credentials" + "table": "task_updates" }, { - "type": "integer", + "type": "text", "notNull": true, "autoincrement": false, - "default": "1", + "default": null, "generated": null, - "name": "authentication_version", + "name": "task_id", "entityType": "columns", - "table": "users" + "table": "task_updates" }, { "type": "integer", @@ -1708,19 +1750,29 @@ "autoincrement": false, "default": null, "generated": null, - "name": "created_at", + "name": "updated_at", "entityType": "columns", - "table": "users" + "table": "task_updates" }, { "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "1", + "generated": null, + "name": "version", + "entityType": "columns", + "table": "task_updates" + }, + { + "type": "text", "notNull": false, "autoincrement": false, "default": null, "generated": null, - "name": "disabled_at", + "name": "assignee", "entityType": "columns", - "table": "users" + "table": "tasks" }, { "type": "text", @@ -1728,19 +1780,29 @@ "autoincrement": false, "default": null, "generated": null, - "name": "id", + "name": "body_markdown", "entityType": "columns", - "table": "users" + "table": "tasks" }, { "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "tasks" + }, + { + "type": "text", "notNull": false, "autoincrement": false, "default": null, "generated": null, - "name": "mfa_enabled_at", + "name": "id", "entityType": "columns", - "table": "users" + "table": "tasks" }, { "type": "text", @@ -1748,19 +1810,19 @@ "autoincrement": false, "default": null, "generated": null, - "name": "password_hash", + "name": "priority", "entityType": "columns", - "table": "users" + "table": "tasks" }, { - "type": "integer", + "type": "text", "notNull": true, "autoincrement": false, "default": null, "generated": null, - "name": "updated_at", + "name": "status", "entityType": "columns", - "table": "users" + "table": "tasks" }, { "type": "text", @@ -1768,66 +1830,446 @@ "autoincrement": false, "default": null, "generated": null, - "name": "username", + "name": "title", "entityType": "columns", - "table": "users" + "table": "tasks" }, { - "columns": [ - "pending_login_id" - ], - "tableTo": "auth_pending_logins", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_auth_challenges_pending_login_id_auth_pending_logins_id_fk", - "entityType": "fks", - "table": "auth_challenges" + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "tasks" }, { - "columns": [ - "session_id" - ], - "tableTo": "auth_sessions", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_auth_challenges_session_id_auth_sessions_id_fk", - "entityType": "fks", - "table": "auth_challenges" + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "1", + "generated": null, + "name": "version", + "entityType": "columns", + "table": "tasks" }, { - "columns": [ - "replaced_session_id" - ], - "tableTo": "auth_sessions", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_auth_pending_logins_replaced_session_id_auth_sessions_id_fk", - "entityType": "fks", - "table": "auth_pending_logins" + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "user_recovery_codes" }, { - "columns": [ - "user_id" - ], - "tableTo": "users", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "user_recovery_codes" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "selector", + "entityType": "columns", + "table": "user_recovery_codes" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "used_at", + "entityType": "columns", + "table": "user_recovery_codes" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "user_recovery_codes" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "validator_hash", + "entityType": "columns", + "table": "user_recovery_codes" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "confirmed_at", + "entityType": "columns", + "table": "user_totp_factors" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "user_totp_factors" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "encrypted_secret", + "entityType": "columns", + "table": "user_totp_factors" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "enrollment_expires_at", + "entityType": "columns", + "table": "user_totp_factors" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "user_totp_factors" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "label", + "entityType": "columns", + "table": "user_totp_factors" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_used_step", + "entityType": "columns", + "table": "user_totp_factors" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret_key_id", + "entityType": "columns", + "table": "user_totp_factors" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "user_totp_factors" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "algorithm", + "entityType": "columns", + "table": "user_webauthn_credentials" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "backed_up", + "entityType": "columns", + "table": "user_webauthn_credentials" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "counter", + "entityType": "columns", + "table": "user_webauthn_credentials" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "user_webauthn_credentials" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "credential_id", + "entityType": "columns", + "table": "user_webauthn_credentials" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "device_type", + "entityType": "columns", + "table": "user_webauthn_credentials" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "user_webauthn_credentials" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "label", + "entityType": "columns", + "table": "user_webauthn_credentials" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_used_at", + "entityType": "columns", + "table": "user_webauthn_credentials" + }, + { + "type": "blob", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "public_key", + "entityType": "columns", + "table": "user_webauthn_credentials" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "rp_id", + "entityType": "columns", + "table": "user_webauthn_credentials" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "transport_mask", + "entityType": "columns", + "table": "user_webauthn_credentials" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "user_webauthn_credentials" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "1", + "generated": null, + "name": "authentication_version", + "entityType": "columns", + "table": "users" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "users" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "disabled_at", + "entityType": "columns", + "table": "users" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "users" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "mfa_enabled_at", + "entityType": "columns", + "table": "users" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "password_hash", + "entityType": "columns", + "table": "users" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "users" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "username", + "entityType": "columns", + "table": "users" + }, + { + "columns": [ + "pending_login_id" + ], + "tableTo": "auth_pending_logins", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_auth_challenges_pending_login_id_auth_pending_logins_id_fk", + "entityType": "fks", + "table": "auth_challenges" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "auth_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_auth_challenges_session_id_auth_sessions_id_fk", + "entityType": "fks", + "table": "auth_challenges" + }, + { + "columns": [ + "replaced_session_id" + ], + "tableTo": "auth_sessions", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "nameExplicit": false, + "name": "fk_auth_pending_logins_replaced_session_id_auth_sessions_id_fk", + "entityType": "fks", + "table": "auth_pending_logins" + }, + { + "columns": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, "name": "fk_auth_pending_logins_user_id_users_id_fk", "entityType": "fks", "table": "auth_pending_logins" @@ -1952,6 +2394,66 @@ "entityType": "fks", "table": "notifications" }, + { + "columns": [ + "task_id" + ], + "tableTo": "tasks", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_task_automation_profiles_task_id_tasks_id_fk", + "entityType": "fks", + "table": "task_automation_profiles" + }, + { + "columns": [ + "task_id" + ], + "tableTo": "tasks", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_task_labels_task_id_tasks_id_fk", + "entityType": "fks", + "table": "task_labels" + }, + { + "columns": [ + "event_id" + ], + "tableTo": "task_events", + "columnsTo": [ + "id" + ], + "onUpdate": "RESTRICT", + "onDelete": "RESTRICT", + "nameExplicit": false, + "name": "fk_task_notification_outbox_event_id_task_events_id_fk", + "entityType": "fks", + "table": "task_notification_outbox" + }, + { + "columns": [ + "task_id" + ], + "tableTo": "tasks", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_task_updates_task_id_tasks_id_fk", + "entityType": "fks", + "table": "task_updates" + }, { "columns": [ "user_id" @@ -2007,6 +2509,16 @@ "entityType": "pks", "table": "automation_principal_capabilities" }, + { + "columns": [ + "task_id", + "label" + ], + "nameExplicit": true, + "name": "task_labels_pk", + "entityType": "pks", + "table": "task_labels" + }, { "columns": [ "id" @@ -2133,6 +2645,51 @@ "table": "schema_migrations", "entityType": "pks" }, + { + "columns": [ + "task_id" + ], + "nameExplicit": false, + "name": "task_automation_profiles_pk", + "table": "task_automation_profiles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "task_events_pk", + "table": "task_events", + "entityType": "pks" + }, + { + "columns": [ + "event_id" + ], + "nameExplicit": false, + "name": "task_notification_outbox_pk", + "table": "task_notification_outbox", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "task_updates_pk", + "table": "task_updates", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tasks_pk", + "table": "tasks", + "entityType": "pks" + }, { "columns": [ "id" @@ -2522,43 +3079,213 @@ { "columns": [ { - "value": "validator_version", + "value": "validator_version", + "isExpression": false + }, + { + "value": "validator_hash", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "automation_credentials_validator_unique", + "entityType": "indexes", + "table": "automation_credentials" + }, + { + "columns": [ + { + "value": "created_at", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "automation_principals_created_id_idx", + "entityType": "indexes", + "table": "automation_principals" + }, + { + "columns": [ + { + "value": "created_at", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": "\"automation_principals\".\"disabled_at\" IS NULL", + "origin": "manual", + "name": "automation_principals_active_created_id_idx", + "entityType": "indexes", + "table": "automation_principals" + }, + { + "columns": [ + { + "value": "monitor_run_id", + "isExpression": false + }, + { + "value": "incident_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "incident_observations_run_incident_unique", + "entityType": "indexes", + "table": "incident_observations" + }, + { + "columns": [ + { + "value": "incident_id", + "isExpression": false + }, + { + "value": "observed_at", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "incident_observations_incident_observed_id_idx", + "entityType": "indexes", + "table": "incident_observations" + }, + { + "columns": [ + { + "value": "monitor_run_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "incident_observations_run_idx", + "entityType": "indexes", + "table": "incident_observations" + }, + { + "columns": [ + { + "value": "monitor_key", + "isExpression": false + }, + { + "value": "fingerprint", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "incidents_monitor_fingerprint_unique", + "entityType": "indexes", + "table": "incidents" + }, + { + "columns": [ + { + "value": "monitor_key", + "isExpression": false + }, + { + "value": "last_seen_at", + "isExpression": false + } + ], + "isUnique": false, + "where": "\"incidents\".\"state\" = 'active'", + "origin": "manual", + "name": "incidents_active_monitor_seen_idx", + "entityType": "indexes", + "table": "incidents" + }, + { + "columns": [ + { + "value": "monitor_key", + "isExpression": false + }, + { + "value": "completed_at", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": "\"monitor_runs\".\"complete_snapshot\" = 1 AND \"monitor_runs\".\"state\" = 'succeeded'", + "origin": "manual", + "name": "monitor_runs_monitor_completed_id_idx", + "entityType": "indexes", + "table": "monitor_runs" + }, + { + "columns": [ + { + "value": "incident_id", + "isExpression": false + }, + { + "value": "incident_generation", "isExpression": false }, { - "value": "validator_hash", + "value": "channel", "isExpression": false } ], "isUnique": true, - "where": null, + "where": "\"notifications\".\"incident_id\" IS NOT NULL", "origin": "manual", - "name": "automation_credentials_validator_unique", + "name": "notifications_incident_generation_channel_unique", "entityType": "indexes", - "table": "automation_credentials" + "table": "notifications" }, { "columns": [ { - "value": "created_at", - "isExpression": false - }, - { - "value": "id", + "value": "occurred_at", "isExpression": false } ], "isUnique": false, - "where": null, + "where": "\"notifications\".\"read_at\" IS NULL", "origin": "manual", - "name": "automation_principals_created_id_idx", + "name": "notifications_unread_occurred_idx", "entityType": "indexes", - "table": "automation_principals" + "table": "notifications" }, { "columns": [ { - "value": "created_at", + "value": "expires_at", "isExpression": false }, { @@ -2567,38 +3294,38 @@ } ], "isUnique": false, - "where": "\"automation_principals\".\"disabled_at\" IS NULL", + "where": null, "origin": "manual", - "name": "automation_principals_active_created_id_idx", + "name": "realtime_events_expires_id_idx", "entityType": "indexes", - "table": "automation_principals" + "table": "realtime_events" }, { "columns": [ { - "value": "monitor_run_id", + "value": "topic", "isExpression": false }, { - "value": "incident_id", + "value": "id", "isExpression": false } ], - "isUnique": true, + "isUnique": false, "where": null, "origin": "manual", - "name": "incident_observations_run_incident_unique", + "name": "realtime_events_topic_id_idx", "entityType": "indexes", - "table": "incident_observations" + "table": "realtime_events" }, { "columns": [ { - "value": "incident_id", + "value": "kind", "isExpression": false }, { - "value": "observed_at", + "value": "occurred_at", "isExpression": false }, { @@ -2609,14 +3336,22 @@ "isUnique": false, "where": null, "origin": "manual", - "name": "incident_observations_incident_observed_id_idx", + "name": "reports_kind_occurred_id_idx", "entityType": "indexes", - "table": "incident_observations" + "table": "reports" }, { "columns": [ { - "value": "monitor_run_id", + "value": "source", + "isExpression": false + }, + { + "value": "source_job_id", + "isExpression": false + }, + { + "value": "occurred_at", "isExpression": false }, { @@ -2627,108 +3362,102 @@ "isUnique": false, "where": null, "origin": "manual", - "name": "incident_observations_run_idx", + "name": "reports_source_job_occurred_id_idx", "entityType": "indexes", - "table": "incident_observations" + "table": "reports" }, { "columns": [ { - "value": "monitor_key", - "isExpression": false - }, - { - "value": "fingerprint", + "value": "cron_job_id", "isExpression": false } ], "isUnique": true, "where": null, "origin": "manual", - "name": "incidents_monitor_fingerprint_unique", + "name": "task_automation_profiles_cron_job_id_unique", "entityType": "indexes", - "table": "incidents" + "table": "task_automation_profiles" }, { "columns": [ { - "value": "monitor_key", + "value": "task_id", "isExpression": false }, { - "value": "last_seen_at", + "value": "created_at", + "isExpression": false + }, + { + "value": "id", "isExpression": false } ], "isUnique": false, - "where": "\"incidents\".\"state\" = 'active'", + "where": null, "origin": "manual", - "name": "incidents_active_monitor_seen_idx", + "name": "task_events_task_created_id_idx", "entityType": "indexes", - "table": "incidents" + "table": "task_events" }, { "columns": [ { - "value": "monitor_key", - "isExpression": false - }, - { - "value": "completed_at", + "value": "label", "isExpression": false }, { - "value": "id", + "value": "task_id", "isExpression": false } ], "isUnique": false, - "where": "\"monitor_runs\".\"complete_snapshot\" = 1 AND \"monitor_runs\".\"state\" = 'succeeded'", + "where": null, "origin": "manual", - "name": "monitor_runs_monitor_completed_id_idx", + "name": "task_labels_label_task_idx", "entityType": "indexes", - "table": "monitor_runs" + "table": "task_labels" }, { "columns": [ { - "value": "incident_id", + "value": "delivered_at", "isExpression": false }, { - "value": "incident_generation", + "value": "available_at", "isExpression": false }, { - "value": "channel", + "value": "lease_expires_at", "isExpression": false - } - ], - "isUnique": true, - "where": "\"notifications\".\"incident_id\" IS NOT NULL", - "origin": "manual", - "name": "notifications_incident_generation_channel_unique", - "entityType": "indexes", - "table": "notifications" - }, - { - "columns": [ + }, { - "value": "occurred_at", + "value": "created_at", + "isExpression": false + }, + { + "value": "event_id", "isExpression": false } ], "isUnique": false, - "where": "\"notifications\".\"read_at\" IS NULL", + "where": null, "origin": "manual", - "name": "notifications_unread_occurred_idx", + "name": "task_notification_outbox_eligible_idx", "entityType": "indexes", - "table": "notifications" + "table": "task_notification_outbox" }, { "columns": [ { - "value": "expires_at", + "value": "task_id", + "isExpression": false + }, + { + "value": "created_at", "isExpression": false }, { @@ -2739,14 +3468,14 @@ "isUnique": false, "where": null, "origin": "manual", - "name": "realtime_events_expires_id_idx", + "name": "task_updates_task_created_id_idx", "entityType": "indexes", - "table": "realtime_events" + "table": "task_updates" }, { "columns": [ { - "value": "topic", + "value": "updated_at", "isExpression": false }, { @@ -2757,18 +3486,22 @@ "isUnique": false, "where": null, "origin": "manual", - "name": "realtime_events_topic_id_idx", + "name": "tasks_updated_id_idx", "entityType": "indexes", - "table": "realtime_events" + "table": "tasks" }, { "columns": [ { - "value": "kind", + "value": "status", "isExpression": false }, { - "value": "occurred_at", + "value": "priority", + "isExpression": false + }, + { + "value": "updated_at", "isExpression": false }, { @@ -2779,22 +3512,22 @@ "isUnique": false, "where": null, "origin": "manual", - "name": "reports_kind_occurred_id_idx", + "name": "tasks_status_priority_updated_id_idx", "entityType": "indexes", - "table": "reports" + "table": "tasks" }, { "columns": [ { - "value": "source", + "value": "assignee", "isExpression": false }, { - "value": "source_job_id", + "value": "status", "isExpression": false }, { - "value": "occurred_at", + "value": "updated_at", "isExpression": false }, { @@ -2805,9 +3538,9 @@ "isUnique": false, "where": null, "origin": "manual", - "name": "reports_source_job_occurred_id_idx", + "name": "tasks_assignee_status_updated_id_idx", "entityType": "indexes", - "table": "reports" + "table": "tasks" }, { "columns": [ @@ -3248,7 +3981,7 @@ "table": "automation_credentials" }, { - "value": "\"capability\" IN ('notifications:read', 'reports:read')", + "value": "\"capability\" IN ('notifications:read', 'reports:read', 'tasks:read', 'tasks:write')", "name": "automation_principal_capabilities_capability_check", "entityType": "checks", "table": "automation_principal_capabilities" @@ -3463,6 +4196,228 @@ "entityType": "checks", "table": "schema_migrations" }, + { + "value": "length(\"cron_job_id\") BETWEEN 1 AND 200 AND instr(\"cron_job_id\", char(0)) = 0 AND length(trim(\"cron_job_id\", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0 AND \"cron_job_id\" NOT GLOB ('*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || char(173) || char(1536) || '-' || char(1541) || char(1564) || char(1757) || char(1807) || char(2192) || '-' || char(2193) || char(2274) || char(6158) || char(8203) || '-' || char(8207) || char(8234) || '-' || char(8238) || char(8288) || '-' || char(8292) || char(8294) || '-' || char(8303) || char(65279) || char(65529) || '-' || char(65531) || char(69821) || char(69837) || char(78896) || '-' || char(78911) || char(113824) || '-' || char(113827) || char(119155) || '-' || char(119162) || char(917505) || char(917536) || '-' || char(917631) || ']*')", + "name": "task_automation_profiles_cron_job_id_check", + "entityType": "checks", + "table": "task_automation_profiles" + }, + { + "value": "\"kind\" = 'openclaw-cron'", + "name": "task_automation_profiles_kind_check", + "entityType": "checks", + "table": "task_automation_profiles" + }, + { + "value": "\"model\" IS NULL OR (length(\"model\") BETWEEN 1 AND 200 AND instr(\"model\", char(0)) = 0 AND length(trim(\"model\", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0 AND \"model\" NOT GLOB ('*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || char(173) || char(1536) || '-' || char(1541) || char(1564) || char(1757) || char(1807) || char(2192) || '-' || char(2193) || char(2274) || char(6158) || char(8203) || '-' || char(8207) || char(8234) || '-' || char(8238) || char(8288) || '-' || char(8292) || char(8294) || '-' || char(8303) || char(65279) || char(65529) || '-' || char(65531) || char(69821) || char(69837) || char(78896) || '-' || char(78911) || char(113824) || '-' || char(113827) || char(119155) || '-' || char(119162) || char(917505) || char(917536) || '-' || char(917631) || ']*'))", + "name": "task_automation_profiles_model_check", + "entityType": "checks", + "table": "task_automation_profiles" + }, + { + "value": "\"recurring\" IN (0, 1)", + "name": "task_automation_profiles_recurring_check", + "entityType": "checks", + "table": "task_automation_profiles" + }, + { + "value": "\"schedule_summary\" IS NULL OR (length(\"schedule_summary\") BETWEEN 1 AND 500 AND instr(\"schedule_summary\", char(0)) = 0 AND length(trim(\"schedule_summary\", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0 AND \"schedule_summary\" NOT GLOB ('*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || char(173) || char(1536) || '-' || char(1541) || char(1564) || char(1757) || char(1807) || char(2192) || '-' || char(2193) || char(2274) || char(6158) || char(8203) || '-' || char(8207) || char(8234) || '-' || char(8238) || char(8288) || '-' || char(8292) || char(8294) || '-' || char(8303) || char(65279) || char(65529) || '-' || char(65531) || char(69821) || char(69837) || char(78896) || '-' || char(78911) || char(113824) || '-' || char(113827) || char(119155) || '-' || char(119162) || char(917505) || char(917536) || '-' || char(917631) || ']*'))", + "name": "task_automation_profiles_schedule_summary_check", + "entityType": "checks", + "table": "task_automation_profiles" + }, + { + "value": "\"session_target\" IS NULL OR (length(\"session_target\") BETWEEN 1 AND 200 AND instr(\"session_target\", char(0)) = 0 AND length(trim(\"session_target\", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0 AND \"session_target\" NOT GLOB ('*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || char(173) || char(1536) || '-' || char(1541) || char(1564) || char(1757) || char(1807) || char(2192) || '-' || char(2193) || char(2274) || char(6158) || char(8203) || '-' || char(8207) || char(8234) || '-' || char(8238) || char(8288) || '-' || char(8292) || char(8294) || '-' || char(8303) || char(65279) || char(65529) || '-' || char(65531) || char(69821) || char(69837) || char(78896) || '-' || char(78911) || char(113824) || '-' || char(113827) || char(119155) || '-' || char(119162) || char(917505) || char(917536) || '-' || char(917631) || ']*'))", + "name": "task_automation_profiles_session_target_check", + "entityType": "checks", + "table": "task_automation_profiles" + }, + { + "value": "length(\"task_id\") = 36 AND instr(\"task_id\", char(0)) = 0 AND length(replace(\"task_id\", '-', '')) = 32 AND replace(\"task_id\", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr(\"task_id\", 9, 1) = '-' AND substr(\"task_id\", 14, 1) = '-' AND substr(\"task_id\", 15, 1) = '7' AND substr(\"task_id\", 19, 1) = '-' AND substr(\"task_id\", 20, 1) GLOB '[89ab]' AND substr(\"task_id\", 24, 1) = '-'", + "name": "task_automation_profiles_task_id_check", + "entityType": "checks", + "table": "task_automation_profiles" + }, + { + "value": "\"thinking\" IS NULL OR (length(\"thinking\") BETWEEN 1 AND 200 AND instr(\"thinking\", char(0)) = 0 AND length(trim(\"thinking\", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0 AND \"thinking\" NOT GLOB ('*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || char(173) || char(1536) || '-' || char(1541) || char(1564) || char(1757) || char(1807) || char(2192) || '-' || char(2193) || char(2274) || char(6158) || char(8203) || '-' || char(8207) || char(8234) || '-' || char(8238) || char(8288) || '-' || char(8292) || char(8294) || '-' || char(8303) || char(65279) || char(65529) || '-' || char(65531) || char(69821) || char(69837) || char(78896) || '-' || char(78911) || char(113824) || '-' || char(113827) || char(119155) || '-' || char(119162) || char(917505) || char(917536) || '-' || char(917631) || ']*'))", + "name": "task_automation_profiles_thinking_check", + "entityType": "checks", + "table": "task_automation_profiles" + }, + { + "value": "(\"actor_kind\" = 'user' AND length(\"actor_id\") = 36 AND instr(\"actor_id\", char(0)) = 0 AND length(replace(\"actor_id\", '-', '')) = 32 AND replace(\"actor_id\", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr(\"actor_id\", 9, 1) = '-' AND substr(\"actor_id\", 14, 1) = '-' AND substr(\"actor_id\", 15, 1) = '7' AND substr(\"actor_id\", 19, 1) = '-' AND substr(\"actor_id\", 20, 1) GLOB '[89ab]' AND substr(\"actor_id\", 24, 1) = '-') OR (\"actor_kind\" = 'automation' AND length(\"actor_id\") BETWEEN 1 AND 64 AND instr(\"actor_id\", char(0)) = 0 AND \"actor_id\" = lower(\"actor_id\") AND substr(\"actor_id\", 1, 1) GLOB '[a-z0-9]' AND \"actor_id\" NOT GLOB '*[^a-z0-9._-]*')", + "name": "task_events_actor_check", + "entityType": "checks", + "table": "task_events" + }, + { + "value": "\"created_at\" BETWEEN 0 AND 8640000000000000", + "name": "task_events_created_at_check", + "entityType": "checks", + "table": "task_events" + }, + { + "value": "\"event_type\" IN ('assigned', 'created', 'deleted', 'moved', 'progress-added', 'progress-deleted', 'progress-updated', 'updated')", + "name": "task_events_event_type_check", + "entityType": "checks", + "table": "task_events" + }, + { + "value": "length(\"id\") = 36 AND instr(\"id\", char(0)) = 0 AND length(replace(\"id\", '-', '')) = 32 AND replace(\"id\", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr(\"id\", 9, 1) = '-' AND substr(\"id\", 14, 1) = '-' AND substr(\"id\", 15, 1) = '7' AND substr(\"id\", 19, 1) = '-' AND substr(\"id\", 20, 1) GLOB '[89ab]' AND substr(\"id\", 24, 1) = '-'", + "name": "task_events_id_check", + "entityType": "checks", + "table": "task_events" + }, + { + "value": "length(CAST(\"payload_json\" AS BLOB)) <= 4096 AND CASE WHEN json_valid(\"payload_json\") THEN json_type(\"payload_json\") = 'object' ELSE 0 END", + "name": "task_events_payload_json_check", + "entityType": "checks", + "table": "task_events" + }, + { + "value": "length(\"task_id\") = 36 AND instr(\"task_id\", char(0)) = 0 AND length(replace(\"task_id\", '-', '')) = 32 AND replace(\"task_id\", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr(\"task_id\", 9, 1) = '-' AND substr(\"task_id\", 14, 1) = '-' AND substr(\"task_id\", 15, 1) = '7' AND substr(\"task_id\", 19, 1) = '-' AND substr(\"task_id\", 20, 1) GLOB '[89ab]' AND substr(\"task_id\", 24, 1) = '-'", + "name": "task_events_task_id_check", + "entityType": "checks", + "table": "task_events" + }, + { + "value": "length(\"label\") BETWEEN 1 AND 64 AND instr(\"label\", char(0)) = 0 AND length(trim(\"label\", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0 AND \"label\" NOT GLOB ('*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || char(173) || char(1536) || '-' || char(1541) || char(1564) || char(1757) || char(1807) || char(2192) || '-' || char(2193) || char(2274) || char(6158) || char(8203) || '-' || char(8207) || char(8234) || '-' || char(8238) || char(8288) || '-' || char(8292) || char(8294) || '-' || char(8303) || char(65279) || char(65529) || '-' || char(65531) || char(69821) || char(69837) || char(78896) || '-' || char(78911) || char(113824) || '-' || char(113827) || char(119155) || '-' || char(119162) || char(917505) || char(917536) || '-' || char(917631) || ']*')", + "name": "task_labels_label_check", + "entityType": "checks", + "table": "task_labels" + }, + { + "value": "length(\"task_id\") = 36 AND instr(\"task_id\", char(0)) = 0 AND length(replace(\"task_id\", '-', '')) = 32 AND replace(\"task_id\", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr(\"task_id\", 9, 1) = '-' AND substr(\"task_id\", 14, 1) = '-' AND substr(\"task_id\", 15, 1) = '7' AND substr(\"task_id\", 19, 1) = '-' AND substr(\"task_id\", 20, 1) GLOB '[89ab]' AND substr(\"task_id\", 24, 1) = '-'", + "name": "task_labels_task_id_check", + "entityType": "checks", + "table": "task_labels" + }, + { + "value": "\"attempt_count\" BETWEEN 0 AND 9007199254740991", + "name": "task_notification_outbox_attempt_count_check", + "entityType": "checks", + "table": "task_notification_outbox" + }, + { + "value": "\"available_at\" BETWEEN 0 AND 8640000000000000 AND \"available_at\" >= \"created_at\"", + "name": "task_notification_outbox_available_at_check", + "entityType": "checks", + "table": "task_notification_outbox" + }, + { + "value": "\"created_at\" BETWEEN 0 AND 8640000000000000", + "name": "task_notification_outbox_created_at_check", + "entityType": "checks", + "table": "task_notification_outbox" + }, + { + "value": "\"delivered_at\" IS NULL OR (\"delivered_at\" BETWEEN 0 AND 8640000000000000 AND \"delivered_at\" >= \"created_at\")", + "name": "task_notification_outbox_delivered_at_check", + "entityType": "checks", + "table": "task_notification_outbox" + }, + { + "value": "length(\"event_id\") = 36 AND instr(\"event_id\", char(0)) = 0 AND length(replace(\"event_id\", '-', '')) = 32 AND replace(\"event_id\", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr(\"event_id\", 9, 1) = '-' AND substr(\"event_id\", 14, 1) = '-' AND substr(\"event_id\", 15, 1) = '7' AND substr(\"event_id\", 19, 1) = '-' AND substr(\"event_id\", 20, 1) GLOB '[89ab]' AND substr(\"event_id\", 24, 1) = '-'", + "name": "task_notification_outbox_event_id_check", + "entityType": "checks", + "table": "task_notification_outbox" + }, + { + "value": "(\"lease_owner\" IS NULL AND \"lease_expires_at\" IS NULL) OR (\"delivered_at\" IS NULL AND \"lease_owner\" IS NOT NULL AND length(\"lease_owner\") = 36 AND instr(\"lease_owner\", char(0)) = 0 AND length(replace(\"lease_owner\", '-', '')) = 32 AND replace(\"lease_owner\", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr(\"lease_owner\", 9, 1) = '-' AND substr(\"lease_owner\", 14, 1) = '-' AND substr(\"lease_owner\", 15, 1) = '7' AND substr(\"lease_owner\", 19, 1) = '-' AND substr(\"lease_owner\", 20, 1) GLOB '[89ab]' AND substr(\"lease_owner\", 24, 1) = '-' AND \"lease_expires_at\" IS NOT NULL AND \"lease_expires_at\" BETWEEN 0 AND 8640000000000000)", + "name": "task_notification_outbox_lease_check", + "entityType": "checks", + "table": "task_notification_outbox" + }, + { + "value": "length(\"message\") BETWEEN 1 AND 2048 AND instr(\"message\", char(0)) = 0 AND length(trim(\"message\", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0 AND \"message\" NOT GLOB ('*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || char(173) || char(1536) || '-' || char(1541) || char(1564) || char(1757) || char(1807) || char(2192) || '-' || char(2193) || char(2274) || char(6158) || char(8203) || '-' || char(8207) || char(8234) || '-' || char(8238) || char(8288) || '-' || char(8292) || char(8294) || '-' || char(8303) || char(65279) || char(65529) || '-' || char(65531) || char(69821) || char(69837) || char(78896) || '-' || char(78911) || char(113824) || '-' || char(113827) || char(119155) || '-' || char(119162) || char(917505) || char(917536) || '-' || char(917631) || ']*') AND length(CAST(\"message\" AS BLOB)) <= 2048", + "name": "task_notification_outbox_message_check", + "entityType": "checks", + "table": "task_notification_outbox" + }, + { + "value": "(\"author_kind\" = 'user' AND length(\"author_id\") = 36 AND instr(\"author_id\", char(0)) = 0 AND length(replace(\"author_id\", '-', '')) = 32 AND replace(\"author_id\", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr(\"author_id\", 9, 1) = '-' AND substr(\"author_id\", 14, 1) = '-' AND substr(\"author_id\", 15, 1) = '7' AND substr(\"author_id\", 19, 1) = '-' AND substr(\"author_id\", 20, 1) GLOB '[89ab]' AND substr(\"author_id\", 24, 1) = '-') OR (\"author_kind\" = 'automation' AND length(\"author_id\") BETWEEN 1 AND 64 AND instr(\"author_id\", char(0)) = 0 AND \"author_id\" = lower(\"author_id\") AND substr(\"author_id\", 1, 1) GLOB '[a-z0-9]' AND \"author_id\" NOT GLOB '*[^a-z0-9._-]*')", + "name": "task_updates_author_check", + "entityType": "checks", + "table": "task_updates" + }, + { + "value": "length(\"id\") = 36 AND instr(\"id\", char(0)) = 0 AND length(replace(\"id\", '-', '')) = 32 AND replace(\"id\", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr(\"id\", 9, 1) = '-' AND substr(\"id\", 14, 1) = '-' AND substr(\"id\", 15, 1) = '7' AND substr(\"id\", 19, 1) = '-' AND substr(\"id\", 20, 1) GLOB '[89ab]' AND substr(\"id\", 24, 1) = '-'", + "name": "task_updates_id_check", + "entityType": "checks", + "table": "task_updates" + }, + { + "value": "length(\"message_markdown\") BETWEEN 1 AND 20000 AND instr(\"message_markdown\", char(0)) = 0 AND length(trim(\"message_markdown\", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0", + "name": "task_updates_message_markdown_check", + "entityType": "checks", + "table": "task_updates" + }, + { + "value": "length(\"task_id\") = 36 AND instr(\"task_id\", char(0)) = 0 AND length(replace(\"task_id\", '-', '')) = 32 AND replace(\"task_id\", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr(\"task_id\", 9, 1) = '-' AND substr(\"task_id\", 14, 1) = '-' AND substr(\"task_id\", 15, 1) = '7' AND substr(\"task_id\", 19, 1) = '-' AND substr(\"task_id\", 20, 1) GLOB '[89ab]' AND substr(\"task_id\", 24, 1) = '-'", + "name": "task_updates_task_id_check", + "entityType": "checks", + "table": "task_updates" + }, + { + "value": "\"created_at\" BETWEEN 0 AND 8640000000000000 AND \"updated_at\" BETWEEN 0 AND 8640000000000000 AND \"updated_at\" >= \"created_at\"", + "name": "task_updates_time_check", + "entityType": "checks", + "table": "task_updates" + }, + { + "value": "\"version\" BETWEEN 1 AND 9007199254740991", + "name": "task_updates_version_check", + "entityType": "checks", + "table": "task_updates" + }, + { + "value": "\"assignee\" IS NULL OR \"assignee\" IN ('mira-2026', 'rajohan')", + "name": "tasks_assignee_check", + "entityType": "checks", + "table": "tasks" + }, + { + "value": "\"body_markdown\" IS NULL OR (length(\"body_markdown\") BETWEEN 1 AND 100000 AND instr(\"body_markdown\", char(0)) = 0 AND length(trim(\"body_markdown\", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0)", + "name": "tasks_body_markdown_check", + "entityType": "checks", + "table": "tasks" + }, + { + "value": "length(\"id\") = 36 AND instr(\"id\", char(0)) = 0 AND length(replace(\"id\", '-', '')) = 32 AND replace(\"id\", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr(\"id\", 9, 1) = '-' AND substr(\"id\", 14, 1) = '-' AND substr(\"id\", 15, 1) = '7' AND substr(\"id\", 19, 1) = '-' AND substr(\"id\", 20, 1) GLOB '[89ab]' AND substr(\"id\", 24, 1) = '-'", + "name": "tasks_id_check", + "entityType": "checks", + "table": "tasks" + }, + { + "value": "\"priority\" IN ('low', 'medium', 'high')", + "name": "tasks_priority_check", + "entityType": "checks", + "table": "tasks" + }, + { + "value": "\"status\" IN ('todo', 'in-progress', 'blocked', 'done')", + "name": "tasks_status_check", + "entityType": "checks", + "table": "tasks" + }, + { + "value": "length(\"title\") BETWEEN 1 AND 240 AND instr(\"title\", char(0)) = 0 AND length(trim(\"title\", char(9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279))) > 0 AND \"title\" NOT GLOB ('*[' || char(1) || '-' || char(31) || char(127) || '-' || char(159) || char(173) || char(1536) || '-' || char(1541) || char(1564) || char(1757) || char(1807) || char(2192) || '-' || char(2193) || char(2274) || char(6158) || char(8203) || '-' || char(8207) || char(8234) || '-' || char(8238) || char(8288) || '-' || char(8292) || char(8294) || '-' || char(8303) || char(65279) || char(65529) || '-' || char(65531) || char(69821) || char(69837) || char(78896) || '-' || char(78911) || char(113824) || '-' || char(113827) || char(119155) || '-' || char(119162) || char(917505) || char(917536) || '-' || char(917631) || ']*')", + "name": "tasks_title_check", + "entityType": "checks", + "table": "tasks" + }, + { + "value": "\"created_at\" BETWEEN 0 AND 8640000000000000 AND \"updated_at\" BETWEEN 0 AND 8640000000000000 AND \"updated_at\" >= \"created_at\"", + "name": "tasks_time_check", + "entityType": "checks", + "table": "tasks" + }, + { + "value": "\"version\" BETWEEN 1 AND 9007199254740991", + "name": "tasks_version_check", + "entityType": "checks", + "table": "tasks" + }, { "value": "length(\"id\") = 36 AND instr(\"id\", char(0)) = 0 AND length(replace(\"id\", '-', '')) = 32 AND replace(\"id\", '-', '') NOT GLOB '*[^0-9a-f]*' AND substr(\"id\", 9, 1) = '-' AND substr(\"id\", 14, 1) = '-' AND substr(\"id\", 15, 1) = '7' AND substr(\"id\", 19, 1) = '-' AND substr(\"id\", 20, 1) GLOB '[89ab]' AND substr(\"id\", 24, 1) = '-'", "name": "user_recovery_codes_id_check", diff --git a/greenfield/package.json b/greenfield/package.json index 8593fce45..13785eb61 100644 --- a/greenfield/package.json +++ b/greenfield/package.json @@ -32,9 +32,7 @@ }, "dependencies": { "@daypicker/react": "10.0.1", - "@dnd-kit/core": "^6.3.1", "@dnd-kit/react": "^0.5.0", - "@dnd-kit/sortable": "^10.0.0", "@headlessui/react": "^2.2.10", "@microlink/react-json-view": "^1.31.28", "@simplewebauthn/browser": "13.3.0", diff --git a/greenfield/scripts/documentation/artifacts.test.ts b/greenfield/scripts/documentation/artifacts.test.ts index 438ad3bda..e39899ecd 100644 --- a/greenfield/scripts/documentation/artifacts.test.ts +++ b/greenfield/scripts/documentation/artifacts.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"; import { monitoringRealtimeTopics } from "../../src/contracts/monitoringRealtime.ts"; import { realtimeSubscriptionMaximumTopics } from "../../src/contracts/realtime.ts"; +import { taskRealtimeTopic } from "../../src/contracts/taskRealtime.ts"; import { buildDocumentationArtifacts } from "./artifacts.ts"; const packageManifest = { @@ -117,23 +118,32 @@ describe("generated contract documentation", () => { const realtimeInputSchema = JSON.parse( artifacts.get("schemas/events.stream.input.schema.json") ?? "null" ) as unknown; - expect(realtimeInputSchema).toMatchObject({ - additionalProperties: false, - properties: { - topics: { - items: { - enum: Object.values(monitoringRealtimeTopics), + expect(realtimeInputSchema).toEqual( + expect.objectContaining({ + additionalProperties: false, + properties: expect.objectContaining({ + lastEventId: expect.objectContaining({ + default: "0", type: "string", + }), + topics: { + items: { + enum: [ + ...Object.values(monitoringRealtimeTopics), + taskRealtimeTopic, + ], + type: "string", + }, + maxItems: realtimeSubscriptionMaximumTopics, + minItems: 1, + type: "array", + uniqueItems: true, }, - maxItems: realtimeSubscriptionMaximumTopics, - minItems: 1, - type: "array", - uniqueItems: true, - }, - }, - required: ["topics"], - type: "object", - }); + }), + required: ["topics"], + type: "object", + }) + ); expect(artifacts.get("packages-and-runtime.md")).toContain( "`@valibot/to-json-schema` | `1.7.1` | `1.7.1` | development" ); diff --git a/greenfield/scripts/documentation/jsonSchema.test.ts b/greenfield/scripts/documentation/jsonSchema.test.ts index 4b0d1a447..c1fcda725 100644 --- a/greenfield/scripts/documentation/jsonSchema.test.ts +++ b/greenfield/scripts/documentation/jsonSchema.test.ts @@ -22,6 +22,15 @@ import { } from "../../src/contracts/automationSecurity.ts"; import { applicationCapabilityListSchema } from "../../src/contracts/security.ts"; import { listSecurityAuditEventsResultSchema } from "../../src/contracts/securityAudit.ts"; +import { + taskDetailSchema, + taskLabelInputSchema, + taskTitleSchema, +} from "../../src/contracts/taskModel.ts"; +import { + listTasksResultSchema, + updateTaskInputSchema, +} from "../../src/contracts/tasks.ts"; import { webAuthnAuthenticationResponseSchema, webAuthnTransportListSchema, @@ -90,8 +99,10 @@ describe("contract JSON Schema conversion", () => { "output" ) ).toMatchObject({ - items: { enum: ["notifications:read", "reports:read"] }, - maxItems: 2, + items: { + enum: ["notifications:read", "reports:read", "tasks:read", "tasks:write"], + }, + maxItems: 4, type: "array", uniqueItems: true, }); @@ -214,6 +225,38 @@ describe("contract JSON Schema conversion", () => { expect(document).toContain("audit continuation cursor"); }); + test("documents task bounds, canonicalization, and runtime relationships", () => { + const titleDocument = JSON.stringify( + convertContractSchema(taskTitleSchema, "test.taskTitle", "input") + ); + expect(titleDocument).toContain("canonical outer whitespace"); + expect(titleDocument).toContain("maxLength"); + + expect( + convertContractSchema(taskLabelInputSchema, "test.taskLabels", "input") + ).toMatchObject({ + maxItems: 20, + type: "array", + uniqueItems: true, + }); + + expect( + convertContractSchema(updateTaskInputSchema, "test.updateTask", "input") + ).toMatchObject({ + properties: { patch: { minProperties: 1 } }, + }); + + const detailDocument = JSON.stringify( + convertContractSchema(taskDetailSchema, "test.taskDetail", "output") + ); + expect(detailDocument).toContain("timestamps not to precede creation"); + const pageDocument = JSON.stringify( + convertContractSchema(listTasksResultSchema, "test.taskPage", "output") + ); + expect(pageDocument).toContain("strict newest-first task ordering"); + expect(pageDocument).toContain("task continuation cursor"); + }); + test("documents the exact TOTP factor-label predicate", () => { const document = convertContractSchema( totpFactorLabelSchema, diff --git a/greenfield/scripts/documentation/jsonSchema.ts b/greenfield/scripts/documentation/jsonSchema.ts index a19037652..6bf190142 100644 --- a/greenfield/scripts/documentation/jsonSchema.ts +++ b/greenfield/scripts/documentation/jsonSchema.ts @@ -35,6 +35,22 @@ import { securityAuditEventsHaveStableOrder, securityAuditPageCursorIsConsistent, } from "../../src/contracts/securityAudit.ts"; +import { + canonicalizeTaskStrings, + freezeTaskStrings, + taskDetailTimesAreOrdered, + taskProgressTimesAreOrdered, + taskStringsAreSorted, + taskTextIsTrimmed, + taskTimesAreOrdered, +} from "../../src/contracts/taskModel.ts"; +import { + newestProgressOrderIsStable, + newestTaskOrderIsStable, + taskPageCursorIsConsistent, + taskPatchHasChange, + taskProgressPageCursorIsConsistent, +} from "../../src/contracts/tasks.ts"; import { hasMatchingWebAuthnAuthenticationCredentialIds, hasMatchingWebAuthnRegistrationCredentialIds, @@ -44,6 +60,7 @@ import { import { getBoundedNonBlankTextMaximumLength, hasNoNulCharacter, + hasNoUnicodeControlOrFormat, hasUniqueArrayItems, } from "../../src/shared/validation.ts"; @@ -62,6 +79,7 @@ const securityLabelControlOrFormatPattern = [ String.raw`\uDB40(?:\uDC01|[\uDC20-\uDC7F])`, ].join("|"); const securityLabelJsonSchemaPattern = `^(?=[\\s\\S]*\\S)(?![\\s\\S]*(?:${securityLabelControlOrFormatPattern}))[\\s\\S]+$`; +const controlSafeTextJsonSchemaPattern = `^(?![\\s\\S]*(?:${securityLabelControlOrFormatPattern}))[\\s\\S]*$`; const noNulJsonSchemaPattern = String.raw`^[^\u0000]*$`; const runtimeCheckComments = new Map([ @@ -133,6 +151,42 @@ const runtimeCheckComments = new Map([ securityAuditPageCursorIsConsistent, "Live Valibot validation additionally requires an audit continuation cursor to identify the returned last event.", ], + [ + taskTextIsTrimmed, + "Live Valibot validation additionally requires compact task text to have canonical outer whitespace.", + ], + [ + taskStringsAreSorted, + "Live Valibot validation additionally requires task strings to use canonical code-unit order.", + ], + [ + taskTimesAreOrdered, + "Live Valibot validation additionally requires task update timestamps not to precede creation.", + ], + [ + taskDetailTimesAreOrdered, + "Live Valibot validation additionally requires task update timestamps not to precede creation.", + ], + [ + taskProgressTimesAreOrdered, + "Live Valibot validation additionally requires progress update timestamps not to precede creation.", + ], + [ + newestTaskOrderIsStable, + "Live Valibot validation additionally requires strict newest-first task ordering by update timestamp and ID.", + ], + [ + taskPageCursorIsConsistent, + "Live Valibot validation additionally requires a task continuation cursor to identify the returned last row.", + ], + [ + newestProgressOrderIsStable, + "Live Valibot validation additionally requires strict newest-first progress ordering by creation timestamp and ID.", + ], + [ + taskProgressPageCursorIsConsistent, + "Live Valibot validation additionally requires a progress continuation cursor to identify the returned last row.", + ], ]); function appendJsonSchemaComment( @@ -259,6 +313,21 @@ export function convertContractSchema( if (valibotAction.type === "check" && requirement === hasNoNulCharacter) { return appendJsonSchemaPattern(jsonSchema, noNulJsonSchemaPattern); } + if ( + valibotAction.type === "check" && + requirement === hasNoUnicodeControlOrFormat + ) { + return appendJsonSchemaPattern( + jsonSchema, + controlSafeTextJsonSchemaPattern + ); + } + if ( + valibotAction.type === "check" && + requirement === taskPatchHasChange + ) { + return { ...jsonSchema, minProperties: 1 }; + } if (valibotAction.type === "check") { const maximumLength = getBoundedNonBlankTextMaximumLength(requirement); @@ -315,7 +384,9 @@ export function convertContractSchema( if ( valibotAction.type === "transform" && (operation === sortWebAuthnTransports || - operation === sortApplicationCapabilities) + operation === sortApplicationCapabilities || + operation === canonicalizeTaskStrings || + operation === freezeTaskStrings) ) { return jsonSchema; } diff --git a/greenfield/src/app/dashboardServer.ts b/greenfield/src/app/dashboardServer.ts index d07f533ca..4c290b0b2 100644 --- a/greenfield/src/app/dashboardServer.ts +++ b/greenfield/src/app/dashboardServer.ts @@ -29,6 +29,8 @@ import { createRequestAuthenticator } from "../server/domains/security/requestAu import { createRequestAuthenticationRepository } from "../server/domains/security/requestAuthenticationRepository.ts"; import { createSecurityAuditLifecycleService } from "../server/domains/security/securityAuditLifecycle.ts"; import { createSecurityAuditLifecycleRepository } from "../server/domains/security/securityAuditLifecycleRepository.ts"; +import { createTaskRepository } from "../server/domains/tasks/repository.ts"; +import { createTaskService } from "../server/domains/tasks/service.ts"; import { type WebConfiguration, parseWebConfiguration, @@ -79,6 +81,7 @@ export interface DashboardServerOptions extends Omit< | "mfaAccountLifecycle" | "mfaLoginLifecycle" | "securityAuditLifecycle" + | "taskService" > { readonly applicationRuntime: DashboardApplicationRuntime; readonly authenticationLeaseDurationMs?: number; @@ -239,6 +242,13 @@ export async function createDashboardServer( repository: createSecurityAuditLifecycleRepository(database), sessionIdleDurationMs: options.sessionIdleDurationMs, }); + const taskNow = options.now; + const taskService = createTaskService({ + ...(taskNow === undefined ? {} : { nowMs: () => taskNow().getTime() }), + repository: createTaskRepository(database, databaseRuntime), + wakeEventPump: () => + options.applicationRuntime.services.realtimeEvents.wake(), + }); const serverOptions: ServerOptions = { applicationRuntime: options.applicationRuntime, authenticateCredential: (credential) => @@ -254,6 +264,7 @@ export async function createDashboardServer( port: options.port, readiness: options.readiness, securityAuditLifecycle, + taskService, trustedProxyAddresses: options.trustedProxyAddresses, }; serverOwnsRuntimeCleanup = true; diff --git a/greenfield/src/app/server.ts b/greenfield/src/app/server.ts index 39ad205eb..726f9898b 100644 --- a/greenfield/src/app/server.ts +++ b/greenfield/src/app/server.ts @@ -7,6 +7,7 @@ import type { AutomationSecurityLifecycleService } from "../server/domains/secur import type { MfaAccountLifecycleService } from "../server/domains/security/mfa/accountLifecycle.ts"; import type { MfaLoginLifecycleService } from "../server/domains/security/mfa/loginLifecycle.ts"; import type { SecurityAuditLifecycleService } from "../server/domains/security/securityAuditLifecycle.ts"; +import type { TaskService } from "../server/domains/tasks/service.ts"; import type { ReadinessController } from "../server/platform/readiness/readinessState.ts"; import type { ApplicationRuntime } from "../server/platform/runtime/applicationRuntime.ts"; import { readRuntimeIdentity } from "../server/platform/runtime/readRuntimeIdentity.ts"; @@ -136,6 +137,7 @@ export interface ServerOptions { readonly port: number; readonly readiness: ReadinessController; readonly securityAuditLifecycle: SecurityAuditLifecycleService; + readonly taskService: TaskService["Service"]; /** Exact proxy peers allowed to supply one overwritten client address. */ readonly trustedProxyAddresses?: readonly string[]; } @@ -173,6 +175,7 @@ export async function createServer(options: ServerOptions): Promise { + this.unsubscribeCount += 1; + }, + }; + } + + emit(value: unknown): void { + this.observer?.onData(value); + } + + fail(error: unknown): void { + this.observer?.onError(error); + } +} + +describe("Dashboard realtime client", () => { + test("validates task subscription input and tracked outputs", () => { + const transport = new ControlledRealtimeTransport(); + const client = createDashboardRealtimeClient(transport); + const outputs: RealtimeStreamOutput[] = []; + const subscription = client.subscribe( + { lastEventId: "0", topics: [taskRealtimeTopic] }, + { onData: (output) => outputs.push(output) } + ); + + expect(transport.path).toBe("events.stream"); + expect(transport.input).toEqual({ + lastEventId: "0", + topics: [taskRealtimeTopic], + }); + transport.emit(taskChange); + expect(outputs).toEqual([taskChange]); + subscription.unsubscribe(); + subscription.unsubscribe(); + expect(transport.unsubscribeCount).toBe(1); + }); + + test("closes malformed streams and redacts transport failures", () => { + const malformedTransport = new ControlledRealtimeTransport(); + const failures: DashboardProtocolError[] = []; + createDashboardRealtimeClient(malformedTransport).subscribe( + { lastEventId: "0", topics: [taskRealtimeTopic] }, + { + onData: () => { + throw new TypeError("Malformed output reached the application"); + }, + onError: (error) => failures.push(error), + } + ); + malformedTransport.emit({ data: { kind: "change" }, id: "private" }); + expect(malformedTransport.unsubscribeCount).toBe(1); + expect(failures).toEqual([new DashboardProtocolError()]); + + const failedTransport = new ControlledRealtimeTransport(); + createDashboardRealtimeClient(failedTransport).subscribe( + { lastEventId: "0", topics: [taskRealtimeTopic] }, + { onData: () => {}, onError: (error) => failures.push(error) } + ); + failedTransport.fail(new Error("private upstream failure")); + failedTransport.fail(new Error("late private upstream failure")); + expect(failures.at(-1)).toEqual(new DashboardProtocolError()); + expect(failures).toHaveLength(2); + expect(failedTransport.unsubscribeCount).toBe(1); + }); +}); diff --git a/greenfield/src/browser/api/realtimeClient.ts b/greenfield/src/browser/api/realtimeClient.ts new file mode 100644 index 000000000..e5c15b764 --- /dev/null +++ b/greenfield/src/browser/api/realtimeClient.ts @@ -0,0 +1,124 @@ +import { createTRPCUntypedClient, httpSubscriptionLink } from "@trpc/client"; +import superjson from "superjson"; +import * as v from "valibot"; + +import { + type RealtimeStreamInput, + type RealtimeStreamOutput, + realtimeStreamInputSchema, + realtimeStreamOutputSchema, +} from "../../contracts/events.ts"; +import { DashboardProtocolError } from "./trpcClient.ts"; + +export interface DashboardRealtimeTransportObserver { + readonly onData: (value: unknown) => void; + readonly onError: (error: unknown) => void; +} + +/** Unsubscribes one browser-owned realtime stream. */ +export interface DashboardRealtimeSubscription { + readonly unsubscribe: () => void; +} + +/** Minimal long-lived tRPC authority retained behind contract validation. */ +export interface DashboardRealtimeTransport { + readonly subscription: ( + path: string, + input: unknown, + observer: DashboardRealtimeTransportObserver + ) => DashboardRealtimeSubscription; +} + +/** Validated callbacks for one durable Dashboard event stream. */ +export interface DashboardRealtimeObserver { + readonly onData: (output: RealtimeStreamOutput) => void; + /** Receives only terminal transport or protocol failures. */ + readonly onError?: (error: DashboardProtocolError) => void; +} + +/** Browser client for contract-validated tracked SSE. */ +export interface DashboardRealtimeClient { + readonly subscribe: ( + input: RealtimeStreamInput, + observer: DashboardRealtimeObserver + ) => DashboardRealtimeSubscription; +} + +/** + * Creates the same-origin tRPC tracked-SSE transport. + * @param url Same-origin tRPC mount. + * @returns One reconnecting subscription transport with cookie credentials. + */ +export function createDashboardRealtimeTransport( + url = "/trpc" +): DashboardRealtimeTransport { + const client = createTRPCUntypedClient({ + links: [ + httpSubscriptionLink({ + eventSourceOptions: { withCredentials: true }, + transformer: superjson, + url, + }), + ], + }); + const transport: DashboardRealtimeTransport = { + subscription(path, input, observer) { + return client.subscription(path, input, { + onData: observer.onData, + onError: observer.onError, + }); + }, + }; + return Object.freeze(transport); +} + +/** + * Creates a validating browser client over the tracked-SSE transport. + * @param transport Injected production or isolated transport. + * @returns Contract-validated realtime subscription operations. + */ +export function createDashboardRealtimeClient( + transport: DashboardRealtimeTransport = createDashboardRealtimeTransport() +): DashboardRealtimeClient { + const client: DashboardRealtimeClient = { + subscribe(input, observer) { + const parsedInput = v.parse(realtimeStreamInputSchema, input); + let active = true; + const transportState: { + subscription?: DashboardRealtimeSubscription; + } = {}; + const close = () => { + if (!active) return; + active = false; + transportState.subscription?.unsubscribe(); + }; + const transportSubscription = transport.subscription( + "events.stream", + parsedInput, + { + onData(value) { + if (!active) return; + const parsed = v.safeParse(realtimeStreamOutputSchema, value); + if (!parsed.success) { + close(); + observer.onError?.(new DashboardProtocolError()); + return; + } + observer.onData(parsed.output); + }, + onError() { + if (!active) return; + close(); + observer.onError?.(new DashboardProtocolError()); + }, + } + ); + transportState.subscription = transportSubscription; + if (!active) transportSubscription.unsubscribe(); + return Object.freeze({ + unsubscribe: close, + }); + }, + }; + return Object.freeze(client); +} diff --git a/greenfield/src/browser/api/realtimeContext.tsx b/greenfield/src/browser/api/realtimeContext.tsx new file mode 100644 index 000000000..01d0adab4 --- /dev/null +++ b/greenfield/src/browser/api/realtimeContext.tsx @@ -0,0 +1,28 @@ +import { useEffect, useState, type ReactNode } from "react"; + +import type { DashboardRealtimeClient } from "./realtimeClient.ts"; +import { dashboardRealtimeContext } from "./realtimeContextValue.ts"; +import { createDashboardRealtimeHub } from "./realtimeHub.ts"; + +interface DashboardRealtimeProviderProps { + readonly children: ReactNode; + readonly client: DashboardRealtimeClient; +} + +/** @returns Browser realtime client context for feature-level subscriptions. */ +export function DashboardRealtimeProvider({ + children, + client, +}: DashboardRealtimeProviderProps) { + const [hub] = useState(() => createDashboardRealtimeHub(client)); + useEffect(() => { + hub.resume(); + return () => hub.pause(); + }, [hub]); + + return ( + + {children} + + ); +} diff --git a/greenfield/src/browser/api/realtimeContextValue.ts b/greenfield/src/browser/api/realtimeContextValue.ts new file mode 100644 index 000000000..1bb16be8c --- /dev/null +++ b/greenfield/src/browser/api/realtimeContextValue.ts @@ -0,0 +1,20 @@ +import { createContext, use } from "react"; + +import type { DashboardRealtimeHub } from "./realtimeHub.ts"; + +/** Internal context shared by the realtime provider and feature hooks. */ +export const dashboardRealtimeContext = createContext( + undefined +); + +/** + * Reads the browser-owned realtime client. + * @returns The configured tracked-SSE client. + */ +export function useDashboardRealtimeHub(): DashboardRealtimeHub { + const hub = use(dashboardRealtimeContext); + if (hub === undefined) { + throw new TypeError("Dashboard realtime provider is missing"); + } + return hub; +} diff --git a/greenfield/src/browser/api/realtimeHub.test.ts b/greenfield/src/browser/api/realtimeHub.test.ts new file mode 100644 index 000000000..2e1b0056e --- /dev/null +++ b/greenfield/src/browser/api/realtimeHub.test.ts @@ -0,0 +1,149 @@ +import { describe, expect, test } from "bun:test"; + +import type { + RealtimeStreamInput, + RealtimeStreamOutput, +} from "../../contracts/events.ts"; +import { monitoringRealtimeTopics } from "../../contracts/monitoringRealtime.ts"; +import { taskRealtimeTopic } from "../../contracts/taskRealtime.ts"; +import type { + DashboardRealtimeClient, + DashboardRealtimeObserver, +} from "./realtimeClient.ts"; +import { createDashboardRealtimeHub } from "./realtimeHub.ts"; + +interface ControlledSubscription { + readonly input: RealtimeStreamInput; + readonly observer: DashboardRealtimeObserver; + unsubscribeCount: number; +} + +class ControlledRealtimeClient implements DashboardRealtimeClient { + readonly subscriptions: ControlledSubscription[] = []; + + subscribe(input: RealtimeStreamInput, observer: DashboardRealtimeObserver) { + const subscription: ControlledSubscription = { + input, + observer, + unsubscribeCount: 0, + }; + this.subscriptions.push(subscription); + return { + unsubscribe: () => { + subscription.unsubscribeCount += 1; + }, + }; + } +} + +function change( + id: string, + topic: typeof taskRealtimeTopic | typeof monitoringRealtimeTopics.reports +): RealtimeStreamOutput { + if (topic === taskRealtimeTopic) { + return { + data: { + event: { + entityId: "019fd984-63e8-7404-a7da-80c6f243794f", + entityType: "task", + occurredAtMs: 1_800_000_000_000, + operation: "updated", + payload: { id: "019fd984-63e8-7404-a7da-80c6f243794f" }, + topic, + }, + kind: "change", + }, + id, + }; + } + return { + data: { + event: { + entityId: "report:daily", + entityType: "report", + occurredAtMs: 1_800_000_000_000, + operation: "created", + payload: { id: "report:daily" }, + topic, + }, + kind: "change", + }, + id, + }; +} + +describe("Dashboard realtime hub", () => { + test("shares one resumable stream and routes only matching topics", () => { + const client = new ControlledRealtimeClient(); + const hub = createDashboardRealtimeHub(client); + const taskOutputs: RealtimeStreamOutput[] = []; + const reportOutputs: RealtimeStreamOutput[] = []; + const tasks = hub.subscribe([taskRealtimeTopic], { + onData: (output) => taskOutputs.push(output), + }); + const first = client.subscriptions[0]!; + const reports = hub.subscribe([monitoringRealtimeTopics.reports], { + onData: (output) => reportOutputs.push(output), + }); + const combined = client.subscriptions[1]!; + + expect(first.unsubscribeCount).toBe(1); + expect(combined.input).toEqual({ + lastEventId: "0", + topics: [monitoringRealtimeTopics.reports, taskRealtimeTopic], + }); + combined.observer.onData(change("11", taskRealtimeTopic)); + combined.observer.onData(change("12", monitoringRealtimeTopics.reports)); + expect(taskOutputs.map(({ id }) => id)).toEqual(["11"]); + expect(reportOutputs.map(({ id }) => id)).toEqual(["12"]); + + reports.unsubscribe(); + const tasksOnly = client.subscriptions[2]!; + expect(combined.unsubscribeCount).toBe(1); + expect(tasksOnly.input).toEqual({ + lastEventId: "12", + topics: [taskRealtimeTopic], + }); + tasks.unsubscribe(); + expect(tasksOnly.unsubscribeCount).toBe(1); + hub.dispose(); + }); + + test("broadcasts resync and failures while disposing idempotently", () => { + const client = new ControlledRealtimeClient(); + const hub = createDashboardRealtimeHub(client); + const outputs: RealtimeStreamOutput[] = []; + const failures: Error[] = []; + hub.subscribe([taskRealtimeTopic], { + onData: (output) => outputs.push(output), + onError: (error) => failures.push(error), + }); + const subscription = client.subscriptions[0]!; + const resync: RealtimeStreamOutput = { + data: { + kind: "resync-required", + reason: "cursor-outside-retention", + }, + id: "20", + }; + const failure = new Error("redacted protocol failure"); + + subscription.observer.onData(resync); + subscription.observer.onError?.(failure); + expect(outputs).toEqual([resync]); + expect(failures).toEqual([failure]); + hub.pause(); + expect(subscription.unsubscribeCount).toBe(1); + hub.resume(); + expect(client.subscriptions[1]?.input).toEqual({ + lastEventId: "20", + topics: [taskRealtimeTopic], + }); + hub.dispose(); + hub.dispose(); + expect(client.subscriptions[1]?.unsubscribeCount).toBe(1); + expect(() => hub.subscribe([taskRealtimeTopic], { onData: () => {} })).toThrow( + "Dashboard realtime hub is disposed" + ); + }); +}); diff --git a/greenfield/src/browser/api/realtimeHub.ts b/greenfield/src/browser/api/realtimeHub.ts new file mode 100644 index 000000000..8d473651b --- /dev/null +++ b/greenfield/src/browser/api/realtimeHub.ts @@ -0,0 +1,163 @@ +import * as v from "valibot"; + +import { + type RealtimeStreamInput, + type RealtimeStreamOutput, + realtimeStreamInputSchema, +} from "../../contracts/events.ts"; +import type { + DashboardRealtimeClient, + DashboardRealtimeObserver, + DashboardRealtimeSubscription, +} from "./realtimeClient.ts"; + +export type DashboardRealtimeTopic = RealtimeStreamInput["topics"][number]; + +/** Shared feature-facing subscription authority for one browser tab. */ +export interface DashboardRealtimeHub { + readonly dispose: () => void; + readonly pause: () => void; + readonly resume: () => void; + readonly subscribe: ( + topics: readonly DashboardRealtimeTopic[], + observer: DashboardRealtimeObserver + ) => DashboardRealtimeSubscription; +} + +interface RealtimeHubListener { + readonly observer: DashboardRealtimeObserver; + readonly topics: ReadonlySet; +} + +function listenerAcceptsOutput( + listener: RealtimeHubListener, + output: RealtimeStreamOutput +): boolean { + return ( + output.data.kind === "resync-required" || + listener.topics.has(output.data.event.topic) + ); +} + +/** + * Multiplexes feature topic listeners over one resumable tRPC SSE subscription. + * @param client Validating browser realtime client. + * @returns One tab-local topic hub with a shared durable cursor. + */ +export function createDashboardRealtimeHub( + client: DashboardRealtimeClient +): DashboardRealtimeHub { + const listeners = new Map(); + let activeSubscription: DashboardRealtimeSubscription | undefined; + let cursor = "0"; + let disposed = false; + let generation = 0; + let paused = false; + + function snapshotListeners(): RealtimeHubListener[] { + const snapshot: RealtimeHubListener[] = []; + for (const listener of listeners.values()) snapshot.push(listener); + return snapshot; + } + + function restartSubscription(): void { + generation += 1; + const currentGeneration = generation; + activeSubscription?.unsubscribe(); + activeSubscription = undefined; + if (listeners.size === 0 || disposed || paused) return; + + const topics = [ + ...new Set( + [...listeners.values()].flatMap((listener) => [...listener.topics]) + ), + ].toSorted(); + const input = v.parse(realtimeStreamInputSchema, { + lastEventId: cursor, + topics, + }); + const subscriptionState: { + terminal: boolean; + value?: DashboardRealtimeSubscription; + } = { terminal: false }; + const subscription = client.subscribe(input, { + onData(output) { + if (disposed || currentGeneration !== generation) return; + cursor = output.id; + for (const listener of snapshotListeners()) { + if (listenerAcceptsOutput(listener, output)) { + listener.observer.onData(output); + } + } + }, + onError(error) { + if (disposed || currentGeneration !== generation) return; + subscriptionState.terminal = true; + if (activeSubscription === subscriptionState.value) { + activeSubscription = undefined; + } + subscriptionState.value?.unsubscribe(); + for (const listener of snapshotListeners()) { + listener.observer.onError?.(error); + } + }, + }); + subscriptionState.value = subscription; + if (disposed || currentGeneration !== generation || subscriptionState.terminal) { + subscription.unsubscribe(); + return; + } + activeSubscription = subscription; + } + + const hub: DashboardRealtimeHub = { + dispose() { + if (disposed) return; + paused = true; + disposed = true; + generation += 1; + listeners.clear(); + activeSubscription?.unsubscribe(); + activeSubscription = undefined; + }, + pause() { + if (disposed || paused) return; + paused = true; + generation += 1; + activeSubscription?.unsubscribe(); + activeSubscription = undefined; + }, + resume() { + if (disposed) { + throw new TypeError("Dashboard realtime hub is disposed"); + } + if (!paused) return; + paused = false; + restartSubscription(); + }, + subscribe(topics, observer) { + if (disposed) { + throw new TypeError("Dashboard realtime hub is disposed"); + } + const parsed = v.parse(realtimeStreamInputSchema, { + topics: [...topics], + }); + const id = Symbol("dashboard-realtime-listener"); + listeners.set(id, { + observer, + topics: new Set(parsed.topics), + }); + restartSubscription(); + let active = true; + return Object.freeze({ + unsubscribe() { + if (!active) return; + active = false; + listeners.delete(id); + restartSubscription(); + }, + }); + }, + }; + return Object.freeze(hub); +} diff --git a/greenfield/src/browser/api/trpcClient.ts b/greenfield/src/browser/api/trpcClient.ts index 0ec79722f..3dceb71d8 100644 --- a/greenfield/src/browser/api/trpcClient.ts +++ b/greenfield/src/browser/api/trpcClient.ts @@ -88,6 +88,10 @@ async function procedureContractsFor( const module = await import("../../contracts/system.ts"); return module.systemProcedureContracts; } + case "tasks": { + const module = await import("../../contracts/tasks.ts"); + return module.taskProcedureContracts; + } default: { throw new DashboardProtocolError(); } diff --git a/greenfield/src/browser/application.test.tsx b/greenfield/src/browser/application.test.tsx index 1dab86e7d..d6e0f0b03 100644 --- a/greenfield/src/browser/application.test.tsx +++ b/greenfield/src/browser/application.test.tsx @@ -8,6 +8,7 @@ import { DashboardBrowserApplication } from "./application.tsx"; import { authStatusQueryKey } from "./auth/authQueries.ts"; import { createDashboardRouter } from "./router.tsx"; import type { DashboardWebAuthnClient } from "./security/webauthn/webauthnClient.ts"; +import { noOpDashboardRealtimeClient } from "./test/realtime.ts"; const { render, screen, waitFor } = await import("@testing-library/react"); const unexpectedWebAuthnClient: DashboardWebAuthnClient = Object.freeze({ @@ -58,6 +59,7 @@ describe("Dashboard browser application", () => { render( - - - - - - + + + + + + + + ); @@ -60,6 +70,7 @@ export default function DashboardBrowserApplicationRoot() { return ( { render( ; diff --git a/greenfield/src/browser/router.tsx b/greenfield/src/browser/router.tsx index e5a909c80..9f2ec1c02 100644 --- a/greenfield/src/browser/router.tsx +++ b/greenfield/src/browser/router.tsx @@ -21,10 +21,15 @@ const accountSecurityRoute = createRoute({ getParentRoute: () => rootRoute, path: "/account-security", }).lazy(() => import("./routes/accountSecurity.lazy.tsx").then((module) => module.Route)); +const tasksRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/tasks", +}).lazy(() => import("./routes/tasks.lazy.tsx").then((module) => module.Route)); const routeTree = rootRoute.addChildren([ overviewRoute, loginRoute, accountSecurityRoute, + tasksRoute, ]); /** diff --git a/greenfield/src/browser/routes/tasks.lazy.tsx b/greenfield/src/browser/routes/tasks.lazy.tsx new file mode 100644 index 000000000..5b1eb87d2 --- /dev/null +++ b/greenfield/src/browser/routes/tasks.lazy.tsx @@ -0,0 +1,14 @@ +import { createLazyRoute } from "@tanstack/react-router"; + +import { AuthenticationBoundary } from "../auth/AuthenticationBoundary.tsx"; +import { TaskBoardRoute } from "../tasks/TaskBoardRoute.tsx"; + +export const Route = createLazyRoute("/tasks")({ + component: function TasksRouteBoundary() { + return ( + + + + ); + }, +}); diff --git a/greenfield/src/browser/security/AccountSecurityRoute.test.tsx b/greenfield/src/browser/security/AccountSecurityRoute.test.tsx index 463616875..e1d2fc418 100644 --- a/greenfield/src/browser/security/AccountSecurityRoute.test.tsx +++ b/greenfield/src/browser/security/AccountSecurityRoute.test.tsx @@ -26,6 +26,7 @@ import { } from "../api/trpcClient.ts"; import { DashboardBrowserApplication } from "../application.tsx"; import { createDashboardRouter } from "../router.tsx"; +import { noOpDashboardRealtimeClient } from "../test/realtime.ts"; import type { DashboardWebAuthnClient } from "./webauthn/webauthnClient.ts"; const { render, screen, waitFor } = await import("@testing-library/react"); @@ -264,6 +265,7 @@ function renderAccountSecurity( render( void; + readonly onCreated: (task: TaskDetail) => void; + readonly open: boolean; +} + +/** @returns Task-creation dialog over the shared task editor. */ +export function NewTaskModal({ onClose, onCreated, open }: NewTaskModalProps) { + return ( + + + + ); +} diff --git a/greenfield/src/browser/tasks/TaskAutomationFields.tsx b/greenfield/src/browser/tasks/TaskAutomationFields.tsx new file mode 100644 index 000000000..11ed3c1f5 --- /dev/null +++ b/greenfield/src/browser/tasks/TaskAutomationFields.tsx @@ -0,0 +1,175 @@ +import { Bot } from "lucide-react"; + +import { Checkbox } from "../ui/Checkbox.tsx"; +import { ExpandableCard } from "../ui/ExpandableCard.tsx"; +import { firstFormFieldError } from "../ui/formErrors.ts"; +import { FormField } from "../ui/FormField.tsx"; +import { Input } from "../ui/Input.tsx"; +import type { TaskEditorFormApi } from "./useTaskEditorController.ts"; + +interface TaskAutomationFieldsProps { + readonly busy: boolean; + readonly form: TaskEditorFormApi; +} + +/** @returns Optional OpenClaw automation relationship fields. */ +export function TaskAutomationFields({ busy, form }: TaskAutomationFieldsProps) { + return ( + state.values.automationEnabled}> + {(automationEnabled) => ( + + + {(field) => ( + + )} + + {automationEnabled && ( +
+ + {(field) => ( + + + field.handleChange( + event.currentTarget.value + ) + } + required + value={field.state.value} + /> + + )} + + + {(field) => ( + + + field.handleChange( + event.currentTarget.value + ) + } + placeholder="Every weekday at 08:00" + value={field.state.value} + /> + + )} + + + {(field) => ( + + + field.handleChange( + event.currentTarget.value + ) + } + value={field.state.value} + /> + + )} + + + {(field) => ( + + + field.handleChange( + event.currentTarget.value + ) + } + value={field.state.value} + /> + + )} + + + {(field) => ( + + + field.handleChange( + event.currentTarget.value + ) + } + value={field.state.value} + /> + + )} + + + {(field) => ( + + )} + +
+ )} +
+ )} +
+ ); +} diff --git a/greenfield/src/browser/tasks/TaskBoard.test.ts b/greenfield/src/browser/tasks/TaskBoard.test.ts new file mode 100644 index 000000000..64998b971 --- /dev/null +++ b/greenfield/src/browser/tasks/TaskBoard.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, test } from "bun:test"; + +import type { TaskSummary } from "../../contracts/taskModel.ts"; +import { taskMoveInputForDrop } from "./taskBoardDrop.ts"; + +const task: TaskSummary = Object.freeze({ + assignee: "mira-2026", + createdAtMs: 1_800_000_000_000, + id: "019fd984-63e8-7404-a7da-80c6f243794f", + labels: [], + priority: "high", + status: "todo", + title: "Move task", + updatedAtMs: 1_800_000_000_000, + version: 3, +}); + +describe("task board drop mapping", () => { + test("moves only a known versioned task to a different status", () => { + expect( + taskMoveInputForDrop([task], `task:${task.id}`, "task-column:in-progress") + ).toEqual({ + expectedVersion: 3, + id: task.id, + status: "in-progress", + }); + expect( + taskMoveInputForDrop([task], `task:${task.id}`, "task-column:todo") + ).toBeUndefined(); + expect( + taskMoveInputForDrop([task], "task:missing", "task-column:blocked") + ).toBeUndefined(); + expect( + taskMoveInputForDrop([task], `task:${task.id}`, "task-column:unknown") + ).toBeUndefined(); + }); +}); diff --git a/greenfield/src/browser/tasks/TaskBoard.tsx b/greenfield/src/browser/tasks/TaskBoard.tsx new file mode 100644 index 000000000..78f989b8c --- /dev/null +++ b/greenfield/src/browser/tasks/TaskBoard.tsx @@ -0,0 +1,73 @@ +import { DragDropProvider, DragOverlay } from "@dnd-kit/react"; + +import type { TaskStatus, TaskSummary } from "../../contracts/taskModel.ts"; +import type { MoveTaskInput } from "../../contracts/tasks.ts"; +import { taskMoveInputForDrop } from "./taskBoardDrop.ts"; +import { TaskCardContent } from "./TaskCard.tsx"; +import { TaskColumn } from "./TaskColumn.tsx"; +import { taskStatusDefinitions } from "./taskPresentation.ts"; + +const taskPriorityRank = Object.freeze({ high: 0, low: 2, medium: 1 }); + +function compareTasks(left: TaskSummary, right: TaskSummary): number { + if (left.status !== "done") { + const priorityDifference = + taskPriorityRank[left.priority] - taskPriorityRank[right.priority]; + if (priorityDifference !== 0) return priorityDifference; + } + const timeDifference = right.updatedAtMs - left.updatedAtMs; + return timeDifference === 0 ? right.id.localeCompare(left.id) : timeDifference; +} + +interface TaskBoardProps { + readonly disabled: boolean; + readonly onMoveTask: (input: MoveTaskInput) => void; + readonly onSelectTask: (taskId: string) => void; + readonly tasks: readonly TaskSummary[]; +} + +/** @returns Four-column task board with accessible dnd-kit movement. */ +export function TaskBoard({ disabled, onMoveTask, onSelectTask, tasks }: TaskBoardProps) { + const tasksByStatus = Object.fromEntries( + taskStatusDefinitions.map(({ status }) => [ + status, + tasks.filter((task) => task.status === status).toSorted(compareTasks), + ]) + ) as Record; + + return ( + { + if (canceled) return; + const input = taskMoveInputForDrop( + tasks, + operation.source?.id, + operation.target?.id + ); + if (input !== undefined) onMoveTask(input); + }} + > +
+ {taskStatusDefinitions.map(({ status }) => ( + + ))} +
+ + {(source) => { + const task = tasks.find( + (candidate) => `task:${candidate.id}` === source.id + ); + return task === undefined ? null : ( + + ); + }} + +
+ ); +} diff --git a/greenfield/src/browser/tasks/TaskBoardRoute.test.tsx b/greenfield/src/browser/tasks/TaskBoardRoute.test.tsx new file mode 100644 index 000000000..5d0c96b5a --- /dev/null +++ b/greenfield/src/browser/tasks/TaskBoardRoute.test.tsx @@ -0,0 +1,340 @@ +import { afterEach, describe, expect, test } from "bun:test"; + +import { createMemoryHistory } from "@tanstack/react-router"; +import { act } from "react"; +import * as v from "valibot"; + +import type { AuthStatus } from "../../contracts/auth.ts"; +import type { + TaskDetail, + TaskProgressUpdate, + TaskSummary, +} from "../../contracts/taskModel.ts"; +import { + addTaskProgressInputSchema, + createTaskInputSchema, + deleteTaskInputSchema, +} from "../../contracts/tasks.ts"; +import { createDashboardQueryClient } from "../api/queryClient.ts"; +import { + createDashboardTrpcClient, + type DashboardTrpcTransport, +} from "../api/trpcClient.ts"; +import { DashboardBrowserApplication } from "../application.tsx"; +import { createDashboardRouter } from "../router.tsx"; +import type { DashboardWebAuthnClient } from "../security/webauthn/webauthnClient.ts"; +import { noOpDashboardRealtimeClient } from "../test/realtime.ts"; + +const { render, screen } = await import("@testing-library/react"); +const userEventModule = await import("@testing-library/user-event"); +const userEvent = userEventModule.default; + +const timestampMs = 1_800_000_000_000; +const operatorUserId = "019fd974-54a2-74dd-a64b-d4186f8d8828"; +const authenticatedStatus: AuthStatus = Object.freeze({ + session: { + authenticatedAtMs: timestampMs, + authMethod: "password" as const, + createdAtMs: timestampMs, + expiresAtMs: timestampMs + 86_400_000, + id: "a".repeat(32), + isCurrent: true, + lastSeenAtMs: timestampMs, + userAgent: "Task browser test", + }, + state: "authenticated", + user: { + id: operatorUserId, + username: "operator", + }, +}); +const initialTask: TaskDetail = Object.freeze({ + assignee: "mira-2026", + bodyMarkdown: "Ship the rewritten task board.", + createdAtMs: timestampMs, + id: "019fd984-63e8-7404-a7da-80c6f243794f", + labels: ["phase-three", "tasks"], + priority: "high", + status: "in-progress", + title: "Build task domain", + updatedAtMs: timestampMs, + version: 1, +}); +const unexpectedWebAuthnClient: DashboardWebAuthnClient = Object.freeze({ + authenticate: () => Promise.reject(new TypeError("Unexpected authentication")), + register: () => Promise.reject(new TypeError("Unexpected registration")), +}); + +interface TransportCall { + readonly input: unknown; + readonly kind: "mutation" | "query"; + readonly path: string; +} + +class TaskTransport implements DashboardTrpcTransport { + readonly calls: TransportCall[] = []; + tasks: TaskDetail[] = [initialTask]; + updates: TaskProgressUpdate[] = []; + + mutation(path: string, input?: unknown): Promise { + this.calls.push({ input, kind: "mutation", path }); + switch (path) { + case "tasks.addUpdate": { + const parsed = v.parse(addTaskProgressInputSchema, input); + const taskIndex = this.tasks.findIndex( + (task) => task.id === parsed.taskId + ); + const task = this.tasks[taskIndex]; + if (task === undefined) { + return Promise.reject(new TypeError("Task is missing")); + } + const update: TaskProgressUpdate = { + author: { id: operatorUserId, kind: "user" }, + createdAtMs: timestampMs + 10, + id: "019fd986-b3d4-7861-b7ce-e42eb18e7af8", + messageMarkdown: parsed.messageMarkdown, + taskId: parsed.taskId, + updatedAtMs: timestampMs + 10, + version: 1, + }; + this.tasks = this.tasks.with(taskIndex, { + ...task, + updatedAtMs: timestampMs + 10, + version: task.version + 1, + }); + this.updates = [update, ...this.updates]; + return Promise.resolve(update); + } + case "tasks.create": { + const parsed = v.parse(createTaskInputSchema, input); + const created: TaskDetail = { + ...(parsed.assignee === undefined + ? {} + : { assignee: parsed.assignee }), + ...(parsed.automation === undefined + ? {} + : { automation: parsed.automation }), + ...(parsed.bodyMarkdown === undefined + ? {} + : { bodyMarkdown: parsed.bodyMarkdown }), + createdAtMs: timestampMs + 1, + id: "019fd985-a9c4-7586-8706-c11563dd5e5d", + labels: parsed.labels ?? [], + priority: parsed.priority ?? "medium", + status: parsed.status ?? "todo", + title: parsed.title, + updatedAtMs: timestampMs + 1, + version: 1, + }; + this.tasks = [created, ...this.tasks]; + return Promise.resolve(created); + } + case "tasks.delete": { + const parsed = v.parse(deleteTaskInputSchema, input); + this.tasks = this.tasks.filter((task) => task.id !== parsed.id); + this.updates = this.updates.filter( + (update) => update.taskId !== parsed.id + ); + return Promise.resolve({ + deletedAtMs: timestampMs + 20, + id: parsed.id, + }); + } + default: { + return Promise.reject(new TypeError(`Unexpected mutation: ${path}`)); + } + } + } + + query(path: string, input?: unknown): Promise { + this.calls.push({ input, kind: "query", path }); + switch (path) { + case "auth.status": { + return Promise.resolve(authenticatedStatus); + } + case "tasks.get": { + const id = + typeof input === "object" && input !== null && "id" in input + ? input.id + : undefined; + return Promise.resolve(this.tasks.find((task) => task.id === id)); + } + case "tasks.list": { + const summaries: TaskSummary[] = this.tasks.map( + ({ bodyMarkdown: _bodyMarkdown, ...task }) => task + ); + return Promise.resolve({ tasks: summaries }); + } + case "tasks.listUpdates": { + const taskId = + typeof input === "object" && input !== null && "taskId" in input + ? input.taskId + : undefined; + return Promise.resolve({ + updates: this.updates.filter((update) => update.taskId === taskId), + }); + } + default: { + return Promise.reject(new TypeError(`Unexpected query: ${path}`)); + } + } + } +} + +const queryClients: ReturnType[] = []; + +function renderTaskRoute(transport: TaskTransport) { + const queryClient = createDashboardQueryClient(); + queryClients.push(queryClient); + const router = createDashboardRouter( + createMemoryHistory({ initialEntries: ["/tasks"] }) + ); + render( + + ); + return queryClient; +} + +afterEach(() => { + for (const queryClient of queryClients.splice(0)) queryClient.clear(); +}); + +describe("Dashboard task route", () => { + test("renders the authenticated four-column board", async () => { + const transport = new TaskTransport(); + renderTaskRoute(transport); + + await screen.findByRole("heading", { level: 1, name: "Tasks" }); + for (const column of ["To do", "In progress", "Blocked", "Done"]) { + expect(screen.getByRole("heading", { level: 2, name: column })).toBeTruthy(); + } + expect( + screen.getByRole("button", { name: "Filter tasks by assignee" }) + ).toBeTruthy(); + expect( + screen.getByRole("button", { name: "Filter tasks by automation" }) + ).toBeTruthy(); + expect(screen.getByText("Build task domain")).toBeTruthy(); + }); + + test("opens and edits the task creation form", async () => { + const transport = new TaskTransport(); + renderTaskRoute(transport); + const user = userEvent.setup(); + + await screen.findByRole("heading", { level: 1, name: "Tasks" }); + await user.click(screen.getByRole("button", { name: "New task" })); + expect(await screen.findByRole("dialog", { name: "New task" })).toBeTruthy(); + await user.type(screen.getByLabelText("Title"), "Draft task"); + expect(screen.getByLabelText("Title").value).toBe("Draft task"); + }); + + test("renders the legacy-shaped board and creates a contract-valid task", async () => { + const transport = new TaskTransport(); + const queryClient = renderTaskRoute(transport); + const user = userEvent.setup(); + + await screen.findByRole("heading", { level: 1, name: "Tasks" }); + for (const column of ["To do", "In progress", "Blocked", "Done"]) { + expect(screen.getByRole("heading", { level: 2, name: column })).toBeTruthy(); + } + expect(screen.getByText("Build task domain")).toBeTruthy(); + + await user.click(screen.getByRole("button", { name: "New task" })); + await user.type(screen.getByLabelText("Title"), "Browser-created task"); + await user.type(screen.getByLabelText("Labels"), "tasks"); + await act(async () => { + screen.getByRole("button", { name: "Create task" }).click(); + for (let attempt = 0; attempt < 50; attempt += 1) { + const created = transport.calls.some( + (call) => call.kind === "mutation" && call.path === "tasks.create" + ); + if ( + created && + screen.queryByRole("heading", { + level: 2, + name: "Progress", + }) !== null && + queryClient.isFetching() === 0 && + queryClient.isMutating() === 0 + ) { + break; + } + await new Promise((resolve) => setTimeout(resolve, 0)); + } + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + expect( + transport.calls.some( + (call) => call.kind === "mutation" && call.path === "tasks.create" + ) + ).toBeTrue(); + expect( + transport.calls.find( + (call) => call.kind === "mutation" && call.path === "tasks.create" + )?.input + ).toMatchObject({ assignee: "mira-2026" }); + expect(queryClient.isFetching()).toBe(0); + expect(queryClient.isMutating()).toBe(0); + expect(screen.getByRole("heading", { level: 2, name: "Progress" })).toBeTruthy(); + expect(screen.getAllByText("Browser-created task")).toHaveLength(2); + }); + + test("adds progress and deletes a task through refreshed versioned state", async () => { + const transport = new TaskTransport(); + const queryClient = renderTaskRoute(transport); + const user = userEvent.setup(); + + await screen.findByRole("heading", { level: 1, name: "Tasks" }); + await user.click( + screen.getByRole("button", { name: "Open task: Build task domain" }) + ); + expect( + await screen.findByRole("heading", { level: 2, name: "Progress" }) + ).toBeTruthy(); + await user.type( + screen.getByLabelText("New progress update"), + "Task browser flow verified" + ); + await act(async () => { + screen.getByRole("button", { name: "Add update" }).click(); + for (let attempt = 0; attempt < 50; attempt += 1) { + if ( + screen.queryByText("Task browser flow verified") !== null && + queryClient.isFetching() === 0 && + queryClient.isMutating() === 0 + ) { + break; + } + await new Promise((resolve) => setTimeout(resolve, 0)); + } + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + expect(screen.getByText("Task browser flow verified")).toBeTruthy(); + + await user.click(screen.getByRole("button", { name: "Delete" })); + await act(async () => { + screen.getByRole("button", { name: "Delete task" }).click(); + for (let attempt = 0; attempt < 50; attempt += 1) { + if (queryClient.isFetching() === 0 && queryClient.isMutating() === 0) { + break; + } + await new Promise((resolve) => setTimeout(resolve, 0)); + } + await new Promise((resolve) => setTimeout(resolve, 250)); + }); + expect(screen.queryByRole("dialog", { name: "Build task domain" })).toBeNull(); + expect(screen.queryByText("Build task domain")).toBeNull(); + expect( + transport.calls.find( + (call) => call.kind === "mutation" && call.path === "tasks.delete" + )?.input + ).toEqual({ expectedVersion: 2, id: initialTask.id }); + }); +}); diff --git a/greenfield/src/browser/tasks/TaskBoardRoute.tsx b/greenfield/src/browser/tasks/TaskBoardRoute.tsx new file mode 100644 index 000000000..9c1027148 --- /dev/null +++ b/greenfield/src/browser/tasks/TaskBoardRoute.tsx @@ -0,0 +1,180 @@ +import { useInfiniteQuery } from "@tanstack/react-query"; +import { ListTodo, Plus } from "lucide-react"; +import { useDeferredValue, useState } from "react"; + +import type { TaskSummary } from "../../contracts/taskModel.ts"; +import type { ListTasksInput } from "../../contracts/tasks.ts"; +import { useDashboardTrpcClient } from "../api/trpcContextValue.ts"; +import { dashboardBrowserFailureMessage } from "../api/trpcError.ts"; +import { Alert } from "../ui/Alert.tsx"; +import { Button } from "../ui/Button.tsx"; +import { EmptyState } from "../ui/EmptyState.tsx"; +import { Icon } from "../ui/Icon.tsx"; +import { LoadingState } from "../ui/LoadingState.tsx"; +import { PageHeader } from "../ui/PageHeader.tsx"; +import { NewTaskModal } from "./NewTaskModal.tsx"; +import { TaskBoard } from "./TaskBoard.tsx"; +import { + type TaskAssigneeFilter, + type TaskAutomationFilter, + TaskBoardToolbar, +} from "./TaskBoardToolbar.tsx"; +import { TaskDetailModal } from "./TaskDetailModal.tsx"; +import { useTaskMutation } from "./taskMutations.ts"; +import { taskListQueryOptions } from "./taskQueries.ts"; +import { useTaskRealtimeInvalidation } from "./useTaskRealtimeInvalidation.ts"; + +function taskFilters( + search: string, + assignee: TaskAssigneeFilter, + automation: TaskAutomationFilter +): ListTasksInput["filters"] { + const normalizedSearch = search.trim(); + if (normalizedSearch.length === 0 && assignee === "all" && automation === "all") { + return; + } + return { + ...(assignee === "all" ? {} : { assignees: [assignee] }), + ...(automation === "all" ? {} : { automation }), + ...(normalizedSearch.length === 0 ? {} : { search: normalizedSearch }), + }; +} + +function uniqueTasks(pages: readonly { readonly tasks: TaskSummary[] }[]): TaskSummary[] { + const tasks = new Map(); + for (const page of pages) { + for (const task of page.tasks) tasks.set(task.id, task); + } + return [...tasks.values()]; +} + +/** @returns Server-filtered task board with complete task mutation dialogs. */ +export function TaskBoardRoute() { + useTaskRealtimeInvalidation(); + const client = useDashboardTrpcClient(); + const [search, setSearch] = useState(""); + const [assignee, setAssignee] = useState("all"); + const [automation, setAutomation] = useState("all"); + const [newTaskOpen, setNewTaskOpen] = useState(false); + const [selectedTaskId, setSelectedTaskId] = useState(); + const deferredSearch = useDeferredValue(search); + const filters = taskFilters(deferredSearch, assignee, automation); + const taskPages = useInfiniteQuery(taskListQueryOptions(client, filters)); + const moveTask = useTaskMutation("tasks.move"); + const tasks = uniqueTasks(taskPages.data?.pages ?? []); + const hasFilters = filters !== undefined; + + return ( +
+ +
+ setNewTaskOpen(true)} + onRefresh={() => void taskPages.refetch()} + onSearchChange={setSearch} + search={search} + /> +
+ + {taskPages.isPending && ( + + )} + {taskPages.isError && ( +
+ + +
+ )} + {taskPages.isSuccess && tasks.length === 0 && ( +
+ { + setSearch(""); + setAssignee("all"); + setAutomation("all"); + }} + variant="secondary" + > + Clear filters + + ) : ( + + ) + } + description={ + hasFilters + ? "Change the search or filters to return to the full board." + : "Create the first task when there is work to track." + } + icon={ListTodo} + title={hasFilters ? "No matching tasks" : "No tasks yet"} + /> +
+ )} + {tasks.length > 0 && ( +
+ moveTask.mutate(input)} + onSelectTask={setSelectedTaskId} + tasks={tasks} + /> + {taskPages.hasNextPage && ( + + )} +
+ )} + setNewTaskOpen(false)} + onCreated={(task) => { + setNewTaskOpen(false); + setSelectedTaskId(task.id); + }} + open={newTaskOpen} + /> + {selectedTaskId !== undefined && ( + setSelectedTaskId(undefined)} + taskId={selectedTaskId} + /> + )} +
+ ); +} diff --git a/greenfield/src/browser/tasks/TaskBoardToolbar.tsx b/greenfield/src/browser/tasks/TaskBoardToolbar.tsx new file mode 100644 index 000000000..9ed17e7dc --- /dev/null +++ b/greenfield/src/browser/tasks/TaskBoardToolbar.tsx @@ -0,0 +1,86 @@ +import { Plus, RefreshCw } from "lucide-react"; + +import { taskAssignees, type TaskAssigneeId } from "../../contracts/taskModel.ts"; +import type { ListTasksInput } from "../../contracts/tasks.ts"; +import { Button } from "../ui/Button.tsx"; +import { Icon } from "../ui/Icon.tsx"; +import { SearchInput } from "../ui/SearchInput.tsx"; +import { Select, type SelectOption } from "../ui/Select.tsx"; + +export type TaskAssigneeFilter = "all" | TaskAssigneeId; +export type TaskAutomationFilter = + | "all" + | NonNullable["automation"]>; + +const assigneeOptions: readonly SelectOption[] = Object.freeze([ + { label: "All assignees", value: "all" }, + ...taskAssignees.map(({ id, label }) => ({ label, value: id })), +]); +const automationOptions: readonly SelectOption[] = Object.freeze([ + { label: "All tasks", value: "all" }, + { label: "Recurring", value: "recurring" }, + { label: "Manual", value: "manual" }, +]); + +interface TaskBoardToolbarProps { + readonly assignee: TaskAssigneeFilter; + readonly automation: TaskAutomationFilter; + readonly busy: boolean; + readonly onAssigneeChange: (value: TaskAssigneeFilter) => void; + readonly onAutomationChange: (value: TaskAutomationFilter) => void; + readonly onCreate: () => void; + readonly onRefresh: () => void; + readonly onSearchChange: (value: string) => void; + readonly search: string; +} + +/** @returns Task search, filters, refresh, and creation controls. */ +export function TaskBoardToolbar({ + assignee, + automation, + busy, + onAssigneeChange, + onAutomationChange, + onCreate, + onRefresh, + onSearchChange, + search, +}: TaskBoardToolbarProps) { + return ( +
+
+ + +
+
+ + +
+
+ ); +} diff --git a/greenfield/src/browser/tasks/TaskCard.tsx b/greenfield/src/browser/tasks/TaskCard.tsx new file mode 100644 index 000000000..b490b88d5 --- /dev/null +++ b/greenfield/src/browser/tasks/TaskCard.tsx @@ -0,0 +1,117 @@ +import { useDraggable } from "@dnd-kit/react"; +import { Bot, GripVertical, UserRound } from "lucide-react"; + +import type { TaskSummary } from "../../contracts/taskModel.ts"; +import { cn } from "../lib/classNames.ts"; +import { Badge } from "../ui/Badge.tsx"; +import { Icon } from "../ui/Icon.tsx"; +import { IconOnlyButton } from "../ui/IconOnlyButton.tsx"; +import { + taskAssigneeLabel, + taskPriorityBadgeVariant, + taskRelativeTime, +} from "./taskPresentation.ts"; + +interface TaskCardContentProps { + readonly overlay?: boolean; + readonly task: TaskSummary; +} + +/** @returns Presentational task-card content shared by the board and drag overlay. */ +export function TaskCardContent({ overlay = false, task }: TaskCardContentProps) { + return ( +
+
+ + {task.priority} + + {task.automation !== undefined && ( + + + {task.automation.recurring ? "Recurring" : "Automated"} + + )} +
+

+ {task.title} +

+ {task.labels.length > 0 && ( +
+ {task.labels.slice(0, 3).map((label) => ( + + {label} + + ))} + {task.labels.length > 3 && ( + + +{task.labels.length - 3} + + )} +
+ )} +
+ + + {taskAssigneeLabel(task.assignee)} + + +
+
+ ); +} + +interface TaskCardProps { + readonly disabled: boolean; + readonly onSelect: (taskId: string) => void; + readonly task: TaskSummary; +} + +/** @returns One draggable, keyboard-selectable task card. */ +export function TaskCard({ disabled, onSelect, task }: TaskCardProps) { + const { handleRef, isDragging, ref } = useDraggable({ + disabled, + id: `task:${task.id}`, + type: "task-card", + }); + + return ( +
+ +
+ ); +} diff --git a/greenfield/src/browser/tasks/TaskColumn.tsx b/greenfield/src/browser/tasks/TaskColumn.tsx new file mode 100644 index 000000000..5ba5645c1 --- /dev/null +++ b/greenfield/src/browser/tasks/TaskColumn.tsx @@ -0,0 +1,67 @@ +import { useDroppable } from "@dnd-kit/react"; + +import type { TaskStatus, TaskSummary } from "../../contracts/taskModel.ts"; +import { cn } from "../lib/classNames.ts"; +import { Heading } from "../ui/Heading.tsx"; +import { Text } from "../ui/Text.tsx"; +import { TaskCard } from "./TaskCard.tsx"; +import { taskStatusDefinitions } from "./taskPresentation.ts"; + +interface TaskColumnProps { + readonly disabled: boolean; + readonly onSelectTask: (taskId: string) => void; + readonly status: TaskStatus; + readonly tasks: readonly TaskSummary[]; +} + +/** @returns One task-board status column and its droppable card region. */ +export function TaskColumn({ disabled, onSelectTask, status, tasks }: TaskColumnProps) { + const definition = taskStatusDefinitions.find( + (candidate) => candidate.status === status + ); + const { isDropTarget, ref } = useDroppable({ + accept: "task-card", + id: `task-column:${status}`, + type: "task-column", + }); + if (definition === undefined) return null; + + return ( +
+
+
+
+ {tasks.length === 0 ? ( + + {definition.emptyLabel} + + ) : ( + tasks.map((task) => ( + + )) + )} +
+
+ ); +} diff --git a/greenfield/src/browser/tasks/TaskDetailModal.tsx b/greenfield/src/browser/tasks/TaskDetailModal.tsx new file mode 100644 index 000000000..c48a4c5d4 --- /dev/null +++ b/greenfield/src/browser/tasks/TaskDetailModal.tsx @@ -0,0 +1,150 @@ +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { Bot, Pencil, Trash2, TriangleAlert } from "lucide-react"; +import { useState } from "react"; + +import { useDashboardTrpcClient } from "../api/trpcContextValue.ts"; +import { dashboardBrowserFailureMessage } from "../api/trpcError.ts"; +import { Alert } from "../ui/Alert.tsx"; +import { Badge } from "../ui/Badge.tsx"; +import { Button } from "../ui/Button.tsx"; +import { Icon } from "../ui/Icon.tsx"; +import { LoadingState } from "../ui/LoadingState.tsx"; +import { Markdown } from "../ui/Markdown.tsx"; +import { Modal } from "../ui/Modal.tsx"; +import { Text } from "../ui/Text.tsx"; +import { TaskEditorForm } from "./TaskEditorForm.tsx"; +import { TaskMetadataControls } from "./TaskMetadataControls.tsx"; +import { useDeleteTaskMutation } from "./taskMutations.ts"; +import { TaskProgressSection } from "./TaskProgressSection.tsx"; +import { taskDetailQueryKey, taskDetailQueryOptions } from "./taskQueries.ts"; + +interface TaskDetailModalProps { + readonly onClose: () => void; + readonly taskId: string; +} + +/** @returns Complete task detail, edit, progress, and deletion dialog. */ +export function TaskDetailModal({ onClose, taskId }: TaskDetailModalProps) { + const client = useDashboardTrpcClient(); + const queryClient = useQueryClient(); + const task = useQuery(taskDetailQueryOptions(client, taskId)); + const deleteTask = useDeleteTaskMutation(onClose); + const [editing, setEditing] = useState(false); + const [confirmingDelete, setConfirmingDelete] = useState(false); + + return ( + + {task.isPending && } + {task.isError && ( + + )} + {task.data !== undefined && editing && ( + setEditing(false)} + onSaved={(saved) => { + queryClient.setQueryData(taskDetailQueryKey(taskId), saved); + setEditing(false); + }} + task={task.data} + /> + )} + {task.data !== undefined && !editing && confirmingDelete && ( +
+
+ +
+ + Delete this task permanently? + + + Progress entries are removed. The immutable audit history + is retained. + +
+
+ +
+ + +
+
+ )} + {task.data !== undefined && !editing && !confirmingDelete && ( +
+ +
+ {task.data.priority} priority + {task.data.labels.map((label) => ( + {label} + ))} + {task.data.automation !== undefined && ( + + + {task.data.automation.scheduleSummary ?? + task.data.automation.cronJobId} + + )} +
+
+ {task.data.bodyMarkdown === undefined ? ( + No task description. + ) : ( + + )} +
+
+ + +
+ +
+ )} +
+ ); +} diff --git a/greenfield/src/browser/tasks/TaskEditorFields.tsx b/greenfield/src/browser/tasks/TaskEditorFields.tsx new file mode 100644 index 000000000..6bd114874 --- /dev/null +++ b/greenfield/src/browser/tasks/TaskEditorFields.tsx @@ -0,0 +1,162 @@ +import { + taskAssignees, + type TaskAssigneeId, + type TaskPriority, + type TaskStatus, +} from "../../contracts/taskModel.ts"; +import { firstFormFieldError } from "../ui/formErrors.ts"; +import { FormField } from "../ui/FormField.tsx"; +import { Input } from "../ui/Input.tsx"; +import { Select, type SelectOption } from "../ui/Select.tsx"; +import { Textarea } from "../ui/Textarea.tsx"; +import { unassignedTaskOwner } from "./taskEditorForm.ts"; +import type { TaskEditorFormApi } from "./useTaskEditorController.ts"; + +type EditorAssignee = TaskAssigneeId | typeof unassignedTaskOwner; + +const priorityOptions: readonly SelectOption[] = Object.freeze([ + { label: "Low", value: "low" }, + { label: "Medium", value: "medium" }, + { label: "High", value: "high" }, +]); +const statusOptions: readonly SelectOption[] = Object.freeze([ + { label: "To do", value: "todo" }, + { label: "In progress", value: "in-progress" }, + { label: "Blocked", value: "blocked" }, + { label: "Done", value: "done" }, +]); +const assigneeOptions: readonly SelectOption[] = Object.freeze([ + { label: "Unassigned", value: unassignedTaskOwner }, + ...taskAssignees.map(({ id, label }) => ({ label, value: id })), +]); + +interface TaskEditorFieldsProps { + readonly busy: boolean; + readonly creating: boolean; + readonly form: TaskEditorFormApi; +} + +/** @returns The general task fields shared by create and edit flows. */ +export function TaskEditorFields({ busy, creating, form }: TaskEditorFieldsProps) { + return ( +
+ + {(field) => ( + + + field.handleChange(event.currentTarget.value) + } + required + value={field.state.value} + /> + + )} + + {creating && ( + <> + + {(field) => ( + + + + )} + + + )} + + {(field) => ( + + + field.handleChange(event.currentTarget.value) + } + placeholder="delivery, database" + value={field.state.value} + /> + + )} + + + {(field) => ( + +