@taskforcehq/taskforce 0.3.304 → 0.3.306
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -247
- package/dist/Taskforce.module.css +295 -89
- package/dist/TaskforceCore.js +378 -111
- package/dist/TaskforceCore.test.js +1221 -107
- package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/assets/fonts/Noir_medium.woff2 +0 -0
- package/dist/assets/fonts/Noir_regular.woff2 +0 -0
- package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/compat/workspaceSyncCompat.js +52 -2
- package/dist/compat/workspaceSyncCompat.test.js +38 -1
- package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
- package/dist/components/context/ContextAttachmentManager.js +549 -0
- package/dist/components/features/AgentsModule.d.ts +10 -1
- package/dist/components/features/AgentsModule.js +260 -15
- package/dist/components/features/AgentsModule.test.js +255 -5
- package/dist/components/features/DocumentIndex.d.ts +1 -1
- package/dist/components/features/DocumentIndex.js +1 -1
- package/dist/components/features/DocumentViewer.d.ts +6 -2
- package/dist/components/features/DocumentViewer.js +90 -10
- package/dist/components/features/DocumentWorkspace.d.ts +5 -22
- package/dist/components/features/DocumentWorkspace.js +12 -155
- package/dist/components/features/DocumentWorkspace.test.js +226 -6
- package/dist/components/features/TaskSettings.js +29 -3
- package/dist/components/features/TaskSettings.test.js +42 -1
- package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
- package/dist/components/features/documentWorkspaceModel.js +57 -0
- package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
- package/dist/components/features/useDocumentWorkspaceController.js +113 -0
- package/dist/components/task/TaskCard.js +19 -28
- package/dist/components/task/TaskCard.test.js +38 -3
- package/dist/components/task/TaskContextUpload.js +4 -526
- package/dist/components/task/TaskForm.d.ts +8 -8
- package/dist/components/task/TaskForm.js +362 -230
- package/dist/components/task/TaskForm.test.js +558 -381
- package/dist/components/task/TaskKanban.d.ts +2 -1
- package/dist/components/task/TaskKanban.js +96 -14
- package/dist/components/task/TaskKanban.test.js +36 -0
- package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
- package/dist/components/ui/EditableAvatarButton.js +18 -0
- package/dist/components/views/PlanComparisonPage.d.ts +9 -1
- package/dist/components/views/PlanComparisonPage.js +70 -17
- package/dist/components/views/PlanComparisonPage.test.js +289 -41
- package/dist/components/views/PlansPage.js +239 -106
- package/dist/components/views/PlansPage.test.js +593 -42
- package/dist/components/views/StandaloneLayout.js +310 -554
- package/dist/components/views/panels/FilterToolbar.test.js +6 -4
- package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
- package/dist/components/views/panels/PlanningDrawer.js +18 -11
- package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
- package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
- package/dist/components/views/planningScope.d.ts +20 -0
- package/dist/components/views/planningScope.js +25 -0
- package/dist/components/views/planningScope.test.d.ts +1 -0
- package/dist/components/views/planningScope.test.js +60 -0
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +27 -14
- package/dist/components/views/standalone/modals/AccountHubModal.js +87 -15
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +42 -2
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
- package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
- package/dist/config/envSchema.js +1 -1
- package/dist/core/AiProfileService.d.ts +45 -3
- package/dist/core/AiProfileService.js +774 -63
- package/dist/core/AiProfileService.test.js +22 -2
- package/dist/core/AiProfiles.test.js +761 -2
- package/dist/core/AttachmentLinkService.js +57 -15
- package/dist/core/AuthIdentityService.d.ts +107 -0
- package/dist/core/AuthIdentityService.js +284 -0
- package/dist/core/AuthTokenService.d.ts +4 -0
- package/dist/core/AuthTokenService.js +34 -6
- package/dist/core/AuthTokenService.test.d.ts +1 -0
- package/dist/core/AuthTokenService.test.js +78 -0
- package/dist/core/EntitlementsPolicy.test.js +77 -15
- package/dist/core/GlobalSettingsService.js +115 -6
- package/dist/core/PlanEntitlementService.d.ts +30 -3
- package/dist/core/PlanEntitlementService.js +454 -93
- package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
- package/dist/core/PlanFeatureCatalog.test.js +84 -0
- package/dist/core/PlanVersionPolicy.test.js +54 -28
- package/dist/core/PlanningEntities.test.js +52 -0
- package/dist/core/SignupMonetizationSettings.test.js +241 -44
- package/dist/core/SyncReconciliationService.js +2 -65
- package/dist/core/SystemAdmin.test.js +244 -88
- package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
- package/dist/core/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +228 -14
- package/dist/core/Taskforce.js +814 -201
- package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
- package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
- package/dist/core/WorkspaceLifecycleService.js +0 -5
- package/dist/core/WorkspacePermissions.test.js +25 -1
- package/dist/core/WorkspacePlanMode.test.js +128 -27
- package/dist/core/shared.d.ts +3 -1
- package/dist/core/shared.js +16 -1
- package/dist/core/types.d.ts +68 -2
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
- package/dist/hooks/sync/auth.js +3 -1
- package/dist/hooks/sync/bootstrap.js +2 -0
- package/dist/hooks/sync/controlPlane.d.ts +37 -0
- package/dist/hooks/sync/controlPlane.js +78 -0
- package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
- package/dist/hooks/sync/controlPlane.test.js +121 -0
- package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
- package/dist/hooks/sync/lifecyclePolicy.js +93 -0
- package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
- package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
- package/dist/hooks/sync/orchestratorShared.js +68 -47
- package/dist/hooks/sync/orchestratorShared.test.js +50 -1
- package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
- package/dist/hooks/sync/pullLifecycle.js +24 -0
- package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
- package/dist/hooks/sync/pullLifecycle.test.js +80 -0
- package/dist/hooks/sync/recovery.d.ts +16 -2
- package/dist/hooks/sync/recovery.js +171 -53
- package/dist/hooks/sync/recovery.test.d.ts +1 -0
- package/dist/hooks/sync/recovery.test.js +292 -0
- package/dist/hooks/sync/transfers.d.ts +16 -3
- package/dist/hooks/sync/transfers.js +186 -65
- package/dist/hooks/sync/transfers.test.d.ts +1 -0
- package/dist/hooks/sync/transfers.test.js +396 -0
- package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
- package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
- package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
- package/dist/hooks/sync/useSyncStatusActions.js +88 -0
- package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
- package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
- package/dist/hooks/sync/useSyncStatusControls.js +60 -0
- package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +16 -6
- package/dist/hooks/useSyncOrchestrator.js +424 -175
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +487 -10
- package/dist/hooks/useTaskData.js +8 -3
- package/dist/hooks/useTaskData.test.js +45 -0
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +12 -1
- package/dist/hooks/useTaskMutations.test.js +30 -1
- package/dist/hooks/useTaskforce.d.ts +65 -4
- package/dist/hooks/useTaskforce.js +564 -243
- package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +2454 -208
- package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
- package/dist/hooks/useWorkspaceSyncController.js +17 -2
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
- package/dist/localization/locales/en-US.d.ts +0 -1
- package/dist/localization/locales/en-US.js +0 -1
- package/dist/localization/locales/es-419.js +0 -1
- package/dist/localization/locales/pt-BR.js +0 -1
- package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- package/dist/mcp/collaborationRegistrar.d.ts +2 -0
- package/dist/mcp/collaborationRegistrar.js +71 -0
- package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
- package/dist/mcp/documentAssetRegistrar.js +346 -0
- package/dist/mcp/runtime.d.ts +2 -0
- package/dist/mcp/runtime.js +2433 -3534
- package/dist/mcp/runtime.test.js +440 -2
- package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
- package/dist/mcp/taskPlanningRegistrar.js +418 -0
- package/dist/mcp/toolCatalog.d.ts +35 -0
- package/dist/mcp/toolCatalog.js +3 -0
- package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
- package/dist/migrations/taskSchemaMigrations.js +171 -61
- package/dist/migrations/taskSchemaMigrations.test.js +15 -0
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/dist/resources/templates/workflows/collaborate.yaml +1 -1
- package/dist/resources/templates/workflows/evaluate.yaml +2 -2
- package/dist/resources/templates/workflows/plan.yaml +1 -1
- package/dist/resources/templates/workflows/review.yaml +2 -2
- package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
- package/dist/server/auth/providers/apple.d.ts +33 -0
- package/dist/server/auth/providers/apple.js +82 -0
- package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
- package/dist/server/auth/providers/appleClientSecret.js +65 -0
- package/dist/server/auth/providers/github.d.ts +26 -0
- package/dist/server/auth/providers/github.js +86 -0
- package/dist/server/auth/providers/google.d.ts +21 -0
- package/dist/server/auth/providers/google.js +56 -0
- package/dist/server/auth/providers/types.d.ts +21 -0
- package/dist/server/auth/providers/types.js +1 -0
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.js +6 -3
- package/dist/server/documentReviewRoutes.test.js +36 -3
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.js +120 -9
- package/dist/server/index.rateLimit.test.js +3 -0
- package/dist/server/index.test.js +106 -3
- package/dist/server/routes/admin.d.ts +4 -5
- package/dist/server/routes/admin.js +410 -80
- package/dist/server/routes/annotatedAttachments.d.ts +1 -1
- package/dist/server/routes/annotatedAttachments.js +1 -1
- package/dist/server/routes/auth.d.ts +16 -6
- package/dist/server/routes/auth.js +1015 -118
- package/dist/server/routes/authSupport.d.ts +30 -0
- package/dist/server/routes/authSupport.js +81 -0
- package/dist/server/routes/billing.d.ts +1 -1
- package/dist/server/routes/billing.js +276 -34
- package/dist/server/routes/billing.test.js +760 -52
- package/dist/server/routes/documentReviews.d.ts +1 -1
- package/dist/server/routes/documentReviews.js +1 -1
- package/dist/server/routes/documents.d.ts +5 -2
- package/dist/server/routes/documents.js +242 -74
- package/dist/server/routes/primitives.d.ts +11 -0
- package/dist/server/routes/primitives.js +73 -0
- package/dist/server/routes/resources.d.ts +1 -1
- package/dist/server/routes/resources.js +1 -1
- package/dist/server/routes/shared.d.ts +29 -3
- package/dist/server/routes/shared.js +69 -4
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.integration.test.js +437 -39
- package/dist/server/routes/sync.js +62 -606
- package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
- package/dist/server/routes/syncAuxRoutes.js +91 -0
- package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
- package/dist/server/routes/syncAuxRoutes.test.js +158 -0
- package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
- package/dist/server/routes/syncPullApplyRoutes.js +204 -0
- package/dist/server/routes/syncPushRoutes.d.ts +24 -0
- package/dist/server/routes/syncPushRoutes.js +212 -0
- package/dist/server/routes/syncRouteGuards.d.ts +36 -0
- package/dist/server/routes/syncRouteGuards.js +67 -0
- package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
- package/dist/server/routes/syncRouteGuards.test.js +94 -0
- package/dist/server/routes/syncRouteTypes.d.ts +13 -0
- package/dist/server/routes/syncRouteTypes.js +1 -0
- package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
- package/dist/server/routes/syncSnapshotRoutes.js +180 -0
- package/dist/server/routes/tasks.d.ts +1 -1
- package/dist/server/routes/tasks.js +44 -2
- package/dist/server/routes/workspaces.d.ts +1 -1
- package/dist/server/routes/workspaces.js +37 -6
- package/dist/server/routes.d.ts +2 -9
- package/dist/server/routes.js +136 -165
- package/dist/server/routes.test.js +2457 -162
- package/dist/services/workflowExportSnapshots.test.js +2 -0
- package/dist/shared/aiProfileSeatScope.d.ts +5 -0
- package/dist/shared/aiProfileSeatScope.js +21 -0
- package/dist/shared/passwordPolicy.d.ts +13 -0
- package/dist/shared/passwordPolicy.js +84 -0
- package/dist/shared/passwordPolicy.test.d.ts +1 -0
- package/dist/shared/passwordPolicy.test.js +34 -0
- package/dist/storage/workspaceAssetStore.d.ts +15 -3
- package/dist/storage/workspaceAssetStore.js +132 -44
- package/dist/storage/workspaceAssetStore.test.js +93 -6
- package/dist/styles/fonts.css +70 -0
- package/dist/sync/collaborationSyncPayload.d.ts +6 -1
- package/dist/sync/collaborationSyncPayload.js +17 -11
- package/dist/sync/collaborationSyncPayload.test.js +13 -1
- package/dist/sync/contentSyncState.d.ts +4 -8
- package/dist/sync/contentSyncState.js +33 -77
- package/dist/sync/syncApplyHandlers.d.ts +7 -0
- package/dist/sync/syncApplyHandlers.js +28 -13
- package/dist/sync/syncApplyHandlers.test.js +72 -3
- package/dist/sync/syncFieldSemantics.d.ts +12 -0
- package/dist/sync/syncFieldSemantics.js +55 -0
- package/dist/sync/syncResourceAdapters.d.ts +44 -0
- package/dist/sync/syncResourceAdapters.js +77 -0
- package/dist/sync/syncService.d.ts +6 -0
- package/dist/sync/syncService.js +60 -0
- package/dist/sync/syncService.test.js +71 -3
- package/dist/sync/taskSyncChangeKey.d.ts +2 -0
- package/dist/sync/taskSyncChangeKey.js +143 -0
- package/dist/sync/taskSyncPayload.js +3 -8
- package/dist/sync/workspacePullFeed.js +4 -3
- package/dist/sync/workspacePullFeed.test.js +43 -0
- package/dist/sync/workspaceRepair.js +5 -2
- package/dist/sync/workspaceSyncModel.d.ts +48 -0
- package/dist/sync/workspaceSyncModel.js +195 -31
- package/dist/sync/workspaceSyncModel.test.js +325 -12
- package/dist/sync/workspaceSyncState.d.ts +19 -0
- package/dist/sync/workspaceSyncState.js +3 -1
- package/dist/sync/workspaceSyncSurface.js +3 -8
- package/dist/types.d.ts +17 -0
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
- package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
- package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
- package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
- package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-9E6ParrY.js} +1 -1
- package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
- package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
- package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
- package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
- package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
- package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
- package/dist/ui/assets/TaskSettings-DZX4jk7e.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BA7jcEpH.js} +1 -1
- package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
- package/dist/ui/assets/index-C6k75jr8.js +6 -0
- package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-DuEd_65c.js} +1 -1
- package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/ui/fonts/Noir_medium.otf +0 -0
- package/dist/ui/fonts/Noir_medium.woff2 +0 -0
- package/dist/ui/fonts/Noir_regular.otf +0 -0
- package/dist/ui/fonts/Noir_regular.woff2 +0 -0
- package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
- package/dist/utils/assignees.d.ts +2 -0
- package/dist/utils/assignees.js +2 -0
- package/dist/utils/avatarUpload.d.ts +27 -0
- package/dist/utils/avatarUpload.js +132 -0
- package/dist/utils/avatarUpload.test.d.ts +1 -0
- package/dist/utils/avatarUpload.test.js +40 -0
- package/dist/utils/bootstrapTrace.d.ts +7 -0
- package/dist/utils/bootstrapTrace.js +29 -0
- package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +199 -0
- package/dist/utils/contextAssetEvents.d.ts +2 -0
- package/dist/utils/syncEventDetails.d.ts +3 -0
- package/dist/utils/syncEventDetails.js +64 -0
- package/dist/utils/syncEventDetails.test.d.ts +1 -0
- package/dist/utils/syncEventDetails.test.js +23 -0
- package/dist/utils/syncEventPresentation.d.ts +15 -0
- package/dist/utils/syncEventPresentation.js +129 -0
- package/dist/utils/syncEventPresentation.test.d.ts +1 -0
- package/dist/utils/syncEventPresentation.test.js +64 -0
- package/dist/utils/syncStatusPresentation.d.ts +72 -0
- package/dist/utils/syncStatusPresentation.js +217 -0
- package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
- package/dist/utils/syncStatusPresentation.test.js +164 -0
- package/dist/utils/taskActivity.d.ts +2 -0
- package/dist/utils/taskActivity.js +228 -22
- package/dist/utils/taskActivity.test.js +119 -8
- package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
- package/dist/utils/workspaceSyncPresentation.js +68 -2
- package/dist/utils/workspaceSyncPresentation.test.js +36 -1
- package/package.json +10 -2
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/export-sqlite-to-postgres.mjs +7 -9
- package/scripts/playwright-auth.sh +5 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- package/scripts/run-e2e-realtime.sh +17 -3
- package/scripts/run-smoke.sh +17 -5
- package/scripts/run-soak.sh +17 -5
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/src/resources/templates/workflows/collaborate.yaml +1 -1
- package/src/resources/templates/workflows/evaluate.yaml +2 -2
- package/src/resources/templates/workflows/plan.yaml +1 -1
- package/src/resources/templates/workflows/review.yaml +2 -2
- package/dist/ui/assets/AgentsModule-BLWVbuKP.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
- package/dist/ui/assets/index-DXUdtH1B.js +0 -6
- package/dist/ui/assets/index-Ii0B0rTO.css +0 -1
- package/scripts/migrate-planning-model.ts +0 -233
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* === SECTION: Navigation === Line ~3410 — setActiveTab, handleNavigation
|
|
28
28
|
* === SECTION: Form === Line ~3489 — resetForm, handleEdit, handleSubmit, saveTask
|
|
29
29
|
* === SECTION: Task CRUD === Line ~3691 — handleDelete, handleUpdateTask, handleToggle*
|
|
30
|
-
* === SECTION:
|
|
30
|
+
* === SECTION: Planning Links === Line ~4129 — workstream and initiative link handlers
|
|
31
31
|
* === SECTION: Category/Type Mgmt === Line ~4462 — handleSaveCategory, handleRemoveCategory
|
|
32
32
|
* === SECTION: Settings Model === Line ~4625 — settingsModel object
|
|
33
33
|
* === SECTION: Return Value === Line ~4817 — final return object (public API shape)
|
|
@@ -43,6 +43,7 @@ import { DEFAULT_TYPE_VALUE } from '../shared/taxonomyDefaults.js';
|
|
|
43
43
|
import { cloneDefaultWorkflowDefinitions } from '../shared/workflowDefaults.js';
|
|
44
44
|
import { resolveClientDeploymentConfig } from '../config/clientDeployment';
|
|
45
45
|
import { resolveTaskforceMcpBaseUrlFromBaseUrl } from '../config/cloudEnvironment';
|
|
46
|
+
import { buildBootstrapTraceId, withBootstrapTraceHeader } from '../utils/bootstrapTrace';
|
|
46
47
|
import { createDefaultAssigneeOptions } from '../utils/assignees';
|
|
47
48
|
import { persistLocalMutationActorUserId, readLocalMutationActorUserId } from '../utils/taskforceApiClient';
|
|
48
49
|
import { useSyncOrchestrator } from './useSyncOrchestrator';
|
|
@@ -67,6 +68,13 @@ import { useTaskforceAuthBootstrap } from './auth/useTaskforceAuthBootstrap';
|
|
|
67
68
|
import { deriveAuthGuardPolicy } from './auth/useAuthGuardPolicy';
|
|
68
69
|
import { useTaskforceWorkspaceBootstrap } from './workspace/useTaskforceWorkspaceBootstrap';
|
|
69
70
|
import { useDataVersionRefresh } from './sync/useDataVersionRefresh';
|
|
71
|
+
function upsertEntityById(items, nextItem) {
|
|
72
|
+
const existingIndex = items.findIndex((item) => item.id === nextItem.id);
|
|
73
|
+
if (existingIndex === -1) {
|
|
74
|
+
return [...items, nextItem];
|
|
75
|
+
}
|
|
76
|
+
return items.map((item, index) => (index === existingIndex ? nextItem : item));
|
|
77
|
+
}
|
|
70
78
|
const ENV_DEPLOYMENT = resolveClientDeploymentConfig(import.meta.env);
|
|
71
79
|
const DEFAULT_CLOUD_APP_ORIGIN = (() => {
|
|
72
80
|
const configuredBase = ENV_DEPLOYMENT.baseUrl;
|
|
@@ -82,6 +90,75 @@ const DEFAULT_CLOUD_APP_ORIGIN = (() => {
|
|
|
82
90
|
return 'https://app.taskforcehq.ai';
|
|
83
91
|
})();
|
|
84
92
|
const BOOTSTRAP_REQUEST_TIMEOUT_MS = 12_000;
|
|
93
|
+
const PLANNING_BOOTSTRAP_REQUEST_TIMEOUT_MS = 20_000;
|
|
94
|
+
const EXPECTED_WORKSPACE_ABORT_REASONS = new Set([
|
|
95
|
+
'external-abort',
|
|
96
|
+
'superseded',
|
|
97
|
+
'workspace-reset',
|
|
98
|
+
'workspace-transition',
|
|
99
|
+
]);
|
|
100
|
+
function hasAbortErrorName(error) {
|
|
101
|
+
if (typeof DOMException !== 'undefined' && error instanceof DOMException) {
|
|
102
|
+
return error.name === 'AbortError';
|
|
103
|
+
}
|
|
104
|
+
return String(error?.name || '').toLowerCase() === 'aborterror';
|
|
105
|
+
}
|
|
106
|
+
function isExpectedWorkspaceAbortReason(reason) {
|
|
107
|
+
return typeof reason === 'string' && EXPECTED_WORKSPACE_ABORT_REASONS.has(reason);
|
|
108
|
+
}
|
|
109
|
+
function isExpectedWorkspaceAbortFailure(error, signal) {
|
|
110
|
+
if (!signal.aborted)
|
|
111
|
+
return false;
|
|
112
|
+
if (error === signal.reason)
|
|
113
|
+
return true;
|
|
114
|
+
if (isExpectedWorkspaceAbortReason(error) || isExpectedWorkspaceAbortReason(signal.reason))
|
|
115
|
+
return true;
|
|
116
|
+
return hasAbortErrorName(error);
|
|
117
|
+
}
|
|
118
|
+
function createBootstrapTimeoutError(timeoutMs) {
|
|
119
|
+
const timeoutError = new Error(`Startup checks timed out after ${timeoutMs}ms.`);
|
|
120
|
+
timeoutError.name = 'BootstrapTimeoutError';
|
|
121
|
+
return timeoutError;
|
|
122
|
+
}
|
|
123
|
+
function sanitizePlanningBootstrapPayload(input) {
|
|
124
|
+
const payload = input && typeof input === 'object' ? input : {};
|
|
125
|
+
const initiatives = Array.isArray(payload.initiatives)
|
|
126
|
+
? payload.initiatives.filter((item) => Boolean(item && typeof item === 'object' && typeof item.id === 'string'))
|
|
127
|
+
: [];
|
|
128
|
+
const workstreams = Array.isArray(payload.workstreams)
|
|
129
|
+
? payload.workstreams.filter((item) => Boolean(item && typeof item === 'object' && typeof item.id === 'string'))
|
|
130
|
+
: [];
|
|
131
|
+
const workstreamTaskSummaries = Array.isArray(payload.workstreamTaskSummaries)
|
|
132
|
+
? payload.workstreamTaskSummaries.flatMap((entry) => {
|
|
133
|
+
if (!entry || typeof entry !== 'object')
|
|
134
|
+
return [];
|
|
135
|
+
const normalizedWorkstreamId = String(entry.workstreamId || '').trim();
|
|
136
|
+
if (!normalizedWorkstreamId)
|
|
137
|
+
return [];
|
|
138
|
+
const rawTasks = Array.isArray(entry.tasks) ? entry.tasks : [];
|
|
139
|
+
const taskCount = Math.max(0, Number(entry.taskCount) || 0);
|
|
140
|
+
const completedTaskCount = Math.max(0, Number(entry.completedTaskCount) || 0);
|
|
141
|
+
return [{
|
|
142
|
+
workstreamId: normalizedWorkstreamId,
|
|
143
|
+
taskCount,
|
|
144
|
+
completedTaskCount,
|
|
145
|
+
tasks: rawTasks
|
|
146
|
+
.filter((task) => Boolean(task && typeof task === 'object' && typeof task.id === 'string'))
|
|
147
|
+
.map((task) => ({
|
|
148
|
+
id: String(task.id || '').trim(),
|
|
149
|
+
referenceNumber: typeof task.referenceNumber === 'number' ? task.referenceNumber : null,
|
|
150
|
+
title: String(task.title || '').trim() || 'Untitled Task',
|
|
151
|
+
status: typeof task.status === 'string' ? task.status : null,
|
|
152
|
+
})),
|
|
153
|
+
}];
|
|
154
|
+
})
|
|
155
|
+
: [];
|
|
156
|
+
return {
|
|
157
|
+
initiatives,
|
|
158
|
+
workstreams,
|
|
159
|
+
workstreamTaskSummaries,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
85
162
|
export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, onClose }) {
|
|
86
163
|
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
87
164
|
const { categories, types, apiEndpoint, apiBaseUrl, cloudAuthBaseUrl, cloudMcpBaseUrl, wsBaseUrl, shortcut: configShortcut } = mergedConfig;
|
|
@@ -131,6 +208,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
131
208
|
const workspaceMode = workspaceModeRaw === 'single-local' || workspaceModeRaw === 'multi-cloud'
|
|
132
209
|
? workspaceModeRaw
|
|
133
210
|
: (runtimeModeHint === 'cloud' ? 'multi-cloud' : 'single-local');
|
|
211
|
+
setRuntimeMode(runtimeModeHint);
|
|
134
212
|
setRuntimeConfigOverride({
|
|
135
213
|
cloudEnvironment: typeof cfg.cloudEnvironment === 'string'
|
|
136
214
|
? String(cfg.cloudEnvironment).trim().toLowerCase()
|
|
@@ -175,10 +253,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
175
253
|
const propApiBaseUrl = normalizeBaseUrl(apiBaseUrl);
|
|
176
254
|
const propCloudAuthBaseUrl = normalizeBaseUrl(cloudAuthBaseUrl);
|
|
177
255
|
const propCloudMcpBaseUrl = normalizeBaseUrl(cloudMcpBaseUrl);
|
|
178
|
-
const explicitConfiguredApiBaseUrl = propApiBaseUrl || envApiBaseUrl || envBaseUrl;
|
|
179
256
|
const runtimeResolvedApiBaseUrl = runtimeConfigOverride.apiBaseUrl || runtimeConfigOverride.baseUrl;
|
|
180
|
-
const
|
|
257
|
+
const envConfiguredApiBaseUrl = isLoopbackHost ? '' : (envApiBaseUrl || envBaseUrl);
|
|
258
|
+
const normalizedApiBaseUrl = propApiBaseUrl
|
|
181
259
|
|| runtimeResolvedApiBaseUrl
|
|
260
|
+
|| envConfiguredApiBaseUrl
|
|
182
261
|
|| '';
|
|
183
262
|
const explicitConfiguredCloudAuthBaseUrl = propCloudAuthBaseUrl || envCloudAuthBaseUrl || envBaseUrl;
|
|
184
263
|
const runtimeResolvedCloudAuthBaseUrl = runtimeConfigOverride.cloudAuthBaseUrl || runtimeConfigOverride.baseUrl;
|
|
@@ -206,10 +285,14 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
206
285
|
|| normalizedCloudAuthBaseUrl
|
|
207
286
|
|| defaultCloudAuthBaseUrl).trim().replace(/\/+$/, '');
|
|
208
287
|
const authOnlyCloudMode = !isCloudHost && Boolean(normalizedCloudAuthBaseUrl) && !normalizedApiBaseUrl;
|
|
209
|
-
|
|
288
|
+
// Real cloud hosts must defer protected bootstrap calls immediately, even before
|
|
289
|
+
// runtime-config finishes, or the one-shot initial bootstrap can consume itself
|
|
290
|
+
// on unauthenticated /config, /taxonomy-state, and /planning/bootstrap reads.
|
|
291
|
+
const shouldProbeCloudAuth = isCloudHost || (runtimeConfigReady && Boolean(normalizedCloudAuthBaseUrl || normalizedApiBaseUrl));
|
|
210
292
|
const shouldGateProtectedApiCalls = shouldProbeCloudAuth && !authOnlyCloudMode;
|
|
211
293
|
const cloudAuthConfigured = Boolean(normalizedCloudAuthBaseUrl || normalizedApiBaseUrl || isCloudHost);
|
|
212
294
|
const initialRuntimeMode = isCloudHost ? 'cloud' : 'local';
|
|
295
|
+
const [runtimeMode, setRuntimeMode] = useState(() => initialRuntimeMode);
|
|
213
296
|
const optimisticLocalAuthUserId = initialRuntimeMode === 'local' && isLoopbackHost && cloudAuthConfigured
|
|
214
297
|
? readLocalMutationActorUserId()
|
|
215
298
|
: '';
|
|
@@ -235,8 +318,16 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
235
318
|
|| path.startsWith('/api/taskforce/billing/')
|
|
236
319
|
|| path.startsWith('/api/taskforce/sync/')
|
|
237
320
|
|| path.startsWith('/api/taskforce/settings/mcp/');
|
|
238
|
-
if (!isCloudHost && isCloudScopedPath
|
|
239
|
-
|
|
321
|
+
if (!isCloudHost && isCloudScopedPath) {
|
|
322
|
+
if (runtimeMode === 'cloud') {
|
|
323
|
+
return path;
|
|
324
|
+
}
|
|
325
|
+
if (!runtimeConfigReady || runtimeConfigOverride.cloudAuthViaLocalProxy) {
|
|
326
|
+
return path;
|
|
327
|
+
}
|
|
328
|
+
if (runtimeConfigOverride.workspaceMode === 'multi-cloud') {
|
|
329
|
+
return path;
|
|
330
|
+
}
|
|
240
331
|
}
|
|
241
332
|
const base = normalizedCloudAuthBaseUrl || normalizedApiBaseUrl;
|
|
242
333
|
if (base) {
|
|
@@ -258,7 +349,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
258
349
|
if (!isCloudHost && isCloudScopedPath)
|
|
259
350
|
return path;
|
|
260
351
|
return path;
|
|
261
|
-
}, [runtimeConfigReady, runtimeConfigOverride.cloudAuthViaLocalProxy, normalizedCloudAuthBaseUrl, normalizedApiBaseUrl, isCloudHost]);
|
|
352
|
+
}, [runtimeConfigReady, runtimeConfigOverride.cloudAuthViaLocalProxy, runtimeConfigOverride.workspaceMode, normalizedCloudAuthBaseUrl, normalizedApiBaseUrl, isCloudHost, runtimeMode]);
|
|
262
353
|
const resolveWebSocketUrl = useCallback((path) => {
|
|
263
354
|
const wsPath = String(path || '').trim() || '/taskforce-ws';
|
|
264
355
|
const normalizedPath = wsPath.startsWith('/') ? wsPath : `/${wsPath}`;
|
|
@@ -308,7 +399,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
308
399
|
}), [projectRoot, initialRuntimeMode]);
|
|
309
400
|
const [mcpScriptPath, setMcpScriptPath] = useState('');
|
|
310
401
|
const [tenantId, setTenantId] = useState('');
|
|
311
|
-
const [runtimeMode, setRuntimeMode] = useState(() => initialRuntimeMode);
|
|
312
402
|
const runtimeContractFallback = resolveRuntimeModeContract(runtimeMode);
|
|
313
403
|
const workspaceMode = runtimeConfigOverride.workspaceMode === 'single-local' || runtimeConfigOverride.workspaceMode === 'multi-cloud'
|
|
314
404
|
? runtimeConfigOverride.workspaceMode
|
|
@@ -323,6 +413,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
323
413
|
const [authWorkspaceId, setAuthWorkspaceId] = useState('');
|
|
324
414
|
const [authUserEmail, setAuthUserEmail] = useState('');
|
|
325
415
|
const [authUserDisplayName, setAuthUserDisplayName] = useState('');
|
|
416
|
+
const [availableAuthProviders, setAvailableAuthProviders] = useState([]);
|
|
326
417
|
const [authUserAvatarUrl, setAuthUserAvatarUrl] = useState('');
|
|
327
418
|
const [hasBetaAccess, setHasBetaAccess] = useState(true);
|
|
328
419
|
const [hydratedWorkspaceRole, setHydratedWorkspaceRole] = useState(null);
|
|
@@ -353,6 +444,12 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
353
444
|
const currentWorkspaceIdRef = useRef(currentWorkspaceId);
|
|
354
445
|
const explicitWorkspaceSelectionRef = useRef(null);
|
|
355
446
|
const workspaceTransitionEpochRef = useRef(0);
|
|
447
|
+
const bootstrapTraceSeedRef = useRef((() => {
|
|
448
|
+
const randomPart = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
449
|
+
? crypto.randomUUID()
|
|
450
|
+
: `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
451
|
+
return randomPart.replace(/[^A-Za-z0-9_-]+/g, '').slice(0, 12) || 'session';
|
|
452
|
+
})());
|
|
356
453
|
const [workspaceTransitionEpoch, setWorkspaceTransitionEpoch] = useState(0);
|
|
357
454
|
const bootstrapConfigAbortRef = useRef(null);
|
|
358
455
|
const referenceDataAbortRef = useRef(null);
|
|
@@ -360,6 +457,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
360
457
|
const assigneeOptionsAbortRef = useRef(null);
|
|
361
458
|
const deletedTasksAbortRef = useRef(null);
|
|
362
459
|
const planningEntitiesAbortRef = useRef(null);
|
|
460
|
+
const planningEntitiesRetryTimerRef = useRef(null);
|
|
363
461
|
const commitCurrentWorkspaceId = useCallback((nextWorkspaceIdRaw, options) => {
|
|
364
462
|
const nextWorkspaceId = String(nextWorkspaceIdRaw || '').trim();
|
|
365
463
|
if (!nextWorkspaceId)
|
|
@@ -404,9 +502,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
404
502
|
return workspaceTransitionEpochRef.current !== requestState.epoch;
|
|
405
503
|
}, []);
|
|
406
504
|
const isAbortError = useCallback((error) => {
|
|
407
|
-
return error
|
|
408
|
-
? error.name === 'AbortError'
|
|
409
|
-
: String(error?.name || '').toLowerCase() === 'aborterror';
|
|
505
|
+
return hasAbortErrorName(error) || isExpectedWorkspaceAbortReason(error);
|
|
410
506
|
}, []);
|
|
411
507
|
const abortWorkspaceScopedRequests = useCallback(() => {
|
|
412
508
|
[
|
|
@@ -420,6 +516,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
420
516
|
ref.current?.abort('workspace-transition');
|
|
421
517
|
ref.current = null;
|
|
422
518
|
});
|
|
519
|
+
if (planningEntitiesRetryTimerRef.current !== null && typeof window !== 'undefined') {
|
|
520
|
+
window.clearTimeout(planningEntitiesRetryTimerRef.current);
|
|
521
|
+
planningEntitiesRetryTimerRef.current = null;
|
|
522
|
+
}
|
|
423
523
|
referenceDataRequestRef.current = null;
|
|
424
524
|
}, []);
|
|
425
525
|
// Local runtime APIs must stay available immediately on app start, even when cloud
|
|
@@ -499,6 +599,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
499
599
|
const [archivedTasks, setArchivedTasks] = useState([]);
|
|
500
600
|
const [initiatives, setInitiatives] = useState([]);
|
|
501
601
|
const [workstreams, setWorkstreams] = useState([]);
|
|
602
|
+
const [planningBootstrapTaskSummaries, setPlanningBootstrapTaskSummaries] = useState([]);
|
|
502
603
|
const [deletedTasks, setDeletedTasks] = useState([]);
|
|
503
604
|
const [loadingTasks, setLoadingTasks] = useState(false);
|
|
504
605
|
const [editingTaskId, setEditingTaskId] = useState(null);
|
|
@@ -540,6 +641,14 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
540
641
|
const abortController = new AbortController();
|
|
541
642
|
const externalSignal = options?.signal;
|
|
542
643
|
const timeoutLabel = Number(timeoutMs) > 0 ? Number(timeoutMs) : BOOTSTRAP_REQUEST_TIMEOUT_MS;
|
|
644
|
+
const requestState = getWorkspaceRequestState();
|
|
645
|
+
const tracedInit = url.startsWith('/api/taskforce/')
|
|
646
|
+
? withBootstrapTraceHeader(init, buildBootstrapTraceId({
|
|
647
|
+
workspaceId: requestState.workspaceId,
|
|
648
|
+
epoch: requestState.epoch,
|
|
649
|
+
seed: bootstrapTraceSeedRef.current,
|
|
650
|
+
}))
|
|
651
|
+
: init;
|
|
543
652
|
const timerId = typeof window !== 'undefined'
|
|
544
653
|
? window.setTimeout(() => abortController.abort('bootstrap-timeout'), timeoutLabel)
|
|
545
654
|
: null;
|
|
@@ -556,18 +665,16 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
556
665
|
}
|
|
557
666
|
try {
|
|
558
667
|
return await fetch(url, {
|
|
559
|
-
...(
|
|
668
|
+
...(tracedInit || {}),
|
|
560
669
|
signal: abortController.signal
|
|
561
670
|
});
|
|
562
671
|
}
|
|
563
672
|
catch (error) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (
|
|
568
|
-
|
|
569
|
-
timeoutError.name = 'BootstrapTimeoutError';
|
|
570
|
-
throw timeoutError;
|
|
673
|
+
if (externalSignal?.aborted && isExpectedWorkspaceAbortFailure(error, externalSignal)) {
|
|
674
|
+
throw externalSignal.reason || error;
|
|
675
|
+
}
|
|
676
|
+
if (hasAbortErrorName(error) || error === 'bootstrap-timeout') {
|
|
677
|
+
throw createBootstrapTimeoutError(timeoutLabel);
|
|
571
678
|
}
|
|
572
679
|
throw error;
|
|
573
680
|
}
|
|
@@ -579,7 +686,50 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
579
686
|
window.clearTimeout(timerId);
|
|
580
687
|
}
|
|
581
688
|
}
|
|
582
|
-
}, []);
|
|
689
|
+
}, [getWorkspaceRequestState]);
|
|
690
|
+
const bootstrapState = useMemo(() => {
|
|
691
|
+
const authPending = !authSessionResolved;
|
|
692
|
+
const stalled = bootstrapPhase === 'stalled';
|
|
693
|
+
const appPending = authSessionResolved && (!configLoaded || workspaceBootstrapPending || stalled);
|
|
694
|
+
let subtitle = 'Checking account, workspace access, and setup status...';
|
|
695
|
+
if (bootstrapPhase === 'auth') {
|
|
696
|
+
subtitle = 'Verifying session...';
|
|
697
|
+
}
|
|
698
|
+
else if (bootstrapPhase === 'workspace') {
|
|
699
|
+
subtitle = 'Resolving workspace access...';
|
|
700
|
+
}
|
|
701
|
+
else if (bootstrapPhase === 'config') {
|
|
702
|
+
subtitle = 'Loading configuration...';
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
phase: bootstrapPhase,
|
|
706
|
+
auth: {
|
|
707
|
+
resolved: authSessionResolved,
|
|
708
|
+
pending: authPending
|
|
709
|
+
},
|
|
710
|
+
config: {
|
|
711
|
+
loaded: configLoaded
|
|
712
|
+
},
|
|
713
|
+
workspace: {
|
|
714
|
+
pending: workspaceBootstrapPending
|
|
715
|
+
},
|
|
716
|
+
pending: authPending || appPending,
|
|
717
|
+
authPending,
|
|
718
|
+
appPending,
|
|
719
|
+
ready: authSessionResolved && configLoaded && !workspaceBootstrapPending && bootstrapPhase === 'ready',
|
|
720
|
+
stalled,
|
|
721
|
+
subtitle,
|
|
722
|
+
error: bootstrapError,
|
|
723
|
+
startedAt: bootstrapStartedAt
|
|
724
|
+
};
|
|
725
|
+
}, [
|
|
726
|
+
authSessionResolved,
|
|
727
|
+
bootstrapError,
|
|
728
|
+
bootstrapPhase,
|
|
729
|
+
bootstrapStartedAt,
|
|
730
|
+
configLoaded,
|
|
731
|
+
workspaceBootstrapPending
|
|
732
|
+
]);
|
|
583
733
|
const isBootstrapTimeoutError = useCallback((error) => {
|
|
584
734
|
return String(error?.name || '') === 'BootstrapTimeoutError';
|
|
585
735
|
}, []);
|
|
@@ -648,7 +798,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
648
798
|
useEffect(() => {
|
|
649
799
|
if (workspaceTransitionEpoch === 0)
|
|
650
800
|
return;
|
|
651
|
-
abortWorkspaceScopedRequests();
|
|
652
801
|
referenceDataRequestRef.current = null;
|
|
653
802
|
lastLoadedUiStateKeyRef.current = '';
|
|
654
803
|
lastAssigneeOptionsLoadKeyRef.current = '';
|
|
@@ -663,8 +812,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
663
812
|
setDeletedTasks([]);
|
|
664
813
|
setTasks([]);
|
|
665
814
|
setArchivedTasks([]);
|
|
666
|
-
setInitiatives([]);
|
|
667
|
-
setWorkstreams([]);
|
|
668
815
|
setLoadingTasks(false);
|
|
669
816
|
if (cloudRealtimeFollowUpRefreshTimerRef.current !== null && typeof window !== 'undefined') {
|
|
670
817
|
window.clearTimeout(cloudRealtimeFollowUpRefreshTimerRef.current);
|
|
@@ -674,7 +821,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
674
821
|
window.clearTimeout(archiveBootstrapTimerRef.current);
|
|
675
822
|
archiveBootstrapTimerRef.current = null;
|
|
676
823
|
}
|
|
677
|
-
}, [
|
|
824
|
+
}, [setArchivedTasks, setLoadingTasks, setTasks, workspaceTransitionEpoch]);
|
|
678
825
|
const fetchDeletedTasks = useCallback(async (isSilent = false, options) => {
|
|
679
826
|
const ignoreAuthGuard = options?.ignoreAuthGuard === true;
|
|
680
827
|
if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
|
|
@@ -722,7 +869,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
722
869
|
}
|
|
723
870
|
}
|
|
724
871
|
catch (err) {
|
|
725
|
-
if (isAbortError(err))
|
|
872
|
+
if (isExpectedWorkspaceAbortFailure(err, abortController.signal) || isAbortError(err))
|
|
726
873
|
return;
|
|
727
874
|
console.error('[Taskforce] Failed to fetch deleted tasks:', err);
|
|
728
875
|
}
|
|
@@ -746,37 +893,105 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
746
893
|
if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
|
|
747
894
|
return;
|
|
748
895
|
const requestState = getWorkspaceRequestState();
|
|
896
|
+
planningEntitiesAbortRef.current?.abort('superseded');
|
|
897
|
+
if (planningEntitiesRetryTimerRef.current !== null && typeof window !== 'undefined') {
|
|
898
|
+
window.clearTimeout(planningEntitiesRetryTimerRef.current);
|
|
899
|
+
planningEntitiesRetryTimerRef.current = null;
|
|
900
|
+
}
|
|
749
901
|
const abortController = new AbortController();
|
|
750
902
|
planningEntitiesAbortRef.current = abortController;
|
|
751
903
|
try {
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
904
|
+
const planningTraceId = buildBootstrapTraceId({
|
|
905
|
+
workspaceId: requestState.workspaceId,
|
|
906
|
+
epoch: requestState.epoch,
|
|
907
|
+
seed: bootstrapTraceSeedRef.current,
|
|
908
|
+
});
|
|
909
|
+
const fetchPlanningRoute = async (url) => {
|
|
910
|
+
if (typeof window === 'undefined') {
|
|
911
|
+
return fetch(url, withBootstrapTraceHeader({ signal: abortController.signal }, planningTraceId));
|
|
912
|
+
}
|
|
913
|
+
const timeoutController = new AbortController();
|
|
914
|
+
const timerId = window.setTimeout(() => timeoutController.abort('planning-timeout'), PLANNING_BOOTSTRAP_REQUEST_TIMEOUT_MS);
|
|
915
|
+
const onAbort = () => timeoutController.abort(abortController.signal.reason || 'external-abort');
|
|
916
|
+
if (abortController.signal.aborted) {
|
|
917
|
+
onAbort();
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
abortController.signal.addEventListener('abort', onAbort, { once: true });
|
|
921
|
+
}
|
|
922
|
+
try {
|
|
923
|
+
return await fetch(url, withBootstrapTraceHeader({ signal: timeoutController.signal }, planningTraceId));
|
|
924
|
+
}
|
|
925
|
+
catch (error) {
|
|
926
|
+
if (abortController.signal.aborted)
|
|
927
|
+
throw abortController.signal.reason || error;
|
|
928
|
+
if (hasAbortErrorName(error) || error === 'planning-timeout') {
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
throw error;
|
|
932
|
+
}
|
|
933
|
+
finally {
|
|
934
|
+
window.clearTimeout(timerId);
|
|
935
|
+
abortController.signal.removeEventListener('abort', onAbort);
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
let bootstrapPayload = null;
|
|
939
|
+
const bootstrapRes = await fetchPlanningRoute('/api/taskforce/planning/bootstrap');
|
|
940
|
+
if (bootstrapRes?.status === 401) {
|
|
757
941
|
handleUnauthorized();
|
|
758
942
|
setInitiatives([]);
|
|
759
943
|
setWorkstreams([]);
|
|
944
|
+
setPlanningBootstrapTaskSummaries([]);
|
|
760
945
|
return;
|
|
761
946
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
947
|
+
if (bootstrapRes?.ok) {
|
|
948
|
+
bootstrapPayload = sanitizePlanningBootstrapPayload(await bootstrapRes.json().catch(() => ({})));
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
const [initiativesRes, workstreamsRes] = await Promise.all([
|
|
952
|
+
fetchPlanningRoute('/api/taskforce/initiatives'),
|
|
953
|
+
fetchPlanningRoute('/api/taskforce/workstreams'),
|
|
954
|
+
]);
|
|
955
|
+
if (!initiativesRes || !workstreamsRes) {
|
|
956
|
+
throw new Error('Planning endpoints timed out during startup.');
|
|
957
|
+
}
|
|
958
|
+
if (initiativesRes.status === 401 || workstreamsRes.status === 401) {
|
|
959
|
+
handleUnauthorized();
|
|
960
|
+
setInitiatives([]);
|
|
961
|
+
setWorkstreams([]);
|
|
962
|
+
setPlanningBootstrapTaskSummaries([]);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const [initiativesData, workstreamsData] = await Promise.all([
|
|
966
|
+
initiativesRes.ok ? initiativesRes.json().catch(() => []) : [],
|
|
967
|
+
workstreamsRes.ok ? workstreamsRes.json().catch(() => []) : [],
|
|
968
|
+
]);
|
|
969
|
+
bootstrapPayload = sanitizePlanningBootstrapPayload({
|
|
970
|
+
initiatives: initiativesData,
|
|
971
|
+
workstreams: workstreamsData,
|
|
972
|
+
workstreamTaskSummaries: [],
|
|
973
|
+
});
|
|
974
|
+
}
|
|
766
975
|
if (isWorkspaceRequestStale(requestState)) {
|
|
767
976
|
return;
|
|
768
977
|
}
|
|
769
|
-
setInitiatives(
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
setWorkstreams(Array.isArray(workstreamsData)
|
|
773
|
-
? workstreamsData.filter((item) => Boolean(item && typeof item === 'object' && typeof item.id === 'string'))
|
|
774
|
-
: []);
|
|
978
|
+
setInitiatives(bootstrapPayload.initiatives);
|
|
979
|
+
setWorkstreams(bootstrapPayload.workstreams);
|
|
980
|
+
setPlanningBootstrapTaskSummaries(bootstrapPayload.workstreamTaskSummaries);
|
|
775
981
|
}
|
|
776
982
|
catch (err) {
|
|
777
|
-
if (isAbortError(err))
|
|
983
|
+
if (isExpectedWorkspaceAbortFailure(err, abortController.signal) || isAbortError(err))
|
|
778
984
|
return;
|
|
779
985
|
console.error('[Taskforce] Failed to fetch planning entities:', err);
|
|
986
|
+
if (typeof window !== 'undefined'
|
|
987
|
+
&& !isWorkspaceRequestStale(requestState)
|
|
988
|
+
&& initiatives.length === 0
|
|
989
|
+
&& workstreams.length === 0) {
|
|
990
|
+
planningEntitiesRetryTimerRef.current = window.setTimeout(() => {
|
|
991
|
+
planningEntitiesRetryTimerRef.current = null;
|
|
992
|
+
void fetchPlanningEntities(options);
|
|
993
|
+
}, 3000);
|
|
994
|
+
}
|
|
780
995
|
}
|
|
781
996
|
finally {
|
|
782
997
|
if (planningEntitiesAbortRef.current === abortController) {
|
|
@@ -784,12 +999,16 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
784
999
|
}
|
|
785
1000
|
}
|
|
786
1001
|
}, [
|
|
1002
|
+
BOOTSTRAP_REQUEST_TIMEOUT_MS,
|
|
1003
|
+
PLANNING_BOOTSTRAP_REQUEST_TIMEOUT_MS,
|
|
1004
|
+
initiatives.length,
|
|
787
1005
|
getWorkspaceRequestState,
|
|
788
1006
|
handleUnauthorized,
|
|
789
1007
|
isAbortError,
|
|
790
1008
|
isWorkspaceRequestStale,
|
|
791
1009
|
shouldDeferProtectedApiCalls,
|
|
792
1010
|
shouldBlockProtectedApiCalls,
|
|
1011
|
+
workstreams.length,
|
|
793
1012
|
]);
|
|
794
1013
|
const fetchTasks = useCallback(async (isSilent = false, options) => {
|
|
795
1014
|
await fetchTasksBase(isSilent, options);
|
|
@@ -815,7 +1034,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
815
1034
|
const setUserGlobalSyncErrorRef = useRef(() => undefined);
|
|
816
1035
|
const checkAuthSessionImplRef = useRef(async () => false);
|
|
817
1036
|
const checkAuthSession = useCallback((options) => checkAuthSessionImplRef.current(options), []);
|
|
818
|
-
const { userGlobalSyncStatus, setUserGlobalSyncStatus, userGlobalSyncError, setUserGlobalSyncError, workspaceLastPullAt, workspaceLastPushAt, workspaceLastErrorAt, workspaceLastErrorMessage, workspaceLastSuccessfulSyncAt, workspaceCloudSyncEnabled, workspaceSyncPhase, workspaceSyncStatus, workspaceSyncSummary, workspaceSyncRecommendedAction, workspaceSyncBusy, workspaceSyncPendingChanges, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspacePullCursorRef, clearWorkspaceRetry, persistWorkspaceSyncPatch, loadWorkspaceSyncState, applyWorkspaceSyncStateSnapshot, syncUserGlobalSettings, buildWorkspaceSyncSignature, pushWorkspaceChangesToCloud, saveWorkspaceCloudSyncSettings, retryUserGlobalSettingsSync, retryWorkspaceCloudSync, resetWorkspaceSyncCursorAndPull, getWorkspaceSyncDiagnostics } = useSyncOrchestrator({
|
|
1037
|
+
const { userGlobalSyncStatus, setUserGlobalSyncStatus, userGlobalSyncError, setUserGlobalSyncError, workspaceLastPullAt, workspaceLastPushAt, workspaceLastErrorAt, workspaceLastErrorMessage, workspaceLastSuccessfulSyncAt, workspaceCloudSyncEnabled, workspaceSyncPhase, workspaceSyncSetupIntent, workspaceSyncStatus, workspaceSyncSummary, workspaceSyncRecommendedAction, workspaceSyncBusy, workspaceSyncPendingChanges, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspaceDeletedTaskWatermarksRef, workspacePullCursorRef, clearWorkspaceRetry, persistWorkspaceSyncPatch, loadWorkspaceSyncState, applyWorkspaceSyncStateSnapshot, syncUserGlobalSettings, buildWorkspaceSyncSignature, pushWorkspaceChangesToCloud, saveWorkspaceCloudSyncSettings, retryUserGlobalSettingsSync, retryWorkspaceCloudSync, resetWorkspaceSyncCursorAndPull, getWorkspaceSyncDiagnostics } = useSyncOrchestrator({
|
|
819
1038
|
currentWorkspaceId,
|
|
820
1039
|
cloudAuthConfigured,
|
|
821
1040
|
runtimeMode,
|
|
@@ -828,6 +1047,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
828
1047
|
realtimeSyncEnabled,
|
|
829
1048
|
tasks,
|
|
830
1049
|
archivedTasks,
|
|
1050
|
+
deletedTasks,
|
|
831
1051
|
initiatives,
|
|
832
1052
|
workstreams,
|
|
833
1053
|
taxonomies,
|
|
@@ -1317,7 +1537,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1317
1537
|
}
|
|
1318
1538
|
}
|
|
1319
1539
|
}, [fetchWithTimeout, isBootstrapTimeoutError, markBootstrapStalled]);
|
|
1320
|
-
const { fetchReferenceData, fetchBootstrapConfig, fetchConfig, fetchWorkspaces, fetchAssigneeOptions, switchWorkspace, resolveWorkspaceAfterAuth, retryBootstrapChecks } = useTaskforceWorkspaceBootstrap({
|
|
1540
|
+
const { fetchReferenceData, fetchBootstrapConfig, fetchConfig, fetchWorkspaces, fetchAssigneeOptions, switchWorkspace, resolveWorkspaceAfterAuth, retryBootstrapChecks, runCloudBootstrap } = useTaskforceWorkspaceBootstrap({
|
|
1321
1541
|
shouldDeferProtectedApiCalls,
|
|
1322
1542
|
shouldBlockProtectedApiCalls,
|
|
1323
1543
|
getWorkspaceRequestState,
|
|
@@ -1357,6 +1577,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1357
1577
|
authSessionResolved,
|
|
1358
1578
|
isAuthenticated,
|
|
1359
1579
|
authUserId,
|
|
1580
|
+
authUserAvatarUrl,
|
|
1360
1581
|
setHydratedWorkspaceRole,
|
|
1361
1582
|
setAssigneeOptions,
|
|
1362
1583
|
setAssigneeOptionsLoaded,
|
|
@@ -1382,6 +1603,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1382
1603
|
showArchive,
|
|
1383
1604
|
taskScope,
|
|
1384
1605
|
settingsSection,
|
|
1606
|
+
workspaceBootstrapPending,
|
|
1385
1607
|
fetchTasks,
|
|
1386
1608
|
fetchArchive,
|
|
1387
1609
|
fetchPlanningEntities,
|
|
@@ -1397,12 +1619,35 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1397
1619
|
useEffect(() => {
|
|
1398
1620
|
if (!configLoaded || workspaceBootstrapPending)
|
|
1399
1621
|
return;
|
|
1400
|
-
const loadKey = `${runtimeMode}:${isAuthenticated ? 'auth' : 'guest'}:${currentWorkspaceId}`;
|
|
1622
|
+
const loadKey = `${runtimeMode}:${isAuthenticated ? 'auth' : 'guest'}:${currentWorkspaceId}:${authUserAvatarUrl || 'no-avatar'}`;
|
|
1401
1623
|
if (assigneeOptionsLoaded && lastAssigneeOptionsLoadKeyRef.current === loadKey)
|
|
1402
1624
|
return;
|
|
1403
1625
|
lastAssigneeOptionsLoadKeyRef.current = loadKey;
|
|
1404
1626
|
void fetchAssigneeOptions();
|
|
1405
|
-
}, [assigneeOptionsLoaded, configLoaded, currentWorkspaceId, fetchAssigneeOptions, isAuthenticated, runtimeMode, workspaceBootstrapPending]);
|
|
1627
|
+
}, [assigneeOptionsLoaded, authUserAvatarUrl, configLoaded, currentWorkspaceId, fetchAssigneeOptions, isAuthenticated, runtimeMode, workspaceBootstrapPending]);
|
|
1628
|
+
useEffect(() => {
|
|
1629
|
+
if (!authSessionResolved || !isAuthenticated)
|
|
1630
|
+
return;
|
|
1631
|
+
const normalizedAuthUserId = String(authUserId || '').trim();
|
|
1632
|
+
if (!normalizedAuthUserId || normalizedAuthUserId === 'anonymous')
|
|
1633
|
+
return;
|
|
1634
|
+
const nextAvatarUrl = String(authUserAvatarUrl || '').trim() || null;
|
|
1635
|
+
setAssigneeOptions((previous) => {
|
|
1636
|
+
let changed = false;
|
|
1637
|
+
const next = previous.map((option) => {
|
|
1638
|
+
if (option.kind !== 'member' || String(option.value || '').trim() !== normalizedAuthUserId)
|
|
1639
|
+
return option;
|
|
1640
|
+
const currentAvatarUrl = typeof option.avatarUrl === 'string' && option.avatarUrl.trim().length > 0
|
|
1641
|
+
? option.avatarUrl.trim()
|
|
1642
|
+
: null;
|
|
1643
|
+
if (currentAvatarUrl === nextAvatarUrl)
|
|
1644
|
+
return option;
|
|
1645
|
+
changed = true;
|
|
1646
|
+
return { ...option, avatarUrl: nextAvatarUrl };
|
|
1647
|
+
});
|
|
1648
|
+
return changed ? next : previous;
|
|
1649
|
+
});
|
|
1650
|
+
}, [authSessionResolved, authUserAvatarUrl, authUserId, isAuthenticated]);
|
|
1406
1651
|
const currentWorkspaceRole = useMemo(() => {
|
|
1407
1652
|
if (workspaceSwitchingEnabled) {
|
|
1408
1653
|
const current = availableWorkspaces.find((workspace) => workspace.id === currentWorkspaceId);
|
|
@@ -1417,44 +1662,86 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1417
1662
|
}, [isAuthenticated]);
|
|
1418
1663
|
const continueAfterCommercialOnboarding = useCallback(async () => {
|
|
1419
1664
|
setBootstrapError(null);
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1665
|
+
const result = await runCloudBootstrap({
|
|
1666
|
+
reason: 'post-plans',
|
|
1667
|
+
ignoreAuthGuard: true,
|
|
1668
|
+
tasksSilent: true,
|
|
1669
|
+
includeDeferredArchive: false
|
|
1670
|
+
});
|
|
1671
|
+
if (!result.authenticated) {
|
|
1672
|
+
return { success: false, destination: 'login', error: result.error || 'Sign in required.', code: result.code || 'AUTH_REQUIRED' };
|
|
1425
1673
|
}
|
|
1426
|
-
if (
|
|
1427
|
-
|
|
1674
|
+
if (!result.success) {
|
|
1675
|
+
return {
|
|
1676
|
+
success: false,
|
|
1677
|
+
destination: 'app',
|
|
1678
|
+
error: result.error || 'Unable to resolve workspace after plan selection.',
|
|
1679
|
+
code: result.code
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
if (result.workspaceSetupRequired) {
|
|
1683
|
+
return { success: true, destination: 'setup' };
|
|
1684
|
+
}
|
|
1685
|
+
return { success: true, destination: 'app' };
|
|
1686
|
+
}, [
|
|
1687
|
+
runCloudBootstrap
|
|
1688
|
+
]);
|
|
1689
|
+
const resolvePostAuthCloudEntry = useCallback(async (options) => {
|
|
1690
|
+
const reason = options?.reason || 'retry';
|
|
1691
|
+
const preferredWorkspaceId = typeof options?.preferredWorkspaceId === 'string' ? options.preferredWorkspaceId.trim() : '';
|
|
1692
|
+
const allowCommercialGate = options?.allowCommercialGate === true;
|
|
1693
|
+
if (preferredWorkspaceId && workspaceSwitchingEnabled) {
|
|
1694
|
+
const authenticated = await checkAuthSession({ force: true });
|
|
1695
|
+
if (!authenticated) {
|
|
1696
|
+
await fetchConfig({ ignoreAuthGuard: true });
|
|
1697
|
+
return { success: false, error: 'Sign in required.', code: 'AUTH_REQUIRED' };
|
|
1698
|
+
}
|
|
1699
|
+
const workspaceResolution = await resolveWorkspaceAfterAuth({ preferredWorkspaceId });
|
|
1428
1700
|
if (!workspaceResolution.success) {
|
|
1429
1701
|
await fetchConfig({ ignoreAuthGuard: true });
|
|
1430
1702
|
return {
|
|
1431
1703
|
success: false,
|
|
1432
|
-
|
|
1433
|
-
error: workspaceResolution.error || 'Unable to resolve workspace after plan selection.',
|
|
1704
|
+
error: workspaceResolution.error || 'Unable to resolve workspace access.',
|
|
1434
1705
|
code: workspaceResolution.code
|
|
1435
1706
|
};
|
|
1436
1707
|
}
|
|
1437
1708
|
if (workspaceResolution.workspaceSetupRequired) {
|
|
1438
1709
|
await fetchConfig({ ignoreAuthGuard: true });
|
|
1439
|
-
return { success: true,
|
|
1710
|
+
return { success: true, workspaceSetupRequired: true };
|
|
1440
1711
|
}
|
|
1441
1712
|
}
|
|
1442
|
-
await
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1713
|
+
const result = await runCloudBootstrap({
|
|
1714
|
+
reason,
|
|
1715
|
+
ignoreAuthGuard: true,
|
|
1716
|
+
tasksSilent: true,
|
|
1717
|
+
includeDeferredArchive: false
|
|
1718
|
+
});
|
|
1719
|
+
if (!result.authenticated) {
|
|
1720
|
+
return { success: false, error: result.error || 'Sign in required.', code: result.code || 'AUTH_REQUIRED' };
|
|
1721
|
+
}
|
|
1722
|
+
if (!result.success) {
|
|
1723
|
+
return {
|
|
1724
|
+
success: false,
|
|
1725
|
+
error: result.error || 'Unable to resolve workspace access.',
|
|
1726
|
+
code: result.code
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
if (result.workspaceSetupRequired) {
|
|
1730
|
+
if (allowCommercialGate) {
|
|
1731
|
+
return { success: true, workspaceSetupRequired: false };
|
|
1732
|
+
}
|
|
1733
|
+
return { success: true, workspaceSetupRequired: true };
|
|
1734
|
+
}
|
|
1735
|
+
if (!allowCommercialGate) {
|
|
1736
|
+
void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
|
|
1737
|
+
}
|
|
1738
|
+
return { success: true, workspaceSetupRequired: false };
|
|
1449
1739
|
}, [
|
|
1450
1740
|
checkAuthSession,
|
|
1451
|
-
fetchBootstrapConfig,
|
|
1452
1741
|
fetchConfig,
|
|
1453
|
-
fetchPlanningEntities,
|
|
1454
|
-
fetchReferenceData,
|
|
1455
|
-
fetchTasks,
|
|
1456
|
-
markBootstrapPhase,
|
|
1457
1742
|
resolveWorkspaceAfterAuth,
|
|
1743
|
+
runCloudBootstrap,
|
|
1744
|
+
syncUserGlobalSettings,
|
|
1458
1745
|
workspaceSwitchingEnabled
|
|
1459
1746
|
]);
|
|
1460
1747
|
const createWorkspace = useCallback(async (name, description) => {
|
|
@@ -1615,30 +1902,34 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1615
1902
|
return { success: false, error: data?.error || 'Sign in failed.', code: data?.code };
|
|
1616
1903
|
}
|
|
1617
1904
|
setError('');
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
return {
|
|
1622
|
-
success: false,
|
|
1623
|
-
error: workspaceResolution.error || 'Unable to resolve workspace after sign in.',
|
|
1624
|
-
code: workspaceResolution.code
|
|
1625
|
-
};
|
|
1626
|
-
}
|
|
1627
|
-
if (workspaceResolution.workspaceSetupRequired) {
|
|
1628
|
-
await fetchConfig({ ignoreAuthGuard: true });
|
|
1629
|
-
return { success: true, workspaceSetupRequired: true };
|
|
1905
|
+
const entry = await resolvePostAuthCloudEntry();
|
|
1906
|
+
if (!entry.success) {
|
|
1907
|
+
return { success: false, error: entry.error || 'Unable to resolve workspace after sign in.', code: entry.code };
|
|
1630
1908
|
}
|
|
1631
|
-
|
|
1632
|
-
fetchConfig({ ignoreAuthGuard: true }),
|
|
1633
|
-
fetchTasks(true, { ignoreAuthGuard: true })
|
|
1634
|
-
]);
|
|
1635
|
-
void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
|
|
1636
|
-
return { success: true };
|
|
1909
|
+
return { success: true, workspaceSetupRequired: entry.workspaceSetupRequired === true };
|
|
1637
1910
|
}
|
|
1638
1911
|
catch {
|
|
1639
1912
|
return { success: false, error: 'Sign in failed.' };
|
|
1640
1913
|
}
|
|
1641
|
-
}, [
|
|
1914
|
+
}, [resolveCloudAuthUrl, cloudAuthConfigured, resolvePostAuthCloudEntry]);
|
|
1915
|
+
/**
|
|
1916
|
+
* Initiates an OAuth login (or invite-claim) flow by redirecting the browser
|
|
1917
|
+
* to the provider's authorisation URL via the server-side start route.
|
|
1918
|
+
*
|
|
1919
|
+
* @param provider - 'google' | 'github' | 'apple'
|
|
1920
|
+
* @param returnTo - Optional relative path to redirect back to after auth (defaults to '/').
|
|
1921
|
+
* @param inviteToken - Optional invite token; when provided the flow uses `invite_claim` mode.
|
|
1922
|
+
*/
|
|
1923
|
+
const beginOAuthLogin = useCallback((provider, returnTo, inviteToken) => {
|
|
1924
|
+
const sanitizedReturnTo = returnTo && /^\/[A-Za-z0-9_\-./?=&%]*$/.test(returnTo)
|
|
1925
|
+
? returnTo
|
|
1926
|
+
: '/';
|
|
1927
|
+
let path = `/api/taskforce/auth/oauth/${encodeURIComponent(provider)}/start?return_to=${encodeURIComponent(sanitizedReturnTo)}`;
|
|
1928
|
+
if (inviteToken) {
|
|
1929
|
+
path += `&invite_token=${encodeURIComponent(inviteToken)}`;
|
|
1930
|
+
}
|
|
1931
|
+
window.location.href = resolveCloudAuthUrl(path);
|
|
1932
|
+
}, [resolveCloudAuthUrl]);
|
|
1642
1933
|
const registerWithCredentials = useCallback(async (email, password, options) => {
|
|
1643
1934
|
if (!cloudAuthConfigured) {
|
|
1644
1935
|
return { success: false, error: 'Cloud auth endpoint is not configured.' };
|
|
@@ -1688,32 +1979,42 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1688
1979
|
const verificationRequired = Boolean(data?.verificationRequired);
|
|
1689
1980
|
if (!verificationRequired) {
|
|
1690
1981
|
setError('');
|
|
1691
|
-
await checkAuthSession({ force: true });
|
|
1692
|
-
const workspaceResolution = await resolveWorkspaceAfterAuth();
|
|
1693
|
-
if (!workspaceResolution.success) {
|
|
1694
|
-
return {
|
|
1695
|
-
success: false,
|
|
1696
|
-
error: workspaceResolution.error || 'Unable to resolve workspace after registration.',
|
|
1697
|
-
code: workspaceResolution.code
|
|
1698
|
-
};
|
|
1699
|
-
}
|
|
1700
1982
|
const hasCommercialOnboardingGate = Boolean(planSelectionRequired
|
|
1701
1983
|
|| checkoutPending
|
|
1702
1984
|
|| commercialState === 'pending_plan_selection'
|
|
1703
1985
|
|| commercialState === 'checkout_pending');
|
|
1704
|
-
if (
|
|
1705
|
-
await
|
|
1986
|
+
if (hasCommercialOnboardingGate) {
|
|
1987
|
+
const authenticated = await checkAuthSession({ force: true });
|
|
1988
|
+
if (!authenticated) {
|
|
1989
|
+
return {
|
|
1990
|
+
success: false,
|
|
1991
|
+
error: 'Unable to resolve workspace after registration.',
|
|
1992
|
+
code: 'AUTH_REQUIRED'
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
return {
|
|
1996
|
+
success: true,
|
|
1997
|
+
workspaceSetupRequired: false,
|
|
1998
|
+
planSelectionRequired,
|
|
1999
|
+
checkoutPending,
|
|
2000
|
+
commercialState,
|
|
2001
|
+
verificationRequired,
|
|
2002
|
+
verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
|
|
2003
|
+
emailSent: data?.emailSent !== false,
|
|
2004
|
+
emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
|
|
2005
|
+
};
|
|
1706
2006
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
2007
|
+
const entry = await resolvePostAuthCloudEntry({ allowCommercialGate: false });
|
|
2008
|
+
if (!entry.success) {
|
|
2009
|
+
return {
|
|
2010
|
+
success: false,
|
|
2011
|
+
error: entry.error || 'Unable to resolve workspace after registration.',
|
|
2012
|
+
code: entry.code
|
|
2013
|
+
};
|
|
1713
2014
|
}
|
|
1714
2015
|
return {
|
|
1715
2016
|
success: true,
|
|
1716
|
-
workspaceSetupRequired:
|
|
2017
|
+
workspaceSetupRequired: entry.workspaceSetupRequired === true,
|
|
1717
2018
|
planSelectionRequired,
|
|
1718
2019
|
checkoutPending,
|
|
1719
2020
|
commercialState,
|
|
@@ -1738,7 +2039,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1738
2039
|
catch {
|
|
1739
2040
|
return { success: false, error: 'Create account failed.' };
|
|
1740
2041
|
}
|
|
1741
|
-
}, [checkAuthSession,
|
|
2042
|
+
}, [checkAuthSession, resolveCloudAuthUrl, cloudAuthConfigured, resolvePostAuthCloudEntry]);
|
|
1742
2043
|
const updateCurrentUserProfile = useCallback(async (input) => {
|
|
1743
2044
|
if (!cloudAuthConfigured) {
|
|
1744
2045
|
return { success: false, error: 'Cloud auth endpoint is not configured.' };
|
|
@@ -1800,6 +2101,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1800
2101
|
success: true,
|
|
1801
2102
|
verificationToken: data?.verificationToken ?? null,
|
|
1802
2103
|
emailSent: data?.emailSent !== false,
|
|
2104
|
+
deliveryAttempted: data?.deliveryAttempted === true
|
|
2105
|
+
|| (typeof data?.verificationToken === 'string' && data.verificationToken.trim().length > 0),
|
|
1803
2106
|
emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
|
|
1804
2107
|
};
|
|
1805
2108
|
}
|
|
@@ -1853,6 +2156,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1853
2156
|
success: true,
|
|
1854
2157
|
resetToken: data?.resetToken ?? null,
|
|
1855
2158
|
emailSent: data?.emailSent !== false,
|
|
2159
|
+
deliveryAttempted: data?.deliveryAttempted === true
|
|
2160
|
+
|| (typeof data?.resetToken === 'string' && data.resetToken.trim().length > 0),
|
|
1856
2161
|
emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
|
|
1857
2162
|
};
|
|
1858
2163
|
}
|
|
@@ -1915,7 +2220,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1915
2220
|
email: typeof data?.email === 'string' ? data.email : undefined,
|
|
1916
2221
|
workspaceId: typeof data?.workspaceId === 'string' ? data.workspaceId : undefined,
|
|
1917
2222
|
inviteeKind: data?.inviteeKind === 'existing_user' ? 'existing_user' : 'new_user',
|
|
1918
|
-
passwordRequired: data?.passwordRequired === true
|
|
2223
|
+
passwordRequired: data?.passwordRequired === true,
|
|
2224
|
+
inviteeState: data?.inviteeState === 'existing_account' ? 'existing_account' : 'pending_setup',
|
|
2225
|
+
availableMethods: Array.isArray(data?.availableMethods) ? data.availableMethods : []
|
|
1919
2226
|
};
|
|
1920
2227
|
}
|
|
1921
2228
|
catch {
|
|
@@ -1943,30 +2250,30 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1943
2250
|
return { success: false, error: data?.error || 'Invite acceptance failed.', code: data?.code };
|
|
1944
2251
|
}
|
|
1945
2252
|
setError('');
|
|
1946
|
-
await checkAuthSession({ force: true });
|
|
1947
2253
|
const preferredWorkspaceId = typeof data?.workspaceId === 'string' ? data.workspaceId : null;
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2254
|
+
if (preferredWorkspaceId && workspaceSwitchingEnabled) {
|
|
2255
|
+
const switched = await switchWorkspace(preferredWorkspaceId, { hydrate: false });
|
|
2256
|
+
if (!switched.success) {
|
|
2257
|
+
return {
|
|
2258
|
+
success: false,
|
|
2259
|
+
error: switched.error || 'Unable to resolve workspace after invite acceptance.',
|
|
2260
|
+
code: switched.code
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
1955
2263
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
2264
|
+
const entry = await resolvePostAuthCloudEntry();
|
|
2265
|
+
if (!entry.success) {
|
|
2266
|
+
return { success: false, error: entry.error || 'Unable to resolve workspace after invite acceptance.', code: entry.code };
|
|
1958
2267
|
}
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
|
|
1964
|
-
return { success: true };
|
|
2268
|
+
if (preferredWorkspaceId && currentWorkspaceIdRef.current !== preferredWorkspaceId) {
|
|
2269
|
+
commitCurrentWorkspaceId(preferredWorkspaceId, { clearExplicitSelection: false });
|
|
2270
|
+
}
|
|
2271
|
+
return { success: true, workspaceSetupRequired: entry.workspaceSetupRequired === true };
|
|
1965
2272
|
}
|
|
1966
2273
|
catch {
|
|
1967
2274
|
return { success: false, error: 'Invite acceptance failed.' };
|
|
1968
2275
|
}
|
|
1969
|
-
}, [
|
|
2276
|
+
}, [commitCurrentWorkspaceId, resolveCloudAuthUrl, cloudAuthConfigured, currentWorkspaceIdRef, resolvePostAuthCloudEntry, switchWorkspace, workspaceSwitchingEnabled]);
|
|
1970
2277
|
const joinInviteWithToken = useCallback(async (token) => {
|
|
1971
2278
|
if (!cloudAuthConfigured) {
|
|
1972
2279
|
return { success: false, error: 'Cloud auth endpoint is not configured.' };
|
|
@@ -1987,30 +2294,93 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1987
2294
|
return { success: false, error: data?.error || 'Invite join failed.', code: data?.code };
|
|
1988
2295
|
}
|
|
1989
2296
|
setError('');
|
|
1990
|
-
await checkAuthSession();
|
|
1991
2297
|
const preferredWorkspaceId = typeof data?.workspaceId === 'string' ? data.workspaceId : null;
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2298
|
+
if (preferredWorkspaceId && workspaceSwitchingEnabled) {
|
|
2299
|
+
const switched = await switchWorkspace(preferredWorkspaceId, { hydrate: false });
|
|
2300
|
+
if (!switched.success) {
|
|
2301
|
+
return {
|
|
2302
|
+
success: false,
|
|
2303
|
+
error: switched.error || 'Unable to resolve workspace after invite join.',
|
|
2304
|
+
code: switched.code
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
1999
2307
|
}
|
|
2000
|
-
|
|
2001
|
-
|
|
2308
|
+
const entry = await resolvePostAuthCloudEntry();
|
|
2309
|
+
if (!entry.success) {
|
|
2310
|
+
return { success: false, error: entry.error || 'Unable to resolve workspace after invite join.', code: entry.code };
|
|
2002
2311
|
}
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
|
|
2008
|
-
return { success: true };
|
|
2312
|
+
if (preferredWorkspaceId && currentWorkspaceIdRef.current !== preferredWorkspaceId) {
|
|
2313
|
+
commitCurrentWorkspaceId(preferredWorkspaceId, { clearExplicitSelection: false });
|
|
2314
|
+
}
|
|
2315
|
+
return { success: true, workspaceSetupRequired: entry.workspaceSetupRequired === true };
|
|
2009
2316
|
}
|
|
2010
2317
|
catch {
|
|
2011
2318
|
return { success: false, error: 'Invite join failed.' };
|
|
2012
2319
|
}
|
|
2013
|
-
}, [
|
|
2320
|
+
}, [commitCurrentWorkspaceId, resolveCloudAuthUrl, cloudAuthConfigured, currentWorkspaceIdRef, resolvePostAuthCloudEntry, switchWorkspace, workspaceSwitchingEnabled]);
|
|
2321
|
+
// ─── Login method management ────────────────────────────────────────────────
|
|
2322
|
+
const fetchLoginMethods = useCallback(async () => {
|
|
2323
|
+
if (!cloudAuthConfigured)
|
|
2324
|
+
return { success: false, error: 'Cloud auth not configured.' };
|
|
2325
|
+
try {
|
|
2326
|
+
const res = await fetch(resolveCloudAuthUrl('/api/taskforce/account/login-methods'), {
|
|
2327
|
+
credentials: 'include'
|
|
2328
|
+
});
|
|
2329
|
+
const data = await res.json().catch(() => ({}));
|
|
2330
|
+
if (!res.ok || !data?.success) {
|
|
2331
|
+
return { success: false, error: data?.error || 'Failed to fetch login methods.' };
|
|
2332
|
+
}
|
|
2333
|
+
return { success: true, methods: Array.isArray(data?.methods) ? data.methods : [] };
|
|
2334
|
+
}
|
|
2335
|
+
catch {
|
|
2336
|
+
return { success: false, error: 'Failed to fetch login methods.' };
|
|
2337
|
+
}
|
|
2338
|
+
}, [resolveCloudAuthUrl, cloudAuthConfigured]);
|
|
2339
|
+
const unlinkLoginMethod = useCallback(async (provider) => {
|
|
2340
|
+
if (!cloudAuthConfigured)
|
|
2341
|
+
return { success: false, error: 'Cloud auth not configured.' };
|
|
2342
|
+
try {
|
|
2343
|
+
const res = await fetch(resolveCloudAuthUrl(`/api/taskforce/account/login-methods/${encodeURIComponent(provider)}`), {
|
|
2344
|
+
method: 'DELETE',
|
|
2345
|
+
credentials: 'include'
|
|
2346
|
+
});
|
|
2347
|
+
const data = await res.json().catch(() => ({}));
|
|
2348
|
+
if (!res.ok || !data?.success) {
|
|
2349
|
+
return { success: false, error: data?.error || 'Unlink failed.', code: data?.code };
|
|
2350
|
+
}
|
|
2351
|
+
return { success: true };
|
|
2352
|
+
}
|
|
2353
|
+
catch {
|
|
2354
|
+
return { success: false, error: 'Unlink failed.' };
|
|
2355
|
+
}
|
|
2356
|
+
}, [resolveCloudAuthUrl, cloudAuthConfigured]);
|
|
2357
|
+
const addPasswordToAccount = useCallback(async (password) => {
|
|
2358
|
+
if (!cloudAuthConfigured)
|
|
2359
|
+
return { success: false, error: 'Cloud auth not configured.' };
|
|
2360
|
+
try {
|
|
2361
|
+
const res = await fetch(resolveCloudAuthUrl('/api/taskforce/account/login-methods/password'), {
|
|
2362
|
+
method: 'POST',
|
|
2363
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2364
|
+
credentials: 'include',
|
|
2365
|
+
body: JSON.stringify({ password })
|
|
2366
|
+
});
|
|
2367
|
+
const data = await res.json().catch(() => ({}));
|
|
2368
|
+
if (!res.ok || !data?.success) {
|
|
2369
|
+
return { success: false, error: data?.error || 'Failed to add password.', code: data?.code };
|
|
2370
|
+
}
|
|
2371
|
+
return { success: true };
|
|
2372
|
+
}
|
|
2373
|
+
catch {
|
|
2374
|
+
return { success: false, error: 'Failed to add password.' };
|
|
2375
|
+
}
|
|
2376
|
+
}, [resolveCloudAuthUrl, cloudAuthConfigured]);
|
|
2377
|
+
const beginOAuthLink = useCallback((provider, returnTo) => {
|
|
2378
|
+
const sanitizedReturnTo = returnTo && /^\/[A-Za-z0-9_\-./?=&%]*$/.test(returnTo)
|
|
2379
|
+
? returnTo
|
|
2380
|
+
: '/';
|
|
2381
|
+
const path = `/api/taskforce/auth/oauth/${encodeURIComponent(provider)}/start?flow=link&return_to=${encodeURIComponent(sanitizedReturnTo)}`;
|
|
2382
|
+
window.location.href = resolveCloudAuthUrl(path);
|
|
2383
|
+
}, [resolveCloudAuthUrl]);
|
|
2014
2384
|
const resetCloudLogoutClientState = useCallback(() => {
|
|
2015
2385
|
abortWorkspaceScopedRequests();
|
|
2016
2386
|
referenceDataRequestRef.current = null;
|
|
@@ -2029,6 +2399,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2029
2399
|
setArchivedTasks([]);
|
|
2030
2400
|
setInitiatives([]);
|
|
2031
2401
|
setWorkstreams([]);
|
|
2402
|
+
setPlanningBootstrapTaskSummaries([]);
|
|
2032
2403
|
setLoadingTasks(false);
|
|
2033
2404
|
if (cloudRealtimeFollowUpRefreshTimerRef.current !== null && typeof window !== 'undefined') {
|
|
2034
2405
|
window.clearTimeout(cloudRealtimeFollowUpRefreshTimerRef.current);
|
|
@@ -2139,6 +2510,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2139
2510
|
throw new Error(data?.error || `Failed to create initiative (${res.status})`);
|
|
2140
2511
|
}
|
|
2141
2512
|
await fetchPlanningEntities();
|
|
2513
|
+
setInitiatives((current) => upsertEntityById(current, data));
|
|
2142
2514
|
return data;
|
|
2143
2515
|
}, [fetchPlanningEntities]);
|
|
2144
2516
|
const updateInitiative = useCallback(async (id, payload) => {
|
|
@@ -2152,6 +2524,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2152
2524
|
throw new Error(data?.error || `Failed to update initiative (${res.status})`);
|
|
2153
2525
|
}
|
|
2154
2526
|
await fetchPlanningEntities();
|
|
2527
|
+
setInitiatives((current) => upsertEntityById(current, data));
|
|
2155
2528
|
return data;
|
|
2156
2529
|
}, [fetchPlanningEntities]);
|
|
2157
2530
|
const archiveInitiative = useCallback(async (id) => {
|
|
@@ -2163,6 +2536,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2163
2536
|
throw new Error(data?.error || `Failed to archive initiative (${res.status})`);
|
|
2164
2537
|
}
|
|
2165
2538
|
await fetchPlanningEntities();
|
|
2539
|
+
setInitiatives((current) => upsertEntityById(current, data));
|
|
2166
2540
|
return data;
|
|
2167
2541
|
}, [fetchPlanningEntities]);
|
|
2168
2542
|
const unarchiveInitiative = useCallback(async (id) => {
|
|
@@ -2174,6 +2548,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2174
2548
|
throw new Error(data?.error || `Failed to unarchive initiative (${res.status})`);
|
|
2175
2549
|
}
|
|
2176
2550
|
await fetchPlanningEntities();
|
|
2551
|
+
setInitiatives((current) => upsertEntityById(current, data));
|
|
2177
2552
|
return data;
|
|
2178
2553
|
}, [fetchPlanningEntities]);
|
|
2179
2554
|
const createWorkstream = useCallback(async (payload) => {
|
|
@@ -2187,6 +2562,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2187
2562
|
throw new Error(data?.error || `Failed to create workstream (${res.status})`);
|
|
2188
2563
|
}
|
|
2189
2564
|
await fetchPlanningEntities();
|
|
2565
|
+
setWorkstreams((current) => upsertEntityById(current, data));
|
|
2190
2566
|
return data;
|
|
2191
2567
|
}, [fetchPlanningEntities]);
|
|
2192
2568
|
const updateWorkstream = useCallback(async (id, payload) => {
|
|
@@ -2200,6 +2576,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2200
2576
|
throw new Error(data?.error || `Failed to update workstream (${res.status})`);
|
|
2201
2577
|
}
|
|
2202
2578
|
await fetchPlanningEntities();
|
|
2579
|
+
setWorkstreams((current) => upsertEntityById(current, data));
|
|
2203
2580
|
return data;
|
|
2204
2581
|
}, [fetchPlanningEntities]);
|
|
2205
2582
|
const archiveWorkstream = useCallback(async (id) => {
|
|
@@ -2211,6 +2588,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2211
2588
|
throw new Error(data?.error || `Failed to archive workstream (${res.status})`);
|
|
2212
2589
|
}
|
|
2213
2590
|
await fetchPlanningEntities();
|
|
2591
|
+
setWorkstreams((current) => upsertEntityById(current, data));
|
|
2214
2592
|
return data;
|
|
2215
2593
|
}, [fetchPlanningEntities]);
|
|
2216
2594
|
const unarchiveWorkstream = useCallback(async (id) => {
|
|
@@ -2222,6 +2600,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2222
2600
|
throw new Error(data?.error || `Failed to unarchive workstream (${res.status})`);
|
|
2223
2601
|
}
|
|
2224
2602
|
await fetchPlanningEntities();
|
|
2603
|
+
setWorkstreams((current) => upsertEntityById(current, data));
|
|
2225
2604
|
return data;
|
|
2226
2605
|
}, [fetchPlanningEntities]);
|
|
2227
2606
|
// Initialize auth once, then bootstrap protected data once auth gates are settled.
|
|
@@ -2238,6 +2617,38 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2238
2617
|
return;
|
|
2239
2618
|
void checkAuthSession();
|
|
2240
2619
|
}, [runtimeConfigReady, checkAuthSession]);
|
|
2620
|
+
// Fetch available OAuth providers once we know the auth endpoint.
|
|
2621
|
+
// Uses a short timeout so an unreachable auth server doesn't hold open
|
|
2622
|
+
// browser connections and slow down subsequent requests on the same origin.
|
|
2623
|
+
useEffect(() => {
|
|
2624
|
+
if (!cloudAuthConfigured)
|
|
2625
|
+
return;
|
|
2626
|
+
let cancelled = false;
|
|
2627
|
+
const controller = new AbortController();
|
|
2628
|
+
const timerId = typeof window !== 'undefined'
|
|
2629
|
+
? window.setTimeout(() => controller.abort(), 5000)
|
|
2630
|
+
: null;
|
|
2631
|
+
fetch(resolveCloudAuthUrl('/api/taskforce/auth/providers'), {
|
|
2632
|
+
credentials: 'include',
|
|
2633
|
+
signal: controller.signal
|
|
2634
|
+
})
|
|
2635
|
+
.then(r => r.ok ? r.json() : null)
|
|
2636
|
+
.then(data => {
|
|
2637
|
+
if (!cancelled && Array.isArray(data?.providers)) {
|
|
2638
|
+
setAvailableAuthProviders(data.providers);
|
|
2639
|
+
}
|
|
2640
|
+
})
|
|
2641
|
+
.catch(() => { })
|
|
2642
|
+
.finally(() => {
|
|
2643
|
+
if (timerId !== null && typeof window !== 'undefined') {
|
|
2644
|
+
window.clearTimeout(timerId);
|
|
2645
|
+
}
|
|
2646
|
+
});
|
|
2647
|
+
return () => {
|
|
2648
|
+
cancelled = true;
|
|
2649
|
+
controller.abort();
|
|
2650
|
+
};
|
|
2651
|
+
}, [cloudAuthConfigured, resolveCloudAuthUrl]);
|
|
2241
2652
|
// Keep local runtime auth state aligned with cloud session changes
|
|
2242
2653
|
// (e.g. user signs in/out in another tab or cloud-hosted app).
|
|
2243
2654
|
useEffect(() => {
|
|
@@ -2261,109 +2672,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2261
2672
|
window.clearInterval(intervalId);
|
|
2262
2673
|
};
|
|
2263
2674
|
}, [runtimeMode, shouldProbeCloudAuth, checkAuthSession]);
|
|
2264
|
-
useEffect(() => {
|
|
2265
|
-
if (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls)
|
|
2266
|
-
return;
|
|
2267
|
-
if (hasBootstrappedDataRef.current)
|
|
2268
|
-
return;
|
|
2269
|
-
hasBootstrappedDataRef.current = true;
|
|
2270
|
-
void (async () => {
|
|
2271
|
-
if (runtimeMode === 'cloud' && authSessionResolved && isAuthenticated) {
|
|
2272
|
-
const workspaceResolution = await resolveWorkspaceAfterAuth();
|
|
2273
|
-
if (!workspaceResolution.success || workspaceResolution.workspaceSetupRequired) {
|
|
2274
|
-
await fetchBootstrapConfig();
|
|
2275
|
-
void fetchReferenceData();
|
|
2276
|
-
return;
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
await fetchBootstrapConfig();
|
|
2280
|
-
await Promise.all([
|
|
2281
|
-
(async () => {
|
|
2282
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
2283
|
-
await fetchTasks(false);
|
|
2284
|
-
logBootstrapDebug('bootstrap_tasks_loaded', {
|
|
2285
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
2286
|
-
});
|
|
2287
|
-
})(),
|
|
2288
|
-
fetchReferenceData(),
|
|
2289
|
-
fetchPlanningEntities()
|
|
2290
|
-
]);
|
|
2291
|
-
if (typeof window !== 'undefined') {
|
|
2292
|
-
if (archiveBootstrapTimerRef.current !== null) {
|
|
2293
|
-
window.clearTimeout(archiveBootstrapTimerRef.current);
|
|
2294
|
-
}
|
|
2295
|
-
archiveBootstrapTimerRef.current = window.setTimeout(() => {
|
|
2296
|
-
archiveBootstrapTimerRef.current = null;
|
|
2297
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
2298
|
-
void fetchArchive(true).then(() => {
|
|
2299
|
-
logBootstrapDebug('bootstrap_archive_loaded_deferred', {
|
|
2300
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
2301
|
-
});
|
|
2302
|
-
});
|
|
2303
|
-
}, 1500);
|
|
2304
|
-
}
|
|
2305
|
-
})();
|
|
2306
|
-
}, [
|
|
2307
|
-
shouldDeferProtectedApiCalls,
|
|
2308
|
-
shouldBlockProtectedApiCalls,
|
|
2309
|
-
runtimeMode,
|
|
2310
|
-
authSessionResolved,
|
|
2311
|
-
isAuthenticated,
|
|
2312
|
-
resolveWorkspaceAfterAuth,
|
|
2313
|
-
fetchBootstrapConfig,
|
|
2314
|
-
fetchTasks,
|
|
2315
|
-
fetchArchive,
|
|
2316
|
-
fetchReferenceData,
|
|
2317
|
-
fetchPlanningEntities
|
|
2318
|
-
]);
|
|
2319
|
-
useEffect(() => {
|
|
2320
|
-
if (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls)
|
|
2321
|
-
return;
|
|
2322
|
-
if (workspaceBootstrapPending)
|
|
2323
|
-
return;
|
|
2324
|
-
if (workspaceSelectionScope === WORKSPACE_BOOTSTRAP_SCOPE)
|
|
2325
|
-
return;
|
|
2326
|
-
if (!hasBootstrappedDataRef.current)
|
|
2327
|
-
return;
|
|
2328
|
-
const loadKey = `${runtimeMode}:${currentWorkspaceId}:${workspaceSelectionScope}`;
|
|
2329
|
-
if (lastConfigRefreshKeyRef.current === loadKey)
|
|
2330
|
-
return;
|
|
2331
|
-
lastConfigRefreshKeyRef.current = loadKey;
|
|
2332
|
-
void fetchConfig();
|
|
2333
|
-
}, [
|
|
2334
|
-
currentWorkspaceId,
|
|
2335
|
-
shouldDeferProtectedApiCalls,
|
|
2336
|
-
shouldBlockProtectedApiCalls,
|
|
2337
|
-
workspaceBootstrapPending,
|
|
2338
|
-
runtimeMode,
|
|
2339
|
-
workspaceSelectionScope,
|
|
2340
|
-
fetchConfig
|
|
2341
|
-
]);
|
|
2342
|
-
useEffect(() => {
|
|
2343
|
-
if (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls)
|
|
2344
|
-
return;
|
|
2345
|
-
if (workspaceBootstrapPending)
|
|
2346
|
-
return;
|
|
2347
|
-
const loadKey = currentWorkspaceId;
|
|
2348
|
-
if (lastWorkspaceSyncStateLoadKeyRef.current === loadKey)
|
|
2349
|
-
return;
|
|
2350
|
-
lastWorkspaceSyncStateLoadKeyRef.current = loadKey;
|
|
2351
|
-
void loadWorkspaceSyncState();
|
|
2352
|
-
}, [shouldDeferProtectedApiCalls, shouldBlockProtectedApiCalls, currentWorkspaceId, loadWorkspaceSyncState, workspaceBootstrapPending]);
|
|
2353
|
-
useEffect(() => {
|
|
2354
|
-
if (runtimeMode !== 'cloud' || !authSessionResolved || !isAuthenticated) {
|
|
2355
|
-
if (runtimeMode !== 'cloud') {
|
|
2356
|
-
setAvailableWorkspaces([]);
|
|
2357
|
-
}
|
|
2358
|
-
workspaceListHydratedRef.current = false;
|
|
2359
|
-
return;
|
|
2360
|
-
}
|
|
2361
|
-
if (workspaceBootstrapPending)
|
|
2362
|
-
return;
|
|
2363
|
-
if (!hasBootstrappedDataRef.current || workspaceListHydratedRef.current)
|
|
2364
|
-
return;
|
|
2365
|
-
fetchWorkspaces();
|
|
2366
|
-
}, [runtimeMode, authSessionResolved, isAuthenticated, fetchWorkspaces, workspaceBootstrapPending]);
|
|
2367
2675
|
// Report task count to parent
|
|
2368
2676
|
useEffect(() => {
|
|
2369
2677
|
if (onTaskCountChange) {
|
|
@@ -2529,6 +2837,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2529
2837
|
return;
|
|
2530
2838
|
lastTaskSurfaceLoadKeyRef.current = loadKey;
|
|
2531
2839
|
fetchTasks();
|
|
2840
|
+
fetchPlanningEntities();
|
|
2532
2841
|
if (showArchive || taskScope === 'archived') {
|
|
2533
2842
|
fetchArchive(true);
|
|
2534
2843
|
}
|
|
@@ -2555,6 +2864,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2555
2864
|
showArchive,
|
|
2556
2865
|
taskScope,
|
|
2557
2866
|
fetchTasks,
|
|
2867
|
+
fetchPlanningEntities,
|
|
2558
2868
|
fetchArchive,
|
|
2559
2869
|
fetchDeletedTasks,
|
|
2560
2870
|
fetchConfig,
|
|
@@ -2723,6 +3033,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2723
3033
|
pushWorkspaceChangesToCloud,
|
|
2724
3034
|
workspacePendingSignatureRef,
|
|
2725
3035
|
workspaceDeletedTaskIdsRef,
|
|
3036
|
+
workspaceDeletedTaskWatermarksRef,
|
|
2726
3037
|
});
|
|
2727
3038
|
const { autoSaveState, flushAutoSave, scheduleWarningPrompt, handleSubmit, confirmScheduleWarning, cancelScheduleWarning, handleAddComment } = useTaskFormActions({
|
|
2728
3039
|
activeCategories,
|
|
@@ -2992,6 +3303,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2992
3303
|
workspaceLastErrorMessage,
|
|
2993
3304
|
workspaceLastSuccessfulSyncAt,
|
|
2994
3305
|
workspaceSyncPhase,
|
|
3306
|
+
workspaceSyncSetupIntent,
|
|
2995
3307
|
workspaceSyncStatus,
|
|
2996
3308
|
workspaceSyncSummary,
|
|
2997
3309
|
workspaceSyncRecommendedAction,
|
|
@@ -3011,6 +3323,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
3011
3323
|
workspaceCloudSyncEnabled,
|
|
3012
3324
|
saveWorkspaceCloudSyncSettings,
|
|
3013
3325
|
applyWorkspaceSyncStateSnapshot,
|
|
3326
|
+
bootstrapState,
|
|
3014
3327
|
authSessionResolved,
|
|
3015
3328
|
workspaceBootstrapPending,
|
|
3016
3329
|
bootstrapPhase,
|
|
@@ -3027,6 +3340,12 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
3027
3340
|
deleteWorkspace,
|
|
3028
3341
|
switchWorkspace,
|
|
3029
3342
|
loginWithCredentials,
|
|
3343
|
+
beginOAuthLogin,
|
|
3344
|
+
availableAuthProviders,
|
|
3345
|
+
fetchLoginMethods,
|
|
3346
|
+
unlinkLoginMethod,
|
|
3347
|
+
addPasswordToAccount,
|
|
3348
|
+
beginOAuthLink,
|
|
3030
3349
|
registerWithCredentials,
|
|
3031
3350
|
updateCurrentUserProfile,
|
|
3032
3351
|
requestEmailVerification,
|
|
@@ -3059,6 +3378,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
3059
3378
|
archivedTasks,
|
|
3060
3379
|
initiatives,
|
|
3061
3380
|
workstreams,
|
|
3381
|
+
planningBootstrapTaskSummaries,
|
|
3062
3382
|
deletedTasks,
|
|
3063
3383
|
activeCategories,
|
|
3064
3384
|
activeTypes,
|
|
@@ -3081,6 +3401,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
3081
3401
|
setFilterAssignees,
|
|
3082
3402
|
filterTaxonomies,
|
|
3083
3403
|
setFilterTaxonomies,
|
|
3404
|
+
hasInitedFilters,
|
|
3084
3405
|
sortBy,
|
|
3085
3406
|
setSortBy,
|
|
3086
3407
|
sortOrder,
|