@taskforcehq/taskforce 0.3.327 → 0.3.328
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Taskforce.module.css +24 -53
- package/dist/components/activity/EntityActivityTimeline.js +15 -5
- package/dist/components/context/AttachmentPreviewHost.js +1 -1
- package/dist/components/context/ContextAttachmentManager.d.ts +3 -0
- package/dist/components/context/ContextAttachmentManager.js +116 -20
- package/dist/components/features/planning/PlanningEntitySummary.d.ts +3 -1
- package/dist/components/features/planning/PlanningEntitySummary.js +3 -2
- package/dist/components/features/planning/PlanningLinkPicker.d.ts +3 -1
- package/dist/components/features/planning/PlanningLinkPicker.js +5 -3
- package/dist/components/features/planning/PlanningModule.js +26 -9
- package/dist/components/features/planning/PlanningVisualIdentity.d.ts +14 -0
- package/dist/components/features/planning/PlanningVisualIdentity.js +106 -0
- package/dist/components/features/planning/planningWorkspaceModel.d.ts +11 -0
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +2 -2
- package/dist/components/task/TaskCard.js +37 -4
- package/dist/components/task/TaskContextUpload.d.ts +3 -0
- package/dist/components/task/TaskContextUpload.js +2 -2
- package/dist/components/task/TaskForm.d.ts +4 -0
- package/dist/components/task/TaskForm.js +32 -7
- package/dist/components/task/TaskImageReviews.js +16 -16
- package/dist/components/task/TaskStatusActions.js +11 -2
- package/dist/components/task/TaskWorkstreamPicker.js +2 -1
- package/dist/components/ui/DocumentTypeIcon.d.ts +7 -0
- package/dist/components/ui/DocumentTypeIcon.js +8 -0
- package/dist/components/ui/TaxonomyDropdown.d.ts +9 -1
- package/dist/components/ui/TaxonomyDropdown.js +30 -3
- package/dist/components/views/StandaloneLayout.js +62 -9
- package/dist/components/views/WidgetView.js +8 -5
- package/dist/components/views/panels/PlanningDrawer.d.ts +7 -1
- package/dist/components/views/panels/PlanningDrawer.js +48 -44
- package/dist/config/envSchema.js +1 -0
- package/dist/config/localServiceCloudTarget.d.ts +10 -0
- package/dist/config/localServiceCloudTarget.js +76 -0
- package/dist/core/AiProfileService.d.ts +1 -0
- package/dist/core/AiProfileService.js +2 -1
- package/dist/core/AttachmentLinkService.js +7 -4
- package/dist/core/DeletedTaskLifecycleService.d.ts +4 -0
- package/dist/core/DeletedTaskLifecycleService.js +21 -0
- package/dist/core/McpTokenService.d.ts +4 -0
- package/dist/core/McpTokenService.js +52 -0
- package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +5 -0
- package/dist/core/TaskAttachmentLinksDurableMutationService.js +20 -11
- package/dist/core/Taskforce.d.ts +63 -1
- package/dist/core/Taskforce.js +558 -73
- package/dist/core/types.d.ts +2 -1
- package/dist/documentReviews/DocumentReviewCommentStore.d.ts +11 -0
- package/dist/documentReviews/DocumentReviewCommentStore.js +35 -2
- package/dist/documentReviews/service.d.ts +4 -0
- package/dist/documentReviews/service.js +141 -46
- package/dist/hooks/tasks/useTaskFormActions.d.ts +3 -0
- package/dist/hooks/tasks/useTaskFormActions.js +33 -7
- package/dist/hooks/useTaskforce.d.ts +59 -50
- package/dist/hooks/useTaskforce.js +103 -1
- package/dist/mcp/canonicalAssetHelpers.d.ts +13 -0
- package/dist/mcp/canonicalAssetHelpers.js +39 -5
- package/dist/mcp/collaborationRegistrar.js +15 -13
- package/dist/mcp/documentAssetRegistrar.js +16 -9
- package/dist/mcp/localServiceProxy.js +4 -1
- package/dist/mcp/runtime.d.ts +16 -0
- package/dist/mcp/runtime.js +566 -171
- package/dist/mcp/taskAttachmentCommandAdapter.d.ts +2 -0
- package/dist/mcp/taskAttachmentCommandAdapter.js +14 -8
- package/dist/mcp/taskPlanningRegistrar.js +42 -34
- package/dist/mcp/toolRegistry.js +2 -2
- package/dist/migrations/taskSchemaMigrations.js +35 -0
- package/dist/runtime/appGateDefinitions.d.ts +2 -2
- package/dist/runtime/appGateDefinitions.js +2 -2
- package/dist/server/appAccess.d.ts +12 -0
- package/dist/server/appAccess.js +21 -0
- package/dist/server/authorizedWorkspaceAccess.d.ts +12 -0
- package/dist/server/authorizedWorkspaceAccess.js +40 -0
- package/dist/server/cloudCollectionRead.d.ts +25 -0
- package/dist/server/cloudCollectionRead.js +120 -0
- package/dist/server/cloudCollectionReadPool.d.ts +52 -0
- package/dist/server/cloudCollectionReadPool.js +140 -0
- package/dist/server/cloudCollectionReadWorker.d.ts +1 -0
- package/dist/server/cloudCollectionReadWorker.js +36 -0
- package/dist/server/index.d.ts +3 -11
- package/dist/server/index.js +53 -40
- package/dist/server/localServiceLease.d.ts +3 -0
- package/dist/server/localServiceLease.js +9 -1
- package/dist/server/routes/documents.d.ts +5 -2
- package/dist/server/routes/documents.js +160 -13
- package/dist/server/routes/durableTaskHttpRouters.d.ts +2 -0
- package/dist/server/routes/durableTaskHttpRouters.js +2 -2
- package/dist/server/routes/localService.js +2 -0
- package/dist/server/routes/primitives.d.ts +2 -0
- package/dist/server/routes/shared.d.ts +24 -0
- package/dist/server/routes/shared.js +114 -18
- package/dist/server/routes/sync.js +22 -0
- package/dist/server/routes/syncV3FeedRoutes.js +4 -2
- package/dist/server/routes/syncV3LocalCaptureRoutes.js +20 -2
- package/dist/server/routes/syncV3OperationRoutes.js +17 -2
- package/dist/server/routes/tasks.js +199 -89
- package/dist/server/routes.js +35 -7
- package/dist/service/localServiceCli.js +36 -25
- package/dist/service/localServiceClientLifecycle.js +67 -6
- package/dist/shared/planningIdentity.d.ts +20 -0
- package/dist/shared/planningIdentity.js +79 -0
- package/dist/storage/postgresAdapter.js +4 -0
- package/dist/storage/postgresRequestDiagnostics.d.ts +8 -0
- package/dist/storage/postgresRequestDiagnostics.js +24 -0
- package/dist/storage/workspaceAssetStore.d.ts +32 -0
- package/dist/storage/workspaceAssetStore.js +70 -0
- package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +16 -0
- package/dist/sync/coordinator/localSyncExecutionPermitStore.js +70 -22
- package/dist/sync/coordinator/workspaceSyncTransportGateway.js +4 -2
- package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +5 -0
- package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.d.ts +1 -1
- package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +15 -0
- package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +26 -3
- package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +2 -0
- package/dist/sync/syncApplyHandlers.d.ts +3 -1
- package/dist/sync/syncApplyHandlers.js +29 -0
- package/dist/sync/syncService.js +9 -0
- package/dist/sync/taskSyncPayload.d.ts +1 -0
- package/dist/sync/taskSyncPayload.js +6 -0
- package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +1 -0
- package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +21 -3
- package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +17 -4
- package/dist/sync/v3/localInitiativeOutboxService.js +4 -2
- package/dist/sync/v3/localOutboxDispatcher.js +5 -1
- package/dist/sync/v3/localTaskAttachmentLinksOutboxHandler.js +12 -2
- package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +1 -0
- package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +15 -3
- package/dist/sync/v3/localWorkstreamOutboxService.js +4 -2
- package/dist/sync/v3/syncDurabilityStore.d.ts +8 -0
- package/dist/sync/v3/syncDurabilityStore.js +171 -33
- package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +1 -0
- package/dist/sync/v3/taskAttachmentLinksMutationService.js +18 -2
- package/dist/sync/v3/taskMetadataCoexistence.js +4 -0
- package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +1 -9
- package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +1 -0
- package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +7 -1
- package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +15 -3
- package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +4 -0
- package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +29 -6
- package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +1 -0
- package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +29 -7
- package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +62 -7
- package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +1 -0
- package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.d.ts +2 -0
- package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.js +10 -0
- package/dist/sync/v3/workspaceSyncV3TaskRootProjection.d.ts +1 -1
- package/dist/sync/v3/workspaceSyncV3TaskRootProjection.js +1 -1
- package/dist/sync/workspacePullFeed.d.ts +6 -1
- package/dist/sync/workspacePullFeed.js +172 -161
- package/dist/sync/workspaceSyncModel.d.ts +11 -0
- package/dist/sync/workspaceSyncModel.js +27 -2
- package/dist/types.d.ts +8 -0
- package/dist/ui/assets/{AiIdentityRosterCard-DB1BLO_j.js → AiIdentityRosterCard-MZs7lVED.js} +1 -1
- package/dist/ui/assets/{AiProfilesModule-DPxQeF9N.js → AiProfilesModule-C3i0LOxe.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-DHNYaNg6.js → AnnotatedAttachmentWorkspace-DVZk5Tln.js} +1 -1
- package/dist/ui/assets/AssetTraySortControl-Kf1pelSy.js +1 -0
- package/dist/ui/assets/ContextAttachmentManager-B8boOs6Q.js +3 -0
- package/dist/ui/assets/{DocumentWorkspace-BC7sy5TG.js → DocumentWorkspace-DXH4XUsB.js} +4 -4
- package/dist/ui/assets/EntityActivityTimeline-BtSTq0ON.js +1 -0
- package/dist/ui/assets/PlanningModule-BPVyEa-f.js +1 -0
- package/dist/ui/assets/PlanningModule-CoIR9hbV.css +1 -0
- package/dist/ui/assets/{PlansPage-BVH_NfgT.js → PlansPage-ChqgxALc.js} +1 -1
- package/dist/ui/assets/TaskContextUpload-CB3ol0sN.js +1 -0
- package/dist/ui/assets/TaskContextUpload-qmBeUw3u.css +1 -0
- package/dist/ui/assets/{TaskSettings-xVsxSa0g.js → TaskSettings-g4ECKNOz.js} +1 -1
- package/dist/ui/assets/{TaskforceAgentsModule-CkAxW19K.js → TaskforceAgentsModule-BazdUwxZ.js} +1 -1
- package/dist/ui/assets/WorkflowManagerModule-DWFspC-o.js +1 -0
- package/dist/ui/assets/index-BUplSsv_.js +7 -0
- package/dist/ui/assets/index-gz2EXuoK.css +1 -0
- package/dist/ui/assets/{vendor-icons-B8ZNsH1g.js → vendor-icons-BkFLXavV.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/constants.d.ts +2 -2
- package/dist/utils/constants.js +1 -1
- package/dist/utils/contextAttachmentUpload.d.ts +2 -0
- package/dist/utils/contextAttachmentUpload.js +7 -0
- package/dist/utils/contextFiles.d.ts +1 -1
- package/dist/utils/contextFiles.js +4 -4
- package/package.json +6 -1
- package/dist/ui/assets/AssetTraySortControl-DphbyHVc.js +0 -1
- package/dist/ui/assets/ContextAttachmentManager-CMxSzHqc.js +0 -3
- package/dist/ui/assets/EntityActivityTimeline-pJ2ZjZ-k.js +0 -1
- package/dist/ui/assets/PlanningModule-CeTqXMMz.css +0 -1
- package/dist/ui/assets/PlanningModule-t5GbvVR9.js +0 -1
- package/dist/ui/assets/TaskContextUpload-Ds4LnDzV.css +0 -1
- package/dist/ui/assets/TaskContextUpload-bl4fOOUz.js +0 -1
- package/dist/ui/assets/WorkflowManagerModule-BKqIGVnF.js +0 -1
- package/dist/ui/assets/index-BzJlVnNS.css +0 -1
- package/dist/ui/assets/index-Cx7vZJHL.js +0 -7
package/dist/core/Taskforce.js
CHANGED
|
@@ -3,11 +3,13 @@ import * as path from 'path';
|
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import { createHash, pbkdf2Sync, randomBytes, timingSafeEqual } from 'crypto';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
+
import { assertPlanningIdentityColor, assertPlanningIdentityIcon, } from '../shared/planningIdentity.js';
|
|
6
7
|
import { createDatabaseAdapter } from '../storage/createDatabaseAdapter.js';
|
|
7
8
|
import { assertLocalDataOwnerPermit, } from '../server/localDataOwnerPermit.js';
|
|
8
9
|
import { resolveLocalDatabaseIdentity } from '../server/localDatabaseIdentity.js';
|
|
9
10
|
import { compareWorkspaceSyncStateRevisions, normalizeWorkspaceSyncStateRevision, } from '../sync/workspaceSyncState.js';
|
|
10
11
|
import { WorkspaceSyncDurabilityStore } from '../sync/v3/syncDurabilityStore.js';
|
|
12
|
+
import { workspaceSyncV3RepairEntityReadBatchSize } from '../sync/v3/workspaceSyncV3RepairSqlBatching.js';
|
|
11
13
|
import { isTaskMetadataActivityEventId } from '../sync/v3/taskMetadataActivityIdentity.js';
|
|
12
14
|
import { isTaskLifecycleActivityEventId } from '../sync/v3/taskLifecycleActivityIdentity.js';
|
|
13
15
|
import { resolveV3OwnedTaskLifecycleIds, resolveV3OwnedTaskMetadataIds, resolveV3OwnedTaskTaxonomyIds, } from '../sync/v3/taskMetadataCoexistence.js';
|
|
@@ -23,7 +25,7 @@ import { archiveAiProfileService, bindAiProfileConnectionService, getAiProfileBy
|
|
|
23
25
|
import { syncCanonicalAttachmentLinksForTargetService, syncCanonicalTaskAttachmentLinksService, } from './AttachmentLinkService.js';
|
|
24
26
|
import { listTaskAttachmentLinksForDurableSync as listTaskAttachmentLinksForDurableSyncService, listWorkspaceTaskAttachmentLinksForDurableSync as listWorkspaceTaskAttachmentLinksForDurableSyncService, normalizeTaskAttachmentDescriptorsForDurableSync as normalizeTaskAttachmentDescriptorsForDurableSyncService, resolveTaskAttachmentLinksForDurableSync as resolveTaskAttachmentLinksForDurableSyncService, } from './TaskAttachmentLinksDurableMutationService.js';
|
|
25
27
|
import { EntityEventIdentityCollisionError, addEntityEventService, addTaskEventService, buildEntityActivityService, buildTaskActivityService, getEntityEventForSyncService, getTaskEventForSyncService, listEntityEventsInternalService, listWorkspaceEntityEventsForSyncService, listTaskEventsInternalService, listWorkspaceTaskEventsForSyncService, replaceEntityEventsService, replaceTaskCommentsService, replaceTaskEventsService, upsertEntityEventForSyncService, deleteEntityEventForSyncService, upsertTaskEventForSyncService, } from './TaskActivityService.js';
|
|
26
|
-
import { addDeletedTaskRecordService, getDeletedTaskService, listDeletedTasksService, mapDeletedTaskRowService, normalizeTaskSnapshotForRestoreService, normalizeDeletedTaskRestoreLifecycleService, purgeExpiredDeletedTasksService, } from './DeletedTaskLifecycleService.js';
|
|
28
|
+
import { addDeletedTaskRecordService, getDeletedTaskService, listDeletedTasksService, listDeletedTasksReadOnlyService, mapDeletedTaskRowService, normalizeTaskSnapshotForRestoreService, normalizeDeletedTaskRestoreLifecycleService, purgeExpiredDeletedTasksService, } from './DeletedTaskLifecycleService.js';
|
|
27
29
|
import { DELETED_TASK_RETENTION_DAYS } from '../shared/deletedTaskRetention.js';
|
|
28
30
|
import { applyWorkspaceSyncSnapshotService, claimPushIdempotencyService, completePushIdempotencyClaimService, deleteDocumentWatermarkService, deleteDocumentWatermarkStrictService, getDocumentWatermarkService, listTasksForSyncService, listWorkspaceSyncDeletesSinceService, readPushIdempotencyService, releasePushIdempotencyClaimService, renewPushIdempotencyClaimService, recordSyncEventDurablyService, recordSyncEventService, upsertDocumentWatermarkService, upsertDocumentWatermarkStrictService, writePushIdempotencyService, } from './SyncReconciliationService.js';
|
|
29
31
|
import { getGlobalSettingsService, hasPersistedGlobalSettingsService, readGlobalConfigService, updateGlobalSettingsService, writeGlobalConfigService, } from './GlobalSettingsService.js';
|
|
@@ -1375,9 +1377,13 @@ export class TaskforceCore {
|
|
|
1375
1377
|
// Initialize Database Adapter (SQLite default; Postgres scaffolded)
|
|
1376
1378
|
const provider = config.database?.provider || 'sqlite';
|
|
1377
1379
|
const connectionString = config.database?.connectionString || process.env.TASKFORCE_DATABASE_URL;
|
|
1380
|
+
const isCloudReadWorker = config.initializationMode === 'cloud-read-worker';
|
|
1378
1381
|
if (provider === 'postgres' && !connectionString) {
|
|
1379
1382
|
throw new Error('[Taskforce] Postgres provider requires a connection string (TASKFORCE_DATABASE_URL).');
|
|
1380
1383
|
}
|
|
1384
|
+
if (isCloudReadWorker && (this.runtimeMode !== 'cloud' || provider !== 'postgres')) {
|
|
1385
|
+
throw new Error('[Taskforce] Cloud read-worker initialization requires cloud runtime with PostgreSQL.');
|
|
1386
|
+
}
|
|
1381
1387
|
if (this.runtimeMode === 'local') {
|
|
1382
1388
|
assertLocalDataOwnerPermit({
|
|
1383
1389
|
permit: config.localDataOwnerPermit,
|
|
@@ -1391,8 +1397,10 @@ export class TaskforceCore {
|
|
|
1391
1397
|
databaseIdentityKind: provider === 'sqlite' ? 'path' : 'opaque',
|
|
1392
1398
|
});
|
|
1393
1399
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1400
|
+
if (!isCloudReadWorker) {
|
|
1401
|
+
this.ensureDirectories();
|
|
1402
|
+
this.migrateLegacyGlobalSettingsDbIfNeeded();
|
|
1403
|
+
}
|
|
1396
1404
|
this.db = createDatabaseAdapter({
|
|
1397
1405
|
provider,
|
|
1398
1406
|
sqliteDbPath: this.dbPath,
|
|
@@ -1428,53 +1436,59 @@ export class TaskforceCore {
|
|
|
1428
1436
|
throw new Error(`[Taskforce] Failed to initialize global settings DB at ${this.globalSettingsDbPath}: ${String(error?.message || error)}`);
|
|
1429
1437
|
}
|
|
1430
1438
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1439
|
+
if (!isCloudReadWorker) {
|
|
1440
|
+
this.initializeSchema();
|
|
1441
|
+
ensureTaskApproachRemoved(this.db);
|
|
1442
|
+
ensureTenantSchema(this.db);
|
|
1443
|
+
ensureWorkspaceSchema(this.db);
|
|
1444
|
+
ensureWorkspaceSyncDurabilitySchema(this.db);
|
|
1445
|
+
if (this.runtimeMode === 'local') {
|
|
1446
|
+
ensureLocalSyncCoordinatorSchema(this.db);
|
|
1447
|
+
}
|
|
1448
|
+
ensureScheduleTaskSchema(this.db);
|
|
1449
|
+
ensureWorkstreamTaskOrderSchema(this.db);
|
|
1450
|
+
ensureAssigneeTaskSchema(this.db);
|
|
1451
|
+
ensureTaskReferenceSchema(this.db);
|
|
1452
|
+
ensurePlanningReferenceSchema(this.db);
|
|
1453
|
+
ensureTaskAssetsSchema(this.db);
|
|
1454
|
+
ensureWorkspaceAssetsSchema(this.db);
|
|
1455
|
+
backfillPlanningAttachmentAssetIdentities(this.db, this.tenantId);
|
|
1456
|
+
ensureDocumentReferenceSchema(this.db);
|
|
1457
|
+
ensureDocumentReviewSessionsSchema(this.db);
|
|
1458
|
+
ensureAnnotatedAttachmentSessionsSchema(this.db);
|
|
1459
|
+
ensureTaskforceAgentsSchema(this.db);
|
|
1460
|
+
ensureAgentRolesSchema(this.db);
|
|
1461
|
+
ensureTaskforceAgentSkillsSchema(this.db);
|
|
1462
|
+
ensureWorkflowSchema(this.db);
|
|
1463
|
+
ensureBillingSchema(this.db);
|
|
1464
|
+
ensureEntitlementsSchema(this.db);
|
|
1465
|
+
ensureAuthIdentitySchema(this.db);
|
|
1466
|
+
this.performMigrationIfNeeded();
|
|
1467
|
+
repairTaskUpdatedAtOnlySyncDrift({
|
|
1468
|
+
db: this.db,
|
|
1469
|
+
tenantId: this.tenantId,
|
|
1470
|
+
loadTask: (taskId, workspaceId) => this.getTask(taskId, workspaceId),
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1463
1473
|
this.planEntitlementService = new PlanEntitlementService(this.db, this.tenantId, {
|
|
1464
1474
|
getWorkspaceOwnerUserId: (wid) => this.getWorkspaceOwnerUserId(wid),
|
|
1465
1475
|
readGlobalConfig: () => this.readGlobalConfig(),
|
|
1466
1476
|
writeGlobalConfig: (config) => this.writeGlobalConfig(config),
|
|
1467
1477
|
});
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1478
|
+
if (!isCloudReadWorker) {
|
|
1479
|
+
this.planEntitlementService.repairPlanCatalogDefaultsIfNeeded();
|
|
1480
|
+
this.planEntitlementService.normalizeLegacyPlanDataIfNeeded();
|
|
1481
|
+
this.planEntitlementService.backfillAccountEntitlements();
|
|
1482
|
+
}
|
|
1471
1483
|
this.authTokenService = new AuthTokenService(this.db, this.tenantId);
|
|
1472
1484
|
this.identityService = new AuthIdentityService(this.db, this.tenantId);
|
|
1473
1485
|
this.authTokenService.setIdentityService(this.identityService);
|
|
1474
1486
|
this.mcpTokenService = new McpTokenService(this.db, this.tenantId, {
|
|
1475
1487
|
getAccountAccessStatus: (userId) => this.getAccountAccessStatus(userId),
|
|
1476
1488
|
});
|
|
1477
|
-
|
|
1489
|
+
if (!isCloudReadWorker) {
|
|
1490
|
+
purgeExpiredDeletedTasksService(this.getDeletedTaskLifecycleServiceDeps());
|
|
1491
|
+
}
|
|
1478
1492
|
}
|
|
1479
1493
|
close() {
|
|
1480
1494
|
if (this.globalSettingsDb && this.globalSettingsDb.open) {
|
|
@@ -1751,6 +1765,7 @@ export class TaskforceCore {
|
|
|
1751
1765
|
order_in_day INTEGER,
|
|
1752
1766
|
workstream_order INTEGER,
|
|
1753
1767
|
workstream_id TEXT,
|
|
1768
|
+
card_cover_asset_id TEXT,
|
|
1754
1769
|
FOREIGN KEY(workstream_id) REFERENCES workstreams(id) ON DELETE SET NULL
|
|
1755
1770
|
);
|
|
1756
1771
|
|
|
@@ -1832,6 +1847,20 @@ export class TaskforceCore {
|
|
|
1832
1847
|
FOREIGN KEY(initiative_id) REFERENCES initiatives(id) ON DELETE SET NULL
|
|
1833
1848
|
);
|
|
1834
1849
|
|
|
1850
|
+
CREATE TABLE IF NOT EXISTS planning_entity_identities (
|
|
1851
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
1852
|
+
workspace_id TEXT NOT NULL DEFAULT 'default',
|
|
1853
|
+
entity_type TEXT NOT NULL,
|
|
1854
|
+
entity_id TEXT NOT NULL,
|
|
1855
|
+
icon TEXT,
|
|
1856
|
+
color TEXT,
|
|
1857
|
+
updated_at TEXT NOT NULL,
|
|
1858
|
+
PRIMARY KEY (tenant_id, workspace_id, entity_type, entity_id)
|
|
1859
|
+
);
|
|
1860
|
+
|
|
1861
|
+
CREATE INDEX IF NOT EXISTS idx_planning_entity_identities_updated
|
|
1862
|
+
ON planning_entity_identities (tenant_id, workspace_id, updated_at, entity_type, entity_id);
|
|
1863
|
+
|
|
1835
1864
|
CREATE TABLE IF NOT EXISTS workspace_workstream_reference_sequences (
|
|
1836
1865
|
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
1837
1866
|
workspace_id TEXT NOT NULL DEFAULT 'default',
|
|
@@ -2222,6 +2251,9 @@ export class TaskforceCore {
|
|
|
2222
2251
|
if (!taskColumns.some((c) => c.name === 'workstream_id')) {
|
|
2223
2252
|
this.db.exec(`ALTER TABLE tasks ADD COLUMN workstream_id TEXT`);
|
|
2224
2253
|
}
|
|
2254
|
+
if (!taskColumns.some((c) => c.name === 'card_cover_asset_id')) {
|
|
2255
|
+
this.db.exec(`ALTER TABLE tasks ADD COLUMN card_cover_asset_id TEXT`);
|
|
2256
|
+
}
|
|
2225
2257
|
const initiativeColumns = this.db.prepare(`PRAGMA table_info(initiatives)`).all();
|
|
2226
2258
|
if (!initiativeColumns.some((c) => c.name === 'comments_json')) {
|
|
2227
2259
|
this.db.exec(`ALTER TABLE initiatives ADD COLUMN comments_json TEXT`);
|
|
@@ -2258,6 +2290,12 @@ export class TaskforceCore {
|
|
|
2258
2290
|
catch {
|
|
2259
2291
|
// Best-effort schema repair for existing deployments.
|
|
2260
2292
|
}
|
|
2293
|
+
try {
|
|
2294
|
+
this.db.exec(`ALTER TABLE tasks ADD COLUMN IF NOT EXISTS card_cover_asset_id TEXT`);
|
|
2295
|
+
}
|
|
2296
|
+
catch {
|
|
2297
|
+
// Best-effort schema repair for existing deployments.
|
|
2298
|
+
}
|
|
2261
2299
|
try {
|
|
2262
2300
|
this.db.exec(`ALTER TABLE initiatives ADD COLUMN IF NOT EXISTS comments_json TEXT`);
|
|
2263
2301
|
this.db.exec(`ALTER TABLE initiatives ADD COLUMN IF NOT EXISTS attachments_json TEXT`);
|
|
@@ -3679,7 +3717,7 @@ export class TaskforceCore {
|
|
|
3679
3717
|
const originalFilename = row.original_filename ? String(row.original_filename) : path.basename(storageKey);
|
|
3680
3718
|
const assetKind = String(canonicalAsset?.kind || row.kind || '').trim();
|
|
3681
3719
|
const isCanonicalDocument = Boolean(canonicalAsset && isValidCanonicalDocumentAsset(canonicalAsset));
|
|
3682
|
-
const
|
|
3720
|
+
const caption = isCanonicalDocument
|
|
3683
3721
|
? getCanonicalDocumentName({
|
|
3684
3722
|
logicalName: canonicalAsset?.logicalName || (row.logical_name ? String(row.logical_name) : null),
|
|
3685
3723
|
originalFilename,
|
|
@@ -3687,11 +3725,7 @@ export class TaskforceCore {
|
|
|
3687
3725
|
assetId: String(row.asset_id || '').trim() || null
|
|
3688
3726
|
})
|
|
3689
3727
|
: (canonicalAsset?.logicalName || (row.logical_name ? String(row.logical_name) : originalFilename));
|
|
3690
|
-
const
|
|
3691
|
-
? displayName
|
|
3692
|
-
: (row.display_name
|
|
3693
|
-
? String(row.display_name)
|
|
3694
|
-
: displayName);
|
|
3728
|
+
const displayName = row.display_name ? String(row.display_name) : originalFilename;
|
|
3695
3729
|
const referenceNumber = typeof canonicalAsset?.referenceNumber === 'number'
|
|
3696
3730
|
? canonicalAsset.referenceNumber
|
|
3697
3731
|
: (Number.isFinite(Number(row.reference_number))
|
|
@@ -3825,6 +3859,9 @@ export class TaskforceCore {
|
|
|
3825
3859
|
orderInDay: this.sanitizeOrderInDay(row.order_in_day),
|
|
3826
3860
|
workstreamOrder: this.sanitizeOrderInDay(row.workstream_order),
|
|
3827
3861
|
workstreamId: this.sanitizeWorkstreamId(row.workstream_id),
|
|
3862
|
+
cardCoverAssetId: typeof row.card_cover_asset_id === 'string' && row.card_cover_asset_id.trim()
|
|
3863
|
+
? row.card_cover_asset_id.trim()
|
|
3864
|
+
: null,
|
|
3828
3865
|
taxonomies: JSON.parse(row.taxonomies || '{}'),
|
|
3829
3866
|
createdBy: this.normalizeCreatedBy(row.created_by, workspaceId, { preserveGenericAi: true }),
|
|
3830
3867
|
createdByActor: this.resolveWorkspaceActor(workspaceId, row.created_by),
|
|
@@ -3857,6 +3894,11 @@ export class TaskforceCore {
|
|
|
3857
3894
|
title: String(row.title || '').trim() || 'Untitled Initiative',
|
|
3858
3895
|
description: typeof row.description === 'string' ? row.description : null,
|
|
3859
3896
|
ownerId: typeof row.owner_id === 'string' && row.owner_id.trim().length > 0 ? row.owner_id.trim() : null,
|
|
3897
|
+
icon: typeof row.identity_icon === 'string' && row.identity_icon.trim().length > 0 ? row.identity_icon.trim() : null,
|
|
3898
|
+
color: typeof row.identity_color === 'string' && row.identity_color.trim().length > 0 ? row.identity_color.trim() : null,
|
|
3899
|
+
identityUpdatedAt: typeof row.identity_updated_at === 'string' && row.identity_updated_at.trim().length > 0
|
|
3900
|
+
? row.identity_updated_at.trim()
|
|
3901
|
+
: null,
|
|
3860
3902
|
createdAt: String(row.created_at || '').trim() || new Date().toISOString(),
|
|
3861
3903
|
updatedAt: typeof row.updated_at === 'string' && row.updated_at.trim().length > 0 ? row.updated_at.trim() : undefined,
|
|
3862
3904
|
order: row.sort_order == null ? null : Number(row.sort_order),
|
|
@@ -3890,6 +3932,11 @@ export class TaskforceCore {
|
|
|
3890
3932
|
title: String(row.title || '').trim() || 'Untitled Workstream',
|
|
3891
3933
|
description: typeof row.description === 'string' ? row.description : null,
|
|
3892
3934
|
ownerId: typeof row.owner_id === 'string' && row.owner_id.trim().length > 0 ? row.owner_id.trim() : null,
|
|
3935
|
+
icon: typeof row.identity_icon === 'string' && row.identity_icon.trim().length > 0 ? row.identity_icon.trim() : null,
|
|
3936
|
+
color: typeof row.identity_color === 'string' && row.identity_color.trim().length > 0 ? row.identity_color.trim() : null,
|
|
3937
|
+
identityUpdatedAt: typeof row.identity_updated_at === 'string' && row.identity_updated_at.trim().length > 0
|
|
3938
|
+
? row.identity_updated_at.trim()
|
|
3939
|
+
: null,
|
|
3893
3940
|
createdAt: String(row.created_at || '').trim() || new Date().toISOString(),
|
|
3894
3941
|
updatedAt: typeof row.updated_at === 'string' && row.updated_at.trim().length > 0 ? row.updated_at.trim() : undefined,
|
|
3895
3942
|
order: row.sort_order == null ? null : Number(row.sort_order),
|
|
@@ -3945,9 +3992,15 @@ export class TaskforceCore {
|
|
|
3945
3992
|
return null;
|
|
3946
3993
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3947
3994
|
const row = this.db.prepare(`
|
|
3948
|
-
SELECT
|
|
3949
|
-
|
|
3950
|
-
|
|
3995
|
+
SELECT i.*, identity.icon AS identity_icon, identity.color AS identity_color,
|
|
3996
|
+
identity.updated_at AS identity_updated_at
|
|
3997
|
+
FROM initiatives i
|
|
3998
|
+
LEFT JOIN planning_entity_identities identity
|
|
3999
|
+
ON identity.tenant_id = i.tenant_id
|
|
4000
|
+
AND identity.workspace_id = i.workspace_id
|
|
4001
|
+
AND identity.entity_type = 'initiative'
|
|
4002
|
+
AND identity.entity_id = i.id
|
|
4003
|
+
WHERE i.tenant_id = ? AND i.workspace_id = ? AND i.id = ?
|
|
3951
4004
|
LIMIT 1
|
|
3952
4005
|
`).get(this.tenantId, normalizedWorkspaceId, normalizedId);
|
|
3953
4006
|
if (!row)
|
|
@@ -3957,10 +4010,16 @@ export class TaskforceCore {
|
|
|
3957
4010
|
listInitiatives(workspaceId) {
|
|
3958
4011
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3959
4012
|
const rows = this.db.prepare(`
|
|
3960
|
-
SELECT
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
4013
|
+
SELECT i.*, identity.icon AS identity_icon, identity.color AS identity_color,
|
|
4014
|
+
identity.updated_at AS identity_updated_at
|
|
4015
|
+
FROM initiatives i
|
|
4016
|
+
LEFT JOIN planning_entity_identities identity
|
|
4017
|
+
ON identity.tenant_id = i.tenant_id
|
|
4018
|
+
AND identity.workspace_id = i.workspace_id
|
|
4019
|
+
AND identity.entity_type = 'initiative'
|
|
4020
|
+
AND identity.entity_id = i.id
|
|
4021
|
+
WHERE i.tenant_id = ? AND i.workspace_id = ?
|
|
4022
|
+
ORDER BY COALESCE(i.sort_order, 2147483647), i.created_at ASC
|
|
3964
4023
|
`).all(this.tenantId, normalizedWorkspaceId);
|
|
3965
4024
|
return rows.map((row) => this.rowToInitiative(row));
|
|
3966
4025
|
}
|
|
@@ -3968,10 +4027,16 @@ export class TaskforceCore {
|
|
|
3968
4027
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3969
4028
|
const safeLimit = Math.max(1, Math.min(10000, Math.floor(limit)));
|
|
3970
4029
|
const rows = this.db.prepare(`
|
|
3971
|
-
SELECT
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
4030
|
+
SELECT i.*, identity.icon AS identity_icon, identity.color AS identity_color,
|
|
4031
|
+
identity.updated_at AS identity_updated_at
|
|
4032
|
+
FROM initiatives i
|
|
4033
|
+
LEFT JOIN planning_entity_identities identity
|
|
4034
|
+
ON identity.tenant_id = i.tenant_id
|
|
4035
|
+
AND identity.workspace_id = i.workspace_id
|
|
4036
|
+
AND identity.entity_type = 'initiative'
|
|
4037
|
+
AND identity.entity_id = i.id
|
|
4038
|
+
WHERE i.tenant_id = ? AND i.workspace_id = ? AND i.id > ?
|
|
4039
|
+
ORDER BY i.id ASC
|
|
3975
4040
|
LIMIT ?
|
|
3976
4041
|
`).all(this.tenantId, normalizedWorkspaceId, String(afterId || ''), safeLimit);
|
|
3977
4042
|
return rows.map((row) => this.rowToInitiative(row));
|
|
@@ -4234,9 +4299,15 @@ export class TaskforceCore {
|
|
|
4234
4299
|
return null;
|
|
4235
4300
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
4236
4301
|
const row = this.db.prepare(`
|
|
4237
|
-
SELECT
|
|
4238
|
-
|
|
4239
|
-
|
|
4302
|
+
SELECT w.*, identity.icon AS identity_icon, identity.color AS identity_color,
|
|
4303
|
+
identity.updated_at AS identity_updated_at
|
|
4304
|
+
FROM workstreams w
|
|
4305
|
+
LEFT JOIN planning_entity_identities identity
|
|
4306
|
+
ON identity.tenant_id = w.tenant_id
|
|
4307
|
+
AND identity.workspace_id = w.workspace_id
|
|
4308
|
+
AND identity.entity_type = 'workstream'
|
|
4309
|
+
AND identity.entity_id = w.id
|
|
4310
|
+
WHERE w.tenant_id = ? AND w.workspace_id = ? AND w.id = ?
|
|
4240
4311
|
LIMIT 1
|
|
4241
4312
|
`).get(this.tenantId, normalizedWorkspaceId, normalizedId);
|
|
4242
4313
|
if (!row)
|
|
@@ -4246,10 +4317,16 @@ export class TaskforceCore {
|
|
|
4246
4317
|
listWorkstreams(workspaceId) {
|
|
4247
4318
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
4248
4319
|
const rows = this.db.prepare(`
|
|
4249
|
-
SELECT
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4320
|
+
SELECT w.*, identity.icon AS identity_icon, identity.color AS identity_color,
|
|
4321
|
+
identity.updated_at AS identity_updated_at
|
|
4322
|
+
FROM workstreams w
|
|
4323
|
+
LEFT JOIN planning_entity_identities identity
|
|
4324
|
+
ON identity.tenant_id = w.tenant_id
|
|
4325
|
+
AND identity.workspace_id = w.workspace_id
|
|
4326
|
+
AND identity.entity_type = 'workstream'
|
|
4327
|
+
AND identity.entity_id = w.id
|
|
4328
|
+
WHERE w.tenant_id = ? AND w.workspace_id = ?
|
|
4329
|
+
ORDER BY COALESCE(w.sort_order, 2147483647), w.created_at ASC
|
|
4253
4330
|
`).all(this.tenantId, normalizedWorkspaceId);
|
|
4254
4331
|
return rows.map((row) => this.rowToWorkstream(row));
|
|
4255
4332
|
}
|
|
@@ -4257,14 +4334,157 @@ export class TaskforceCore {
|
|
|
4257
4334
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
4258
4335
|
const safeLimit = Math.max(1, Math.min(10000, Math.floor(limit)));
|
|
4259
4336
|
const rows = this.db.prepare(`
|
|
4260
|
-
SELECT
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4337
|
+
SELECT w.*, identity.icon AS identity_icon, identity.color AS identity_color,
|
|
4338
|
+
identity.updated_at AS identity_updated_at
|
|
4339
|
+
FROM workstreams w
|
|
4340
|
+
LEFT JOIN planning_entity_identities identity
|
|
4341
|
+
ON identity.tenant_id = w.tenant_id
|
|
4342
|
+
AND identity.workspace_id = w.workspace_id
|
|
4343
|
+
AND identity.entity_type = 'workstream'
|
|
4344
|
+
AND identity.entity_id = w.id
|
|
4345
|
+
WHERE w.tenant_id = ? AND w.workspace_id = ? AND w.id > ?
|
|
4346
|
+
ORDER BY w.id ASC
|
|
4264
4347
|
LIMIT ?
|
|
4265
4348
|
`).all(this.tenantId, normalizedWorkspaceId, String(afterId || ''), safeLimit);
|
|
4266
4349
|
return rows.map((row) => this.rowToWorkstream(row));
|
|
4267
4350
|
}
|
|
4351
|
+
listPlanningEntityIdentities(workspaceId) {
|
|
4352
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
4353
|
+
const rows = this.db.prepare(`
|
|
4354
|
+
SELECT entity_type, entity_id, icon, color, updated_at
|
|
4355
|
+
FROM planning_entity_identities
|
|
4356
|
+
WHERE tenant_id = ? AND workspace_id = ?
|
|
4357
|
+
ORDER BY updated_at ASC, entity_type ASC, entity_id ASC
|
|
4358
|
+
`).all(this.tenantId, normalizedWorkspaceId);
|
|
4359
|
+
return rows.map((row) => ({
|
|
4360
|
+
entityType: row.entity_type === 'workstream' ? 'workstream' : 'initiative',
|
|
4361
|
+
entityId: String(row.entity_id || '').trim(),
|
|
4362
|
+
icon: assertPlanningIdentityIcon(row.icon),
|
|
4363
|
+
color: assertPlanningIdentityColor(row.color),
|
|
4364
|
+
updatedAt: String(row.updated_at || '').trim(),
|
|
4365
|
+
}));
|
|
4366
|
+
}
|
|
4367
|
+
writePlanningEntityIdentity(identity, workspaceId) {
|
|
4368
|
+
const existing = this.db.prepare(`
|
|
4369
|
+
SELECT icon, color, updated_at
|
|
4370
|
+
FROM planning_entity_identities
|
|
4371
|
+
WHERE tenant_id = ? AND workspace_id = ? AND entity_type = ? AND entity_id = ?
|
|
4372
|
+
LIMIT 1
|
|
4373
|
+
`).get(this.tenantId, workspaceId, identity.entityType, identity.entityId);
|
|
4374
|
+
if (existing) {
|
|
4375
|
+
this.db.prepare(`
|
|
4376
|
+
UPDATE planning_entity_identities
|
|
4377
|
+
SET icon = ?, color = ?, updated_at = ?
|
|
4378
|
+
WHERE tenant_id = ? AND workspace_id = ? AND entity_type = ? AND entity_id = ?
|
|
4379
|
+
`).run(identity.icon, identity.color, identity.updatedAt, this.tenantId, workspaceId, identity.entityType, identity.entityId);
|
|
4380
|
+
}
|
|
4381
|
+
else {
|
|
4382
|
+
this.db.prepare(`
|
|
4383
|
+
INSERT INTO planning_entity_identities (
|
|
4384
|
+
tenant_id, workspace_id, entity_type, entity_id, icon, color, updated_at
|
|
4385
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
4386
|
+
`).run(this.tenantId, workspaceId, identity.entityType, identity.entityId, identity.icon, identity.color, identity.updatedAt);
|
|
4387
|
+
}
|
|
4388
|
+
return identity;
|
|
4389
|
+
}
|
|
4390
|
+
updatePlanningEntityIdentity(entityType, entityId, patch, workspaceId) {
|
|
4391
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
4392
|
+
const normalizedEntityId = String(entityId || '').trim();
|
|
4393
|
+
const entity = entityType === 'initiative'
|
|
4394
|
+
? this.getInitiative(normalizedEntityId, normalizedWorkspaceId)
|
|
4395
|
+
: this.getWorkstream(normalizedEntityId, normalizedWorkspaceId);
|
|
4396
|
+
if (!entity) {
|
|
4397
|
+
throw new TaskforceRuleError(`${entityType === 'initiative' ? 'Initiative' : 'Workstream'} not found: ${normalizedEntityId}`, 404, entityType === 'initiative' ? 'INITIATIVE_NOT_FOUND' : 'WORKSTREAM_NOT_FOUND');
|
|
4398
|
+
}
|
|
4399
|
+
if (entity.isArchived) {
|
|
4400
|
+
throw new TaskforceRuleError('Restore the planning entity before changing its visual identity.', 409, 'PLANNING_IDENTITY_ARCHIVED');
|
|
4401
|
+
}
|
|
4402
|
+
if (!Object.prototype.hasOwnProperty.call(patch, 'icon')
|
|
4403
|
+
&& !Object.prototype.hasOwnProperty.call(patch, 'color')) {
|
|
4404
|
+
throw new TaskforceRuleError('Planning identity update must include icon or color.', 400, 'PLANNING_IDENTITY_INVALID');
|
|
4405
|
+
}
|
|
4406
|
+
let icon;
|
|
4407
|
+
let color;
|
|
4408
|
+
try {
|
|
4409
|
+
icon = Object.prototype.hasOwnProperty.call(patch, 'icon')
|
|
4410
|
+
? assertPlanningIdentityIcon(patch.icon)
|
|
4411
|
+
: assertPlanningIdentityIcon(entity.icon);
|
|
4412
|
+
color = Object.prototype.hasOwnProperty.call(patch, 'color')
|
|
4413
|
+
? assertPlanningIdentityColor(patch.color)
|
|
4414
|
+
: assertPlanningIdentityColor(entity.color);
|
|
4415
|
+
}
|
|
4416
|
+
catch (error) {
|
|
4417
|
+
throw new TaskforceRuleError(error instanceof Error ? error.message : 'Planning identity is invalid.', 400, 'PLANNING_IDENTITY_INVALID');
|
|
4418
|
+
}
|
|
4419
|
+
this.writePlanningEntityIdentity({
|
|
4420
|
+
entityType,
|
|
4421
|
+
entityId: normalizedEntityId,
|
|
4422
|
+
icon,
|
|
4423
|
+
color,
|
|
4424
|
+
updatedAt: new Date().toISOString(),
|
|
4425
|
+
}, normalizedWorkspaceId);
|
|
4426
|
+
this.emitRealtimeMutation({
|
|
4427
|
+
workspaceId: normalizedWorkspaceId,
|
|
4428
|
+
entityType,
|
|
4429
|
+
changeType: 'identity-updated',
|
|
4430
|
+
entityIds: [normalizedEntityId],
|
|
4431
|
+
affectedCollections: ['planning'],
|
|
4432
|
+
});
|
|
4433
|
+
return entityType === 'initiative'
|
|
4434
|
+
? this.getInitiative(normalizedEntityId, normalizedWorkspaceId)
|
|
4435
|
+
: this.getWorkstream(normalizedEntityId, normalizedWorkspaceId);
|
|
4436
|
+
}
|
|
4437
|
+
applyPlanningEntityIdentityForSync(identity, workspaceId) {
|
|
4438
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId || undefined);
|
|
4439
|
+
const entityType = identity.entityType === 'workstream' ? 'workstream' : 'initiative';
|
|
4440
|
+
const entityId = String(identity.entityId || '').trim();
|
|
4441
|
+
const updatedAt = String(identity.updatedAt || '').trim();
|
|
4442
|
+
if (!entityId || !Number.isFinite(Date.parse(updatedAt))) {
|
|
4443
|
+
throw new TaskforceRuleError('Planning identity sync payload is invalid.', 400, 'PLANNING_IDENTITY_INVALID');
|
|
4444
|
+
}
|
|
4445
|
+
const entityExists = entityType === 'initiative'
|
|
4446
|
+
? this.initiativeExistsForSync(entityId, normalizedWorkspaceId)
|
|
4447
|
+
: this.workstreamExistsForSync(entityId, normalizedWorkspaceId);
|
|
4448
|
+
if (!entityExists)
|
|
4449
|
+
return null;
|
|
4450
|
+
const incoming = {
|
|
4451
|
+
entityType,
|
|
4452
|
+
entityId,
|
|
4453
|
+
icon: assertPlanningIdentityIcon(identity.icon),
|
|
4454
|
+
color: assertPlanningIdentityColor(identity.color),
|
|
4455
|
+
updatedAt: new Date(Date.parse(updatedAt)).toISOString(),
|
|
4456
|
+
};
|
|
4457
|
+
const existing = this.db.prepare(`
|
|
4458
|
+
SELECT icon, color, updated_at
|
|
4459
|
+
FROM planning_entity_identities
|
|
4460
|
+
WHERE tenant_id = ? AND workspace_id = ? AND entity_type = ? AND entity_id = ?
|
|
4461
|
+
LIMIT 1
|
|
4462
|
+
`).get(this.tenantId, normalizedWorkspaceId, entityType, entityId);
|
|
4463
|
+
if (existing) {
|
|
4464
|
+
const existingUpdatedAt = String(existing.updated_at || '').trim();
|
|
4465
|
+
const timeComparison = incoming.updatedAt.localeCompare(existingUpdatedAt);
|
|
4466
|
+
const incomingTieBreak = `${incoming.icon || ''}\u0000${incoming.color || ''}`;
|
|
4467
|
+
const existingTieBreak = `${String(existing.icon || '')}\u0000${String(existing.color || '')}`;
|
|
4468
|
+
if (timeComparison < 0 || (timeComparison === 0 && incomingTieBreak <= existingTieBreak)) {
|
|
4469
|
+
return {
|
|
4470
|
+
entityType,
|
|
4471
|
+
entityId,
|
|
4472
|
+
icon: assertPlanningIdentityIcon(existing.icon),
|
|
4473
|
+
color: assertPlanningIdentityColor(existing.color),
|
|
4474
|
+
updatedAt: existingUpdatedAt,
|
|
4475
|
+
};
|
|
4476
|
+
}
|
|
4477
|
+
}
|
|
4478
|
+
const applied = this.writePlanningEntityIdentity(incoming, normalizedWorkspaceId);
|
|
4479
|
+
this.emitRealtimeMutation({
|
|
4480
|
+
workspaceId: normalizedWorkspaceId,
|
|
4481
|
+
entityType,
|
|
4482
|
+
changeType: 'identity-updated',
|
|
4483
|
+
entityIds: [entityId],
|
|
4484
|
+
affectedCollections: ['planning'],
|
|
4485
|
+
});
|
|
4486
|
+
return applied;
|
|
4487
|
+
}
|
|
4268
4488
|
listPlanningBootstrap(workspaceId, diagnostics) {
|
|
4269
4489
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
4270
4490
|
const startedAt = Date.now();
|
|
@@ -5302,6 +5522,10 @@ export class TaskforceCore {
|
|
|
5302
5522
|
}
|
|
5303
5523
|
return apply(durableDeleteOwnedKeys);
|
|
5304
5524
|
})(),
|
|
5525
|
+
listDurableDeleteOwnedTaskIds: (workspaceId) => new Set(new WorkspaceSyncDurabilityStore(this.db)
|
|
5526
|
+
.listTaskMetadataOwnership({ tenantId: this.tenantId, workspaceId })
|
|
5527
|
+
.filter((ownership) => ownership.activeDeleteOperation || ownership.deleteOwned)
|
|
5528
|
+
.map((ownership) => ownership.taskId)),
|
|
5305
5529
|
};
|
|
5306
5530
|
}
|
|
5307
5531
|
getSyncReconciliationServiceDeps() {
|
|
@@ -5360,9 +5584,11 @@ export class TaskforceCore {
|
|
|
5360
5584
|
const clauses = [
|
|
5361
5585
|
'tenant_id = ?',
|
|
5362
5586
|
'workspace_id = ?',
|
|
5363
|
-
'is_archived = 0',
|
|
5364
5587
|
];
|
|
5365
5588
|
const params = [this.tenantId, normalizedWorkspaceId];
|
|
5589
|
+
if (!filters.includeArchived) {
|
|
5590
|
+
clauses.push('is_archived = 0');
|
|
5591
|
+
}
|
|
5366
5592
|
if (filters.category) {
|
|
5367
5593
|
clauses.push('category = ?');
|
|
5368
5594
|
params.push(filters.category);
|
|
@@ -5467,6 +5693,114 @@ export class TaskforceCore {
|
|
|
5467
5693
|
};
|
|
5468
5694
|
});
|
|
5469
5695
|
}
|
|
5696
|
+
listTaskCountsByWorkstream(workspaceId, workstreamId) {
|
|
5697
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
5698
|
+
const clauses = [
|
|
5699
|
+
'tenant_id = ?',
|
|
5700
|
+
'workspace_id = ?',
|
|
5701
|
+
'workstream_id IS NOT NULL',
|
|
5702
|
+
"TRIM(workstream_id) <> ''",
|
|
5703
|
+
];
|
|
5704
|
+
const params = [this.tenantId, normalizedWorkspaceId];
|
|
5705
|
+
const normalizedWorkstreamId = this.sanitizeWorkstreamId(workstreamId);
|
|
5706
|
+
if (normalizedWorkstreamId) {
|
|
5707
|
+
clauses.push('workstream_id = ?');
|
|
5708
|
+
params.push(normalizedWorkstreamId);
|
|
5709
|
+
}
|
|
5710
|
+
const rows = this.db.prepare(`
|
|
5711
|
+
SELECT
|
|
5712
|
+
workstream_id,
|
|
5713
|
+
COUNT(*) AS total,
|
|
5714
|
+
SUM(CASE WHEN is_archived = 0 THEN 1 ELSE 0 END) AS active,
|
|
5715
|
+
SUM(CASE WHEN is_archived = 1 THEN 1 ELSE 0 END) AS archived
|
|
5716
|
+
FROM tasks
|
|
5717
|
+
WHERE ${clauses.join(' AND ')}
|
|
5718
|
+
GROUP BY workstream_id
|
|
5719
|
+
`).all(...params);
|
|
5720
|
+
return rows.map((row) => ({
|
|
5721
|
+
workstreamId: String(row.workstream_id || '').trim(),
|
|
5722
|
+
total: Number(row.total || 0),
|
|
5723
|
+
active: Number(row.active || 0),
|
|
5724
|
+
archived: Number(row.archived || 0),
|
|
5725
|
+
}));
|
|
5726
|
+
}
|
|
5727
|
+
listTaskReferenceSummaries(taskIds, workspaceId) {
|
|
5728
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
5729
|
+
const normalizedIds = Array.from(new Set(taskIds.map((id) => String(id || '').trim()).filter(Boolean)));
|
|
5730
|
+
if (normalizedIds.length === 0)
|
|
5731
|
+
return [];
|
|
5732
|
+
const placeholders = normalizedIds.map(() => '?').join(', ');
|
|
5733
|
+
const rows = this.db.prepare(`
|
|
5734
|
+
SELECT id, reference_number, local_reference_number, title, status
|
|
5735
|
+
FROM tasks
|
|
5736
|
+
WHERE tenant_id = ?
|
|
5737
|
+
AND workspace_id = ?
|
|
5738
|
+
AND id IN (${placeholders})
|
|
5739
|
+
`).all(this.tenantId, normalizedWorkspaceId, ...normalizedIds);
|
|
5740
|
+
return rows.map((row) => {
|
|
5741
|
+
const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
|
|
5742
|
+
const localReferenceNumber = this.sanitizeTaskReferenceNumber(row.local_reference_number);
|
|
5743
|
+
return {
|
|
5744
|
+
id: String(row.id || '').trim(),
|
|
5745
|
+
referenceNumber,
|
|
5746
|
+
localReferenceNumber,
|
|
5747
|
+
referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
|
|
5748
|
+
title: String(row.title || ''),
|
|
5749
|
+
status: this.normalizeStatusValue(row.status),
|
|
5750
|
+
};
|
|
5751
|
+
});
|
|
5752
|
+
}
|
|
5753
|
+
searchTaskSummaries(query, scope = 'all', workspaceId) {
|
|
5754
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
5755
|
+
const normalizedQuery = String(query || '').trim();
|
|
5756
|
+
if (!normalizedQuery)
|
|
5757
|
+
return [];
|
|
5758
|
+
const exactReferenceNumber = parseTaskReference(normalizedQuery);
|
|
5759
|
+
const exactLocalReferenceNumber = parseLocalTaskReference(normalizedQuery);
|
|
5760
|
+
const term = `%${normalizedQuery}%`;
|
|
5761
|
+
const clauses = [
|
|
5762
|
+
'tenant_id = ?',
|
|
5763
|
+
'workspace_id = ?',
|
|
5764
|
+
'(title LIKE ? OR description LIKE ? OR id LIKE ? OR reference_number = ? OR local_reference_number = ?)',
|
|
5765
|
+
];
|
|
5766
|
+
const params = [
|
|
5767
|
+
this.tenantId,
|
|
5768
|
+
normalizedWorkspaceId,
|
|
5769
|
+
term,
|
|
5770
|
+
term,
|
|
5771
|
+
term,
|
|
5772
|
+
exactReferenceNumber || -1,
|
|
5773
|
+
exactLocalReferenceNumber || -1,
|
|
5774
|
+
];
|
|
5775
|
+
if (scope === 'active')
|
|
5776
|
+
clauses.push('is_archived = 0');
|
|
5777
|
+
if (scope === 'archive')
|
|
5778
|
+
clauses.push('is_archived = 1');
|
|
5779
|
+
const rows = this.db.prepare(`
|
|
5780
|
+
SELECT id, reference_number, local_reference_number, title, status, assignee, category, priority
|
|
5781
|
+
FROM tasks
|
|
5782
|
+
WHERE ${clauses.join(' AND ')}
|
|
5783
|
+
ORDER BY
|
|
5784
|
+
CASE WHEN id = ? OR reference_number = ? OR local_reference_number = ? THEN 0 ELSE 1 END,
|
|
5785
|
+
created_at DESC
|
|
5786
|
+
LIMIT 50
|
|
5787
|
+
`).all(...params, normalizedQuery, exactReferenceNumber || -1, exactLocalReferenceNumber || -1);
|
|
5788
|
+
return rows.map((row) => {
|
|
5789
|
+
const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
|
|
5790
|
+
const localReferenceNumber = this.sanitizeTaskReferenceNumber(row.local_reference_number);
|
|
5791
|
+
return {
|
|
5792
|
+
id: String(row.id || '').trim(),
|
|
5793
|
+
referenceNumber,
|
|
5794
|
+
localReferenceNumber,
|
|
5795
|
+
referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
|
|
5796
|
+
title: String(row.title || ''),
|
|
5797
|
+
status: this.normalizeStatusValue(row.status),
|
|
5798
|
+
assignee: String(row.assignee || '').trim() || null,
|
|
5799
|
+
category: String(row.category || ''),
|
|
5800
|
+
priority: this.normalizePriorityValue(row.priority),
|
|
5801
|
+
};
|
|
5802
|
+
});
|
|
5803
|
+
}
|
|
5470
5804
|
getDataVersion() {
|
|
5471
5805
|
if (this.db.provider === 'sqlite') {
|
|
5472
5806
|
const version = this.db.pragma('data_version', { simple: true });
|
|
@@ -5511,6 +5845,9 @@ export class TaskforceCore {
|
|
|
5511
5845
|
listDeletedTasks(workspaceId) {
|
|
5512
5846
|
return listDeletedTasksService(this.getDeletedTaskLifecycleServiceDeps(), workspaceId);
|
|
5513
5847
|
}
|
|
5848
|
+
listDeletedTasksReadOnly(workspaceId) {
|
|
5849
|
+
return listDeletedTasksReadOnlyService(this.getDeletedTaskLifecycleServiceDeps(), workspaceId);
|
|
5850
|
+
}
|
|
5514
5851
|
getDeletedTask(id, workspaceId) {
|
|
5515
5852
|
return getDeletedTaskService(this.getDeletedTaskLifecycleServiceDeps(), id, workspaceId);
|
|
5516
5853
|
}
|
|
@@ -5594,11 +5931,13 @@ export class TaskforceCore {
|
|
|
5594
5931
|
const deletedStatsSql = this.db.provider === 'postgres'
|
|
5595
5932
|
? `SELECT count(*) as total,
|
|
5596
5933
|
sum(case when task_snapshot_json::jsonb ->> 'status' = 'done' then 1 else 0 end) as done_count,
|
|
5597
|
-
sum(case when task_snapshot_json::jsonb ->> 'status' = 'cancelled' then 1 else 0 end) as cancelled_count
|
|
5934
|
+
sum(case when task_snapshot_json::jsonb ->> 'status' = 'cancelled' then 1 else 0 end) as cancelled_count,
|
|
5935
|
+
sum(case when coalesce(task_snapshot_json::jsonb ->> 'status', '') not in ('done', 'cancelled') then 1 else 0 end) as other_count
|
|
5598
5936
|
FROM deleted_tasks WHERE tenant_id = ? AND workspace_id = ?`
|
|
5599
5937
|
: `SELECT count(*) as total,
|
|
5600
5938
|
sum(case when json_extract(task_snapshot_json, '$.status') = 'done' then 1 else 0 end) as done_count,
|
|
5601
|
-
sum(case when json_extract(task_snapshot_json, '$.status') = 'cancelled' then 1 else 0 end) as cancelled_count
|
|
5939
|
+
sum(case when json_extract(task_snapshot_json, '$.status') = 'cancelled' then 1 else 0 end) as cancelled_count,
|
|
5940
|
+
sum(case when coalesce(json_extract(task_snapshot_json, '$.status'), '') not in ('done', 'cancelled') then 1 else 0 end) as other_count
|
|
5602
5941
|
FROM deleted_tasks WHERE tenant_id = ? AND workspace_id = ?`;
|
|
5603
5942
|
const deletedStats = this.db.prepare(deletedStatsSql).get(this.tenantId, normalizedWorkspaceId);
|
|
5604
5943
|
return {
|
|
@@ -5612,6 +5951,7 @@ export class TaskforceCore {
|
|
|
5612
5951
|
total: Number(deletedStats.total || 0),
|
|
5613
5952
|
doneCount: Number(deletedStats.done_count || 0),
|
|
5614
5953
|
cancelledCount: Number(deletedStats.cancelled_count || 0),
|
|
5954
|
+
otherCount: Number(deletedStats.other_count || 0),
|
|
5615
5955
|
retentionDays: DELETED_TASK_RETENTION_DAYS
|
|
5616
5956
|
}
|
|
5617
5957
|
};
|
|
@@ -7835,6 +8175,9 @@ export class TaskforceCore {
|
|
|
7835
8175
|
bindMcpOAuthConnectorGrantAiProfile(input) {
|
|
7836
8176
|
return this.mcpTokenService.bindMcpOAuthConnectorGrantAiProfile(input);
|
|
7837
8177
|
}
|
|
8178
|
+
listMcpOAuthRestorableAiProfileIds(input) {
|
|
8179
|
+
return this.mcpTokenService.listMcpOAuthRestorableAiProfileIds(input);
|
|
8180
|
+
}
|
|
7838
8181
|
recordMcpOAuthConnectorGrantUsage(input) {
|
|
7839
8182
|
return this.mcpTokenService.recordMcpOAuthConnectorGrantUsage(input);
|
|
7840
8183
|
}
|
|
@@ -8566,6 +8909,12 @@ export class TaskforceCore {
|
|
|
8566
8909
|
return inferredAssignee || 'unassigned';
|
|
8567
8910
|
})();
|
|
8568
8911
|
const normalizedAttachments = normalizedInput.attachments ?? [];
|
|
8912
|
+
const requestedCardCoverAssetId = typeof normalizedInput.cardCoverAssetId === 'string'
|
|
8913
|
+
? normalizedInput.cardCoverAssetId.trim()
|
|
8914
|
+
: null;
|
|
8915
|
+
if (requestedCardCoverAssetId && !normalizedAttachments.some((item) => (typeof item !== 'string' && item.assetId === requestedCardCoverAssetId && item.linkRole === 'image'))) {
|
|
8916
|
+
throw new TaskforceRuleError('Task card cover must reference an image already attached to this task.', 400, 'TASK_CARD_COVER_INVALID');
|
|
8917
|
+
}
|
|
8569
8918
|
const normalizedComments = this.normalizeIncomingComments(normalizedInput.comments, normalizedWorkspaceId);
|
|
8570
8919
|
const normalizedDescription = this.normalizeGeneratedDescription(normalizedInput.description, createdBySignal);
|
|
8571
8920
|
const workstreamId = this.sanitizeWorkstreamId(normalizedInput.workstreamId);
|
|
@@ -8598,6 +8947,7 @@ export class TaskforceCore {
|
|
|
8598
8947
|
? this.nextWorkstreamTaskOrder(workstreamId, normalizedWorkspaceId)
|
|
8599
8948
|
: null,
|
|
8600
8949
|
workstreamId,
|
|
8950
|
+
cardCoverAssetId: requestedCardCoverAssetId,
|
|
8601
8951
|
checklistItems: Array.isArray(normalizedInput.checklistItems)
|
|
8602
8952
|
? normalizedInput.checklistItems.map((item, index) => ({
|
|
8603
8953
|
id: String(item?.id || this.generateEntityId('checklist')),
|
|
@@ -8628,7 +8978,7 @@ export class TaskforceCore {
|
|
|
8628
8978
|
local_reference_number,
|
|
8629
8979
|
created_at, updated_at, is_archived, taxonomies, created_by, assignee,
|
|
8630
8980
|
scheduled_date, due_date, scheduled_week_key, order_in_day, workstream_order,
|
|
8631
|
-
workstream_id
|
|
8981
|
+
workstream_id, card_cover_asset_id
|
|
8632
8982
|
) VALUES (
|
|
8633
8983
|
@id, @tenantId, @title, @description, @status, @priority, @complexity, @type, @category,
|
|
8634
8984
|
@workspaceId,
|
|
@@ -8636,7 +8986,7 @@ export class TaskforceCore {
|
|
|
8636
8986
|
@localReferenceNumber,
|
|
8637
8987
|
@createdAt, @updatedAt, 0, @taxonomies, @createdBy, @assignee,
|
|
8638
8988
|
@scheduledDate, @dueDate, @scheduledWeekKey, @orderInDay, @workstreamOrder,
|
|
8639
|
-
@workstreamId
|
|
8989
|
+
@workstreamId, @cardCoverAssetId
|
|
8640
8990
|
)
|
|
8641
8991
|
`);
|
|
8642
8992
|
stmt.run({
|
|
@@ -9422,6 +9772,23 @@ export class TaskforceCore {
|
|
|
9422
9772
|
if (Object.prototype.hasOwnProperty.call(patch, 'workstreamOrder')) {
|
|
9423
9773
|
patch.workstreamOrder = this.sanitizeOrderInDay(patch.workstreamOrder);
|
|
9424
9774
|
}
|
|
9775
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'cardCoverAssetId')) {
|
|
9776
|
+
const requestedCoverAssetId = typeof patch.cardCoverAssetId === 'string'
|
|
9777
|
+
? patch.cardCoverAssetId.trim()
|
|
9778
|
+
: null;
|
|
9779
|
+
if (requestedCoverAssetId) {
|
|
9780
|
+
const attachment = (patch.attachments || existing.attachments || []).find((item) => (typeof item !== 'string' && item.assetId === requestedCoverAssetId && item.linkRole === 'image'));
|
|
9781
|
+
if (!attachment) {
|
|
9782
|
+
throw new TaskforceRuleError('Task card cover must reference an image already attached to this task.', 400, 'TASK_CARD_COVER_INVALID');
|
|
9783
|
+
}
|
|
9784
|
+
}
|
|
9785
|
+
patch.cardCoverAssetId = requestedCoverAssetId;
|
|
9786
|
+
}
|
|
9787
|
+
else if (Object.prototype.hasOwnProperty.call(patch, 'attachments') && existing.cardCoverAssetId) {
|
|
9788
|
+
const coverStillAttached = (patch.attachments || []).some((item) => (typeof item !== 'string' && item.assetId === existing.cardCoverAssetId && item.linkRole === 'image'));
|
|
9789
|
+
if (!coverStillAttached)
|
|
9790
|
+
patch.cardCoverAssetId = null;
|
|
9791
|
+
}
|
|
9425
9792
|
const hasScheduledDatePatch = Object.prototype.hasOwnProperty.call(patch, 'scheduledDate');
|
|
9426
9793
|
const hasScheduledWeekKeyPatch = Object.prototype.hasOwnProperty.call(patch, 'scheduledWeekKey');
|
|
9427
9794
|
const hasOrderInDayPatch = Object.prototype.hasOwnProperty.call(patch, 'orderInDay');
|
|
@@ -9486,7 +9853,7 @@ export class TaskforceCore {
|
|
|
9486
9853
|
const mappableFields = [
|
|
9487
9854
|
'title', 'description', 'status', 'completedAt', 'priority', 'complexity', 'type', 'category',
|
|
9488
9855
|
'canceledReason', 'createdBy', 'assignee', 'scheduledDate', 'dueDate',
|
|
9489
|
-
'scheduledWeekKey', 'orderInDay', 'workstreamOrder', 'workstreamId', 'referenceNumber', 'localReferenceNumber'
|
|
9856
|
+
'scheduledWeekKey', 'orderInDay', 'workstreamOrder', 'workstreamId', 'cardCoverAssetId', 'referenceNumber', 'localReferenceNumber'
|
|
9490
9857
|
];
|
|
9491
9858
|
const propertyToColumn = {
|
|
9492
9859
|
canceledReason: 'canceled_reason',
|
|
@@ -9500,6 +9867,7 @@ export class TaskforceCore {
|
|
|
9500
9867
|
orderInDay: 'order_in_day',
|
|
9501
9868
|
workstreamOrder: 'workstream_order',
|
|
9502
9869
|
workstreamId: 'workstream_id',
|
|
9870
|
+
cardCoverAssetId: 'card_cover_asset_id',
|
|
9503
9871
|
};
|
|
9504
9872
|
mappableFields.forEach(f => {
|
|
9505
9873
|
if (patch[f] !== undefined) {
|
|
@@ -10398,6 +10766,123 @@ export class TaskforceCore {
|
|
|
10398
10766
|
listTasksForSync(workspaceId, cursor, limit) {
|
|
10399
10767
|
return listTasksForSyncService(this.getSyncReconciliationServiceDeps(), workspaceId, cursor, limit);
|
|
10400
10768
|
}
|
|
10769
|
+
/**
|
|
10770
|
+
* Full task inventory for authoritative repair without the per-task UI
|
|
10771
|
+
* hydration performed by rowToTask. Repair serializes task roots, comments,
|
|
10772
|
+
* and checklist items separately; task activity, actor decoration, and
|
|
10773
|
+
* resolved attachment bodies are not part of that canonical inventory.
|
|
10774
|
+
*/
|
|
10775
|
+
listTasksForAuthoritativeRepair(workspaceId) {
|
|
10776
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
10777
|
+
const rows = this.db.prepare(`
|
|
10778
|
+
SELECT *
|
|
10779
|
+
FROM tasks
|
|
10780
|
+
WHERE tenant_id = ? AND workspace_id = ?
|
|
10781
|
+
ORDER BY COALESCE(updated_at, created_at) ASC, id ASC
|
|
10782
|
+
`).all(this.tenantId, normalizedWorkspaceId);
|
|
10783
|
+
if (rows.length === 0)
|
|
10784
|
+
return [];
|
|
10785
|
+
const commentsByTaskId = new Map();
|
|
10786
|
+
const checklistByTaskId = new Map();
|
|
10787
|
+
const taskIds = rows.map((row) => String(row.id || '').trim()).filter(Boolean);
|
|
10788
|
+
const batchSize = workspaceSyncV3RepairEntityReadBatchSize(this.db);
|
|
10789
|
+
const normalizedCommentAuthor = new Map();
|
|
10790
|
+
for (let offset = 0; offset < taskIds.length; offset += batchSize) {
|
|
10791
|
+
const batch = taskIds.slice(offset, offset + batchSize);
|
|
10792
|
+
const placeholders = batch.map(() => '?').join(', ');
|
|
10793
|
+
const commentRows = this.db.prepare(`
|
|
10794
|
+
SELECT id, task_id, author, submitted_author, text, timestamp
|
|
10795
|
+
FROM comments
|
|
10796
|
+
WHERE tenant_id = ? AND workspace_id = ? AND task_id IN (${placeholders})
|
|
10797
|
+
ORDER BY task_id ASC, timestamp ASC
|
|
10798
|
+
`).all(this.tenantId, normalizedWorkspaceId, ...batch);
|
|
10799
|
+
for (const comment of commentRows) {
|
|
10800
|
+
const taskId = String(comment.task_id || '').trim();
|
|
10801
|
+
const rawAuthor = String(comment.author || 'user');
|
|
10802
|
+
let author = normalizedCommentAuthor.get(rawAuthor);
|
|
10803
|
+
if (!author) {
|
|
10804
|
+
author = this.normalizeCommentAuthor(rawAuthor, normalizedWorkspaceId);
|
|
10805
|
+
normalizedCommentAuthor.set(rawAuthor, author);
|
|
10806
|
+
}
|
|
10807
|
+
const items = commentsByTaskId.get(taskId) || [];
|
|
10808
|
+
items.push({
|
|
10809
|
+
id: String(comment.id || '').trim(),
|
|
10810
|
+
author,
|
|
10811
|
+
submittedAuthor: String(comment.submitted_author || comment.author || 'user').trim() || 'user',
|
|
10812
|
+
text: String(comment.text || ''),
|
|
10813
|
+
timestamp: String(comment.timestamp || ''),
|
|
10814
|
+
});
|
|
10815
|
+
commentsByTaskId.set(taskId, items);
|
|
10816
|
+
}
|
|
10817
|
+
const checklistRows = this.db.prepare(`
|
|
10818
|
+
SELECT id, task_id, title, is_completed, sort_order, created_at, updated_at
|
|
10819
|
+
FROM checklist_items
|
|
10820
|
+
WHERE tenant_id = ? AND workspace_id = ? AND task_id IN (${placeholders})
|
|
10821
|
+
ORDER BY task_id ASC, sort_order ASC, created_at ASC
|
|
10822
|
+
`).all(this.tenantId, normalizedWorkspaceId, ...batch);
|
|
10823
|
+
for (const checklistRow of checklistRows) {
|
|
10824
|
+
const item = this.rowToChecklistItem(checklistRow);
|
|
10825
|
+
const items = checklistByTaskId.get(item.taskId) || [];
|
|
10826
|
+
items.push(item);
|
|
10827
|
+
checklistByTaskId.set(item.taskId, items);
|
|
10828
|
+
}
|
|
10829
|
+
}
|
|
10830
|
+
const sanitizedAssigneeByValue = new Map();
|
|
10831
|
+
const normalizedCreatedBy = new Map();
|
|
10832
|
+
return rows.map((row) => {
|
|
10833
|
+
const taskId = String(row.id || '').trim();
|
|
10834
|
+
const rawAssignee = String(row.assignee || '');
|
|
10835
|
+
let sanitizedAssignee = sanitizedAssigneeByValue.get(rawAssignee);
|
|
10836
|
+
if (sanitizedAssignee === undefined) {
|
|
10837
|
+
sanitizedAssignee = this.sanitizeAssignee(row.assignee, normalizedWorkspaceId);
|
|
10838
|
+
sanitizedAssigneeByValue.set(rawAssignee, sanitizedAssignee);
|
|
10839
|
+
}
|
|
10840
|
+
const assignee = this.repairTaskAssignee(taskId, normalizedWorkspaceId, row.assignee, sanitizedAssignee) ? sanitizedAssignee : String(row.assignee || '').trim() || 'unassigned';
|
|
10841
|
+
const rawCreatedBy = String(row.created_by || '');
|
|
10842
|
+
if (!normalizedCreatedBy.has(rawCreatedBy)) {
|
|
10843
|
+
normalizedCreatedBy.set(rawCreatedBy, this.normalizeCreatedBy(row.created_by, normalizedWorkspaceId, { preserveGenericAi: true }));
|
|
10844
|
+
}
|
|
10845
|
+
const scheduledDate = this.sanitizeDateOnly(row.scheduled_date);
|
|
10846
|
+
const dueDate = this.sanitizeDateOnly(row.due_date);
|
|
10847
|
+
const scheduledWeekKey = row.scheduled_week_key
|
|
10848
|
+
|| (scheduledDate ? this.dateOnlyToIsoWeekKey(scheduledDate) : null);
|
|
10849
|
+
const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
|
|
10850
|
+
const localReferenceNumber = this.sanitizeTaskReferenceNumber(row.local_reference_number);
|
|
10851
|
+
return {
|
|
10852
|
+
id: taskId,
|
|
10853
|
+
referenceNumber,
|
|
10854
|
+
localReferenceNumber,
|
|
10855
|
+
referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
|
|
10856
|
+
title: row.title,
|
|
10857
|
+
description: row.description,
|
|
10858
|
+
status: this.normalizeStatusValue(row.status),
|
|
10859
|
+
priority: this.normalizePriorityValue(row.priority),
|
|
10860
|
+
complexity: row.complexity,
|
|
10861
|
+
type: row.type,
|
|
10862
|
+
category: row.category,
|
|
10863
|
+
createdAt: row.created_at,
|
|
10864
|
+
updatedAt: row.updated_at,
|
|
10865
|
+
completedAt: row.completed_at,
|
|
10866
|
+
canceledReason: row.canceled_reason,
|
|
10867
|
+
isArchived: row.is_archived === 1,
|
|
10868
|
+
scheduledDate,
|
|
10869
|
+
dueDate,
|
|
10870
|
+
scheduledWeekKey,
|
|
10871
|
+
orderInDay: this.sanitizeOrderInDay(row.order_in_day),
|
|
10872
|
+
workstreamOrder: this.sanitizeOrderInDay(row.workstream_order),
|
|
10873
|
+
workstreamId: this.sanitizeWorkstreamId(row.workstream_id),
|
|
10874
|
+
cardCoverAssetId: typeof row.card_cover_asset_id === 'string' && row.card_cover_asset_id.trim()
|
|
10875
|
+
? row.card_cover_asset_id.trim()
|
|
10876
|
+
: null,
|
|
10877
|
+
taxonomies: JSON.parse(row.taxonomies || '{}'),
|
|
10878
|
+
createdBy: normalizedCreatedBy.get(rawCreatedBy),
|
|
10879
|
+
assignee,
|
|
10880
|
+
comments: commentsByTaskId.get(taskId) || [],
|
|
10881
|
+
attachments: [],
|
|
10882
|
+
checklistItems: checklistByTaskId.get(taskId) || [],
|
|
10883
|
+
};
|
|
10884
|
+
});
|
|
10885
|
+
}
|
|
10401
10886
|
listWorkspaceSyncDeletesSince(workspaceId, cursor, limit) {
|
|
10402
10887
|
return listWorkspaceSyncDeletesSinceService(this.getSyncReconciliationServiceDeps(), workspaceId, cursor, limit);
|
|
10403
10888
|
}
|