@taskforcehq/taskforce 0.3.302 → 0.3.304
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 +113 -5
- package/dist/TaskforceCore.js +560 -208
- package/dist/TaskforceCore.test.js +1833 -91
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/TaskSettings.js +58 -38
- package/dist/components/features/TaskSettings.test.js +139 -40
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +3 -3
- package/dist/components/task/TaskActionHeader.test.js +8 -0
- package/dist/components/task/TaskCard.js +16 -1
- package/dist/components/task/TaskCard.test.js +31 -0
- package/dist/components/task/TaskForm.d.ts +2 -1
- package/dist/components/task/TaskForm.js +22 -4
- package/dist/components/task/TaskForm.test.js +46 -0
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +31 -160
- package/dist/components/task/TaskKanban.test.js +17 -0
- package/dist/components/task/TaskStatusActions.d.ts +6 -1
- package/dist/components/task/TaskStatusActions.js +8 -3
- package/dist/components/task/TaskStatusActions.test.js +9 -0
- package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
- package/dist/components/task/taskKanbanDropRules.js +187 -0
- package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
- package/dist/components/task/taskKanbanDropRules.test.js +114 -0
- package/dist/components/ui/Modal.d.ts +1 -1
- package/dist/components/ui/Modal.js +1 -0
- package/dist/components/views/AppShellHeader.js +5 -1
- package/dist/components/views/AppShellHeader.test.js +4 -0
- package/dist/components/views/PlanComparisonPage.d.ts +3 -2
- package/dist/components/views/PlanComparisonPage.js +4 -2
- package/dist/components/views/PlanComparisonPage.test.js +4 -0
- package/dist/components/views/PlansPage.d.ts +7 -1
- package/dist/components/views/PlansPage.js +139 -53
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +157 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +216 -186
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
- package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +31 -5
- package/dist/core/PlanEntitlementService.js +350 -222
- package/dist/core/SignupMonetizationSettings.test.js +208 -49
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/TaskActivityService.d.ts +4 -1
- package/dist/core/TaskActivityService.js +45 -0
- package/dist/core/TaskAuditTimeline.test.js +52 -1
- package/dist/core/Taskforce.d.ts +65 -20
- package/dist/core/Taskforce.js +261 -51
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
- package/dist/core/WorkspacePlanMode.test.js +65 -0
- package/dist/core/types.d.ts +11 -12
- package/dist/documentReviews/service.d.ts +1 -1
- package/dist/documentReviews/types.d.ts +1 -1
- package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
- package/dist/hooks/sync/auth.d.ts +37 -0
- package/dist/hooks/sync/auth.js +61 -0
- package/dist/hooks/sync/bootstrap.d.ts +34 -0
- package/dist/hooks/sync/bootstrap.js +52 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
- package/dist/hooks/sync/orchestratorShared.js +209 -0
- package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
- package/dist/hooks/sync/orchestratorShared.test.js +49 -0
- package/dist/hooks/sync/realtime.d.ts +25 -0
- package/dist/hooks/sync/realtime.js +60 -0
- package/dist/hooks/sync/recovery.d.ts +69 -0
- package/dist/hooks/sync/recovery.js +118 -0
- package/dist/hooks/sync/transfers.d.ts +122 -0
- package/dist/hooks/sync/transfers.js +431 -0
- package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
- package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
- package/dist/hooks/ui/useResourceExport.d.ts +3 -1
- package/dist/hooks/ui/useResourceExport.js +5 -2
- package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
- package/dist/hooks/useRealtimeSync.js +2 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
- package/dist/hooks/useSyncOrchestrator.js +315 -860
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
- package/dist/hooks/useTaskMutations.js +45 -6
- package/dist/hooks/useTaskMutations.test.js +147 -0
- package/dist/hooks/useTaskforce.d.ts +11 -2
- package/dist/hooks/useTaskforce.js +290 -812
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +22 -10
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
- package/dist/hooks/workspace/workspaceLocalState.js +100 -0
- package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
- package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
- package/dist/localization/locales/en-US.js +2 -2
- package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
- package/dist/mcp/canonicalAssetHelpers.js +259 -0
- package/dist/mcp/clientIntegrations.d.ts +12 -1
- package/dist/mcp/clientIntegrations.js +161 -6
- package/dist/mcp/clientIntegrations.test.js +76 -4
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +264 -505
- package/dist/mcp/runtime.test.js +353 -89
- package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
- package/dist/mcp/taskAttachmentHelpers.js +60 -0
- package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
- package/dist/mcp/taskAttachmentTypes.js +1 -0
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +15 -0
- package/dist/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +48 -3
- package/dist/server/index.test.js +14 -2
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +91 -40
- package/dist/server/routes/auth.d.ts +1 -0
- package/dist/server/routes/auth.js +231 -55
- package/dist/server/routes/billing.js +473 -39
- package/dist/server/routes/billing.test.js +972 -37
- package/dist/server/routes/shared.d.ts +30 -0
- package/dist/server/routes/shared.js +31 -3
- package/dist/server/routes/sync.integration.test.js +180 -0
- package/dist/server/routes/sync.js +125 -242
- package/dist/server/routes/tasks.js +49 -7
- package/dist/server/routes.js +11 -22
- package/dist/server/routes.test.js +530 -81
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.js +4 -0
- package/dist/sync/collaborationSyncPayload.test.js +8 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -1
- package/dist/sync/collaborationSyncState.js +19 -0
- package/dist/sync/contentSyncState.d.ts +10 -0
- package/dist/sync/contentSyncState.js +277 -14
- package/dist/sync/syncApplyHandlers.d.ts +87 -1
- package/dist/sync/syncApplyHandlers.js +370 -8
- package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.test.js +377 -0
- package/dist/sync/syncService.d.ts +9 -1
- package/dist/sync/syncService.js +57 -10
- package/dist/sync/syncService.test.js +48 -0
- package/dist/sync/workspacePullFeed.d.ts +11 -0
- package/dist/sync/workspacePullFeed.js +123 -68
- package/dist/sync/workspacePullFeed.test.js +180 -0
- package/dist/sync/workspaceSyncModel.d.ts +22 -1
- package/dist/sync/workspaceSyncModel.js +65 -0
- package/dist/sync/workspaceSyncModel.test.js +119 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
- package/dist/ui/assets/index-DXUdtH1B.js +6 -0
- package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
- package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
- package/dist/utils/accountProfileSummaryCache.js +97 -0
- package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
- package/dist/utils/accountProfileSummaryCache.test.js +63 -0
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +5 -3
- package/scripts/playwright-auth.sh +4 -0
- package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
- package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
- package/dist/ui/assets/index-MXeWoebC.css +0 -1
- package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
- package/dist/utils/billingStatusCache.d.ts +0 -21
- package/dist/utils/billingStatusCache.js +0 -70
|
@@ -39,12 +39,11 @@ import { normalizeTaskFromApi } from '../utils/taskNormalization.js';
|
|
|
39
39
|
import { getSupportedLocales, initializeLocale } from '../localization';
|
|
40
40
|
import { WORKSPACE_BOOTSTRAP_SCOPE, isReservedWorkspaceId, resolveWorkspaceSelectionScopeForRuntime, resolveWorkspaceUiStateScopeForRuntime } from '../utils/workspaceIdentity';
|
|
41
41
|
import { resolveRuntimeModeContract } from '../shared/runtimeContract';
|
|
42
|
-
import { hasWorkspaceTaxonomyStatePayload, normalizeWorkspaceTaxonomyStatePayload } from '../shared/taxonomyState.js';
|
|
43
42
|
import { DEFAULT_TYPE_VALUE } from '../shared/taxonomyDefaults.js';
|
|
44
43
|
import { cloneDefaultWorkflowDefinitions } from '../shared/workflowDefaults.js';
|
|
45
44
|
import { resolveClientDeploymentConfig } from '../config/clientDeployment';
|
|
46
45
|
import { resolveTaskforceMcpBaseUrlFromBaseUrl } from '../config/cloudEnvironment';
|
|
47
|
-
import { createDefaultAssigneeOptions
|
|
46
|
+
import { createDefaultAssigneeOptions } from '../utils/assignees';
|
|
48
47
|
import { persistLocalMutationActorUserId, readLocalMutationActorUserId } from '../utils/taskforceApiClient';
|
|
49
48
|
import { useSyncOrchestrator } from './useSyncOrchestrator';
|
|
50
49
|
import { useRealtimeSync } from './useRealtimeSync';
|
|
@@ -62,9 +61,12 @@ import { useTaskFilters } from './tasks/useTaskFilters';
|
|
|
62
61
|
import { useTaskFormActions } from './tasks/useTaskFormActions';
|
|
63
62
|
import { useTaskRelationships } from './tasks/useTaskRelationships';
|
|
64
63
|
import { useTaskActions } from './tasks/useTaskActions';
|
|
65
|
-
import { logBootstrapDebug, readPersistedAppUiState, readPersistedWorkspaceId, writePersistedAppUiState, writePersistedWorkspaceId } from './workspace/workspaceLocalState';
|
|
64
|
+
import { clearPersistedWorkspaceId, logBootstrapDebug, persistRemoteUiState, readPersistedAppUiState, readPersistedWorkspaceId, writePersistedAppUiState, writePersistedWorkspaceId } from './workspace/workspaceLocalState';
|
|
66
65
|
import { useTaskforceApiRouting } from './workspace/useTaskforceApiRouting';
|
|
67
|
-
import {
|
|
66
|
+
import { useTaskforceAuthBootstrap } from './auth/useTaskforceAuthBootstrap';
|
|
67
|
+
import { deriveAuthGuardPolicy } from './auth/useAuthGuardPolicy';
|
|
68
|
+
import { useTaskforceWorkspaceBootstrap } from './workspace/useTaskforceWorkspaceBootstrap';
|
|
69
|
+
import { useDataVersionRefresh } from './sync/useDataVersionRefresh';
|
|
68
70
|
const ENV_DEPLOYMENT = resolveClientDeploymentConfig(import.meta.env);
|
|
69
71
|
const DEFAULT_CLOUD_APP_ORIGIN = (() => {
|
|
70
72
|
const configuredBase = ENV_DEPLOYMENT.baseUrl;
|
|
@@ -203,7 +205,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
203
205
|
|| envCloudBaseUrl
|
|
204
206
|
|| normalizedCloudAuthBaseUrl
|
|
205
207
|
|| defaultCloudAuthBaseUrl).trim().replace(/\/+$/, '');
|
|
206
|
-
const authOnlyCloudMode = Boolean(normalizedCloudAuthBaseUrl) && !normalizedApiBaseUrl;
|
|
208
|
+
const authOnlyCloudMode = !isCloudHost && Boolean(normalizedCloudAuthBaseUrl) && !normalizedApiBaseUrl;
|
|
207
209
|
const shouldProbeCloudAuth = runtimeConfigReady && (Boolean(normalizedCloudAuthBaseUrl || normalizedApiBaseUrl) || isCloudHost);
|
|
208
210
|
const shouldGateProtectedApiCalls = shouldProbeCloudAuth && !authOnlyCloudMode;
|
|
209
211
|
const cloudAuthConfigured = Boolean(normalizedCloudAuthBaseUrl || normalizedApiBaseUrl || isCloudHost);
|
|
@@ -229,6 +231,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
229
231
|
if (!path.startsWith('/'))
|
|
230
232
|
return path;
|
|
231
233
|
const isCloudScopedPath = path.startsWith('/api/taskforce/auth/')
|
|
234
|
+
|| path.startsWith('/api/taskforce/account/')
|
|
235
|
+
|| path.startsWith('/api/taskforce/billing/')
|
|
232
236
|
|| path.startsWith('/api/taskforce/sync/')
|
|
233
237
|
|| path.startsWith('/api/taskforce/settings/mcp/');
|
|
234
238
|
if (!isCloudHost && isCloudScopedPath && (!runtimeConfigReady || runtimeConfigOverride.cloudAuthViaLocalProxy)) {
|
|
@@ -343,6 +347,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
343
347
|
const [bootstrapError, setBootstrapError] = useState(null);
|
|
344
348
|
const [bootstrapStartedAt, setBootstrapStartedAt] = useState(null);
|
|
345
349
|
const authSessionRequestRef = useRef(null);
|
|
350
|
+
const authSessionEpochRef = useRef(0);
|
|
346
351
|
const authSessionLastCheckedAtRef = useRef(0);
|
|
347
352
|
const authSessionLastResultRef = useRef(hasOptimisticLocalAuth);
|
|
348
353
|
const currentWorkspaceIdRef = useRef(currentWorkspaceId);
|
|
@@ -373,25 +378,16 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
373
378
|
useEffect(() => {
|
|
374
379
|
currentWorkspaceIdRef.current = currentWorkspaceId;
|
|
375
380
|
}, [currentWorkspaceId]);
|
|
376
|
-
const applyResolvedWorkspaceId = useCallback((nextWorkspaceIdRaw
|
|
381
|
+
const applyResolvedWorkspaceId = useCallback((nextWorkspaceIdRaw) => {
|
|
377
382
|
const nextWorkspaceId = String(nextWorkspaceIdRaw || '').trim();
|
|
378
383
|
if (!nextWorkspaceId)
|
|
379
384
|
return;
|
|
380
|
-
setCurrentWorkspaceId((
|
|
385
|
+
setCurrentWorkspaceId(() => {
|
|
381
386
|
const explicitWorkspaceId = String(explicitWorkspaceSelectionRef.current || '').trim();
|
|
382
387
|
let resolvedWorkspaceId = nextWorkspaceId;
|
|
383
388
|
if (explicitWorkspaceId && explicitWorkspaceId !== nextWorkspaceId) {
|
|
384
389
|
resolvedWorkspaceId = explicitWorkspaceId;
|
|
385
390
|
}
|
|
386
|
-
else {
|
|
387
|
-
const prevValue = String(prev || '').trim();
|
|
388
|
-
if (options?.preserveNonDefaultDefault
|
|
389
|
-
&& nextWorkspaceId === 'default'
|
|
390
|
-
&& prevValue
|
|
391
|
-
&& prevValue.toLowerCase() !== 'default') {
|
|
392
|
-
resolvedWorkspaceId = prevValue;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
391
|
currentWorkspaceIdRef.current = resolvedWorkspaceId;
|
|
396
392
|
return resolvedWorkspaceId;
|
|
397
393
|
});
|
|
@@ -475,7 +471,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
475
471
|
shouldBlockProtectedApiCalls
|
|
476
472
|
});
|
|
477
473
|
const { zenMode, setZenModeState, toggleZenMode } = useZenMode();
|
|
478
|
-
const { exportEnvironment, setExportEnvironment, availableEnvironments, loadAvailableEnvironments, exportWorkflowsPath, setExportWorkflowsPath, exportingResource, exportResult, handleExportWorkflows } = useResourceExport({
|
|
474
|
+
const { exportEnvironment, setExportEnvironment, availableEnvironments, loadAvailableEnvironments, exportWorkflowsPath, setExportWorkflowsPath, exportingResource, exportResult, handleExportWorkflows } = useResourceExport({
|
|
475
|
+
storagePath,
|
|
476
|
+
shouldDeferProtectedApiCalls,
|
|
477
|
+
shouldBlockProtectedApiCalls
|
|
478
|
+
});
|
|
479
479
|
// Unsaved Changes Modal State
|
|
480
480
|
const [unsavedModalOpen, setUnsavedModalOpen] = useState(false);
|
|
481
481
|
const [pendingNavigation, setPendingNavigation] = useState(null);
|
|
@@ -811,158 +811,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
811
811
|
await fetchDeletedTasks(true);
|
|
812
812
|
await fetchPlanningEntities();
|
|
813
813
|
}, [fetchDeletedTasks, fetchPlanningEntities, refreshTaskCollectionsFromInvalidationBase]);
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
// Keep prior auth identity during bootstrap until runtime auth config is resolved.
|
|
819
|
-
// This avoids transient "signed out" UI flicker on refresh.
|
|
820
|
-
return authSessionLastResultRef.current;
|
|
821
|
-
}
|
|
822
|
-
if (!shouldProbeCloudAuth) {
|
|
823
|
-
setRuntimeMode('local');
|
|
824
|
-
setAuthRequiredForApi(false);
|
|
825
|
-
setIsAuthenticated(false);
|
|
826
|
-
setAuthUserId('anonymous');
|
|
827
|
-
setAuthWorkspaceId('');
|
|
828
|
-
persistLocalMutationActorUserId(null);
|
|
829
|
-
setAuthUserEmail('');
|
|
830
|
-
setAuthUserDisplayName('');
|
|
831
|
-
setAuthUserAvatarUrl('');
|
|
832
|
-
setUserGlobalSyncStatus('disconnected');
|
|
833
|
-
setUserGlobalSyncError(null);
|
|
834
|
-
setHasBetaAccess(true);
|
|
835
|
-
setAvailableWorkspaces([]);
|
|
836
|
-
setHydratedWorkspaceRole(null);
|
|
837
|
-
setAssigneeOptions(createDefaultAssigneeOptions());
|
|
838
|
-
setAuthBlocked(false);
|
|
839
|
-
setAuthSessionResolved(true);
|
|
840
|
-
authSessionLastResultRef.current = false;
|
|
841
|
-
authSessionLastCheckedAtRef.current = Date.now();
|
|
842
|
-
return false;
|
|
843
|
-
}
|
|
844
|
-
const sessionUrl = resolveCloudAuthUrl('/api/taskforce/auth/session');
|
|
845
|
-
const now = Date.now();
|
|
846
|
-
if (!force && authSessionRequestRef.current)
|
|
847
|
-
return authSessionRequestRef.current;
|
|
848
|
-
if (!force && authSessionResolved && (now - authSessionLastCheckedAtRef.current) < 1500) {
|
|
849
|
-
return authSessionLastResultRef.current;
|
|
850
|
-
}
|
|
851
|
-
const request = (async () => {
|
|
852
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
853
|
-
const abortController = new AbortController();
|
|
854
|
-
const timeoutId = typeof window !== 'undefined'
|
|
855
|
-
? window.setTimeout(() => abortController.abort(), 8000)
|
|
856
|
-
: null;
|
|
857
|
-
try {
|
|
858
|
-
const res = await fetch(sessionUrl, {
|
|
859
|
-
method: 'GET',
|
|
860
|
-
credentials: 'include',
|
|
861
|
-
mode: 'cors',
|
|
862
|
-
signal: abortController.signal
|
|
863
|
-
});
|
|
864
|
-
if (res.status === 429) {
|
|
865
|
-
setAuthSessionResolved(true);
|
|
866
|
-
return authSessionLastResultRef.current;
|
|
867
|
-
}
|
|
868
|
-
if (!res.ok) {
|
|
869
|
-
if (isLoopbackHost)
|
|
870
|
-
persistLocalMutationActorUserId(null);
|
|
871
|
-
setAuthSessionResolved(true);
|
|
872
|
-
authSessionLastResultRef.current = false;
|
|
873
|
-
return false;
|
|
874
|
-
}
|
|
875
|
-
const data = await res.json();
|
|
876
|
-
const required = Boolean(data.authRequiredForApi);
|
|
877
|
-
const authenticated = Boolean(data.authenticated);
|
|
878
|
-
const betaAccess = authenticated ? (data.betaAccess !== false) : true;
|
|
879
|
-
const workspaceId = typeof data.workspaceId === 'string' && data.workspaceId.trim().length > 0
|
|
880
|
-
? data.workspaceId.trim()
|
|
881
|
-
: '';
|
|
882
|
-
const userId = typeof data.userId === 'string' && data.userId.trim().length > 0
|
|
883
|
-
? data.userId.trim()
|
|
884
|
-
: 'anonymous';
|
|
885
|
-
const userEmail = typeof data.email === 'string' && data.email.trim().length > 0
|
|
886
|
-
? data.email.trim().toLowerCase()
|
|
887
|
-
: '';
|
|
888
|
-
const userDisplayName = typeof data.displayName === 'string'
|
|
889
|
-
? data.displayName.trim()
|
|
890
|
-
: '';
|
|
891
|
-
const userAvatarUrl = typeof data.avatarUrl === 'string'
|
|
892
|
-
? data.avatarUrl.trim()
|
|
893
|
-
: '';
|
|
894
|
-
const isLocalRuntime = runtimeMode === 'local';
|
|
895
|
-
const keepLocalWorkspaceContext = runtimeMode === 'local' || authOnlyCloudMode;
|
|
896
|
-
setAuthRequiredForApi(authOnlyCloudMode ? false : (isLocalRuntime ? false : required));
|
|
897
|
-
setIsAuthenticated(authenticated);
|
|
898
|
-
setAuthUserId(authenticated ? userId : 'anonymous');
|
|
899
|
-
setAuthWorkspaceId(authenticated ? workspaceId : '');
|
|
900
|
-
if (isLoopbackHost) {
|
|
901
|
-
persistLocalMutationActorUserId(authenticated ? userId : null);
|
|
902
|
-
}
|
|
903
|
-
setAuthUserEmail(authenticated ? userEmail : '');
|
|
904
|
-
setAuthUserDisplayName(authenticated ? userDisplayName : '');
|
|
905
|
-
setAuthUserAvatarUrl(authenticated ? userAvatarUrl : '');
|
|
906
|
-
if (authenticated) {
|
|
907
|
-
setError((prev) => prev === AUTH_REQUIRED_ERROR ? '' : prev);
|
|
908
|
-
}
|
|
909
|
-
setUserGlobalSyncStatus(authenticated ? 'idle' : 'disconnected');
|
|
910
|
-
setUserGlobalSyncError(null);
|
|
911
|
-
setHasBetaAccess(betaAccess);
|
|
912
|
-
if (keepLocalWorkspaceContext) {
|
|
913
|
-
// Local runtime resolves workspace identity from local DB via /api/taskforce/config.
|
|
914
|
-
// Do not override it from cloud auth session payloads.
|
|
915
|
-
}
|
|
916
|
-
else {
|
|
917
|
-
applyResolvedWorkspaceId(workspaceId || readPersistedWorkspaceId(workspaceSelectionScope) || 'default');
|
|
918
|
-
}
|
|
919
|
-
setAuthBlocked(authOnlyCloudMode ? false : (isLocalRuntime ? false : (required && !authenticated)));
|
|
920
|
-
// Runtime mode should be sourced from /api/taskforce/config, not
|
|
921
|
-
// from auth-session responses which may come from a separate cloud-auth origin.
|
|
922
|
-
setAuthSessionResolved(true);
|
|
923
|
-
authSessionLastResultRef.current = authenticated;
|
|
924
|
-
logBootstrapDebug('auth_session_resolved', {
|
|
925
|
-
authenticated,
|
|
926
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
927
|
-
});
|
|
928
|
-
if (authenticated)
|
|
929
|
-
markBootstrapPhase('ready');
|
|
930
|
-
return authenticated;
|
|
931
|
-
}
|
|
932
|
-
catch {
|
|
933
|
-
if (isLoopbackHost)
|
|
934
|
-
persistLocalMutationActorUserId(null);
|
|
935
|
-
setAuthSessionResolved(true);
|
|
936
|
-
authSessionLastResultRef.current = false;
|
|
937
|
-
logBootstrapDebug('auth_session_failed', {
|
|
938
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
939
|
-
});
|
|
940
|
-
markBootstrapStalled('Unable to verify your session.');
|
|
941
|
-
return authSessionLastResultRef.current;
|
|
942
|
-
}
|
|
943
|
-
finally {
|
|
944
|
-
if (timeoutId !== null && typeof window !== 'undefined') {
|
|
945
|
-
window.clearTimeout(timeoutId);
|
|
946
|
-
}
|
|
947
|
-
authSessionLastCheckedAtRef.current = Date.now();
|
|
948
|
-
authSessionRequestRef.current = null;
|
|
949
|
-
}
|
|
950
|
-
})();
|
|
951
|
-
authSessionRequestRef.current = request;
|
|
952
|
-
return request;
|
|
953
|
-
}, [
|
|
954
|
-
runtimeConfigReady,
|
|
955
|
-
shouldProbeCloudAuth,
|
|
956
|
-
authSessionResolved,
|
|
957
|
-
resolveCloudAuthUrl,
|
|
958
|
-
authOnlyCloudMode,
|
|
959
|
-
AUTH_REQUIRED_ERROR,
|
|
960
|
-
runtimeMode,
|
|
961
|
-
applyResolvedWorkspaceId,
|
|
962
|
-
workspaceSelectionScope,
|
|
963
|
-
markBootstrapPhase,
|
|
964
|
-
markBootstrapStalled
|
|
965
|
-
]);
|
|
814
|
+
const setUserGlobalSyncStatusRef = useRef(() => undefined);
|
|
815
|
+
const setUserGlobalSyncErrorRef = useRef(() => undefined);
|
|
816
|
+
const checkAuthSessionImplRef = useRef(async () => false);
|
|
817
|
+
const checkAuthSession = useCallback((options) => checkAuthSessionImplRef.current(options), []);
|
|
966
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({
|
|
967
819
|
currentWorkspaceId,
|
|
968
820
|
cloudAuthConfigured,
|
|
@@ -1044,6 +896,50 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1044
896
|
assigneeOptionsLoaded,
|
|
1045
897
|
assigneeOptions
|
|
1046
898
|
});
|
|
899
|
+
setUserGlobalSyncStatusRef.current = setUserGlobalSyncStatus;
|
|
900
|
+
setUserGlobalSyncErrorRef.current = setUserGlobalSyncError;
|
|
901
|
+
const { checkAuthSession: checkAuthSessionImpl } = useTaskforceAuthBootstrap({
|
|
902
|
+
runtimeConfigReady,
|
|
903
|
+
shouldProbeCloudAuth,
|
|
904
|
+
authSessionResolved,
|
|
905
|
+
resolveCloudAuthUrl,
|
|
906
|
+
authOnlyCloudMode,
|
|
907
|
+
authRequiredError: AUTH_REQUIRED_ERROR,
|
|
908
|
+
runtimeMode,
|
|
909
|
+
workspaceSelectionScope,
|
|
910
|
+
markBootstrapPhase,
|
|
911
|
+
markBootstrapStalled,
|
|
912
|
+
setRuntimeMode,
|
|
913
|
+
setAuthRequiredForApi,
|
|
914
|
+
setIsAuthenticated,
|
|
915
|
+
setAuthUserId,
|
|
916
|
+
setAuthWorkspaceId,
|
|
917
|
+
setAuthUserEmail,
|
|
918
|
+
setAuthUserDisplayName,
|
|
919
|
+
setAuthUserAvatarUrl,
|
|
920
|
+
setUserGlobalSyncStatus: (value) => setUserGlobalSyncStatusRef.current(value),
|
|
921
|
+
setUserGlobalSyncError: (value) => setUserGlobalSyncErrorRef.current(value),
|
|
922
|
+
setHasBetaAccess,
|
|
923
|
+
setAvailableWorkspaces,
|
|
924
|
+
setHydratedWorkspaceRole,
|
|
925
|
+
setAssigneeOptions,
|
|
926
|
+
setAuthBlocked,
|
|
927
|
+
setAuthSessionResolved,
|
|
928
|
+
setError,
|
|
929
|
+
applyResolvedWorkspaceId,
|
|
930
|
+
readPersistedWorkspaceId,
|
|
931
|
+
authSessionRequestRef,
|
|
932
|
+
authSessionEpochRef,
|
|
933
|
+
authSessionLastCheckedAtRef,
|
|
934
|
+
authSessionLastResultRef,
|
|
935
|
+
isLoopbackHost,
|
|
936
|
+
setAuthStateReadyRefs: (authenticated, resolved, required) => {
|
|
937
|
+
authSessionResolvedRef.current = resolved;
|
|
938
|
+
authRequiredForApiRef.current = required;
|
|
939
|
+
isAuthenticatedRef.current = authenticated;
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
checkAuthSessionImplRef.current = checkAuthSessionImpl;
|
|
1047
943
|
const [activeWorkspaceModule, setActiveWorkspaceModule] = useState('tasks');
|
|
1048
944
|
const [showComments, setShowComments] = useState(false);
|
|
1049
945
|
const [descriptionFocused, setDescriptionFocused] = useState(false);
|
|
@@ -1067,6 +963,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1067
963
|
const skipNextUiStatePersistRef = useRef(false);
|
|
1068
964
|
const referenceDataRequestRef = useRef(null);
|
|
1069
965
|
const workspaceListHydratedRef = useRef(false);
|
|
966
|
+
const hasBootstrappedDataRef = useRef(false);
|
|
1070
967
|
const [uiStateReady, setUiStateReady] = useState(false);
|
|
1071
968
|
const { handleSaveSettings, handleSaveTheme, handleSaveGlobalTheme, handleJsonBackupEnabledChange, handleSaveGlobalJsonBackupEnabled, handleSaveGlobalWeekStartsOn, handleSaveLocale, handleManualComplexityEnabledChange, handleChecklistDropdownEnabledChange, handleShowTaskCardStatusLabelChange, handleResetProjectToGlobal } = useSettingsPersistence({
|
|
1072
969
|
keyShortcut,
|
|
@@ -1234,15 +1131,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1234
1131
|
writePersistedAppUiState(uiStateSnapshot, workspaceUiStateScope);
|
|
1235
1132
|
const timeoutId = window.setTimeout(async () => {
|
|
1236
1133
|
try {
|
|
1237
|
-
await
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
body: JSON.stringify({
|
|
1242
|
-
stateKey: 'app',
|
|
1243
|
-
workspaceId: currentWorkspaceIdRef.current,
|
|
1244
|
-
patch: uiStateSnapshot
|
|
1245
|
-
})
|
|
1134
|
+
await persistRemoteUiState({
|
|
1135
|
+
stateKey: 'app',
|
|
1136
|
+
workspaceId: currentWorkspaceIdRef.current,
|
|
1137
|
+
patch: uiStateSnapshot
|
|
1246
1138
|
});
|
|
1247
1139
|
}
|
|
1248
1140
|
catch {
|
|
@@ -1279,13 +1171,15 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1279
1171
|
? data.userId.trim()
|
|
1280
1172
|
: 'anonymous';
|
|
1281
1173
|
const serverAuthenticated = nextRuntimeMode === 'cloud' && nextUserId !== 'anonymous';
|
|
1174
|
+
const knownAuthenticated = serverAuthenticated || isAuthenticatedRef.current;
|
|
1175
|
+
const authResolved = authSessionResolvedRef.current;
|
|
1282
1176
|
setRuntimeMode(nextRuntimeMode);
|
|
1283
1177
|
setAuthRequiredForApi(nextAuthRequiredForApi);
|
|
1284
1178
|
if (serverAuthenticated) {
|
|
1285
1179
|
setIsAuthenticated(true);
|
|
1286
1180
|
setAuthUserId(nextUserId);
|
|
1287
1181
|
setAuthBlocked(false);
|
|
1288
|
-
if (!
|
|
1182
|
+
if (!authResolved) {
|
|
1289
1183
|
setAuthSessionResolved(true);
|
|
1290
1184
|
}
|
|
1291
1185
|
}
|
|
@@ -1295,11 +1189,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1295
1189
|
commitCurrentWorkspaceId(nextWorkspaceId);
|
|
1296
1190
|
}
|
|
1297
1191
|
else {
|
|
1298
|
-
applyResolvedWorkspaceId(nextWorkspaceId
|
|
1192
|
+
applyResolvedWorkspaceId(nextWorkspaceId);
|
|
1299
1193
|
}
|
|
1300
1194
|
}
|
|
1301
1195
|
if (nextRuntimeMode === 'cloud' && nextAuthRequiredForApi) {
|
|
1302
|
-
setAuthBlocked(!
|
|
1196
|
+
setAuthBlocked(!knownAuthenticated);
|
|
1303
1197
|
}
|
|
1304
1198
|
else {
|
|
1305
1199
|
setAuthBlocked(false);
|
|
@@ -1397,7 +1291,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1397
1291
|
else {
|
|
1398
1292
|
setShowTaskCardStatusLabel(true);
|
|
1399
1293
|
}
|
|
1400
|
-
}, [applyResolvedWorkspaceId,
|
|
1294
|
+
}, [applyResolvedWorkspaceId, commitCurrentWorkspaceId]);
|
|
1401
1295
|
const fetchBuildInfo = useCallback(async () => {
|
|
1402
1296
|
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
1403
1297
|
try {
|
|
@@ -1423,417 +1317,83 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1423
1317
|
}
|
|
1424
1318
|
}
|
|
1425
1319
|
}, [fetchWithTimeout, isBootstrapTimeoutError, markBootstrapStalled]);
|
|
1426
|
-
const fetchReferenceData
|
|
1427
|
-
const ignoreAuthGuard = options?.ignoreAuthGuard === true;
|
|
1428
|
-
const force = options?.force === true;
|
|
1429
|
-
if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
|
|
1430
|
-
return;
|
|
1431
|
-
const requestState = getWorkspaceRequestState();
|
|
1432
|
-
if (!force && referenceDataRequestRef.current?.workspaceId === requestState.workspaceId) {
|
|
1433
|
-
await referenceDataRequestRef.current.promise;
|
|
1434
|
-
return;
|
|
1435
|
-
}
|
|
1436
|
-
setReferenceDataLoaded(false);
|
|
1437
|
-
const abortController = new AbortController();
|
|
1438
|
-
referenceDataAbortRef.current = abortController;
|
|
1439
|
-
const request = (async () => {
|
|
1440
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
1441
|
-
await Promise.allSettled([
|
|
1442
|
-
(async () => {
|
|
1443
|
-
const taxonomyRes = await fetch('/api/taskforce/taxonomy-state', { signal: abortController.signal });
|
|
1444
|
-
if (!taxonomyRes.ok)
|
|
1445
|
-
return;
|
|
1446
|
-
const taxonomyData = await taxonomyRes.json().catch(() => ({}));
|
|
1447
|
-
const hasUnifiedPayload = hasWorkspaceTaxonomyStatePayload(taxonomyData);
|
|
1448
|
-
if (!hasUnifiedPayload) {
|
|
1449
|
-
const [catRes, typeRes, priorityRes, taxRes] = await Promise.all([
|
|
1450
|
-
fetch('/api/taskforce/categories', { signal: abortController.signal }),
|
|
1451
|
-
fetch('/api/taskforce/types', { signal: abortController.signal }),
|
|
1452
|
-
fetch('/api/taskforce/priorities', { signal: abortController.signal }),
|
|
1453
|
-
fetch('/api/taskforce/taxonomies', { signal: abortController.signal })
|
|
1454
|
-
]);
|
|
1455
|
-
const [catData, typeData, priorityData, taxData] = await Promise.all([
|
|
1456
|
-
catRes.ok ? catRes.json().catch(() => ({})) : Promise.resolve({}),
|
|
1457
|
-
typeRes.ok ? typeRes.json().catch(() => ({})) : Promise.resolve({}),
|
|
1458
|
-
priorityRes.ok ? priorityRes.json().catch(() => ({})) : Promise.resolve({}),
|
|
1459
|
-
taxRes.ok ? taxRes.json().catch(() => ({})) : Promise.resolve({})
|
|
1460
|
-
]);
|
|
1461
|
-
taxonomyData.categories = catData.categories;
|
|
1462
|
-
taxonomyData.types = typeData.types;
|
|
1463
|
-
taxonomyData.priorities = priorityData.priorities;
|
|
1464
|
-
taxonomyData.taxonomies = taxData.taxonomies;
|
|
1465
|
-
}
|
|
1466
|
-
const normalizedTaxonomyState = normalizeWorkspaceTaxonomyStatePayload(taxonomyData);
|
|
1467
|
-
if (isWorkspaceRequestStale(requestState))
|
|
1468
|
-
return;
|
|
1469
|
-
if (normalizedTaxonomyState.categories.length > 0)
|
|
1470
|
-
setCustomCategories(normalizedTaxonomyState.categories);
|
|
1471
|
-
if (normalizedTaxonomyState.types.length > 0)
|
|
1472
|
-
setCustomTypes(normalizedTaxonomyState.types);
|
|
1473
|
-
if (normalizedTaxonomyState.priorities.length > 0)
|
|
1474
|
-
setPriorities(normalizedTaxonomyState.priorities);
|
|
1475
|
-
setTaxonomies(normalizedTaxonomyState.taxonomies);
|
|
1476
|
-
setTaxonomyDisplayLabels(normalizedTaxonomyState.displayLabels || {});
|
|
1477
|
-
})(),
|
|
1478
|
-
(async () => {
|
|
1479
|
-
await fetchWorkflows();
|
|
1480
|
-
})(),
|
|
1481
|
-
(async () => {
|
|
1482
|
-
await fetchInitiativeTemplates();
|
|
1483
|
-
})(),
|
|
1484
|
-
(async () => {
|
|
1485
|
-
await loadAvailableEnvironments();
|
|
1486
|
-
})(),
|
|
1487
|
-
]);
|
|
1488
|
-
if (isWorkspaceRequestStale(requestState))
|
|
1489
|
-
return;
|
|
1490
|
-
setReferenceDataLoaded(true);
|
|
1491
|
-
logBootstrapDebug('reference_data_loaded', {
|
|
1492
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
1493
|
-
});
|
|
1494
|
-
})();
|
|
1495
|
-
referenceDataRequestRef.current = { workspaceId: requestState.workspaceId, promise: request };
|
|
1496
|
-
try {
|
|
1497
|
-
await request;
|
|
1498
|
-
}
|
|
1499
|
-
catch (error) {
|
|
1500
|
-
if (!isAbortError(error))
|
|
1501
|
-
throw error;
|
|
1502
|
-
}
|
|
1503
|
-
finally {
|
|
1504
|
-
if (referenceDataRequestRef.current?.promise === request) {
|
|
1505
|
-
referenceDataRequestRef.current = null;
|
|
1506
|
-
}
|
|
1507
|
-
if (referenceDataAbortRef.current === abortController) {
|
|
1508
|
-
referenceDataAbortRef.current = null;
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
}, [
|
|
1512
|
-
fetchWorkflows,
|
|
1513
|
-
fetchInitiativeTemplates,
|
|
1514
|
-
getWorkspaceRequestState,
|
|
1515
|
-
isAbortError,
|
|
1516
|
-
isWorkspaceRequestStale,
|
|
1517
|
-
loadAvailableEnvironments,
|
|
1320
|
+
const { fetchReferenceData, fetchBootstrapConfig, fetchConfig, fetchWorkspaces, fetchAssigneeOptions, switchWorkspace, resolveWorkspaceAfterAuth, retryBootstrapChecks } = useTaskforceWorkspaceBootstrap({
|
|
1518
1321
|
shouldDeferProtectedApiCalls,
|
|
1519
|
-
shouldBlockProtectedApiCalls
|
|
1520
|
-
]);
|
|
1521
|
-
const fetchBootstrapConfig = useCallback(async (options) => {
|
|
1522
|
-
const ignoreAuthGuard = options?.ignoreAuthGuard === true;
|
|
1523
|
-
if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
|
|
1524
|
-
return;
|
|
1525
|
-
markBootstrapPhase('config');
|
|
1526
|
-
const requestState = getWorkspaceRequestState();
|
|
1527
|
-
const abortController = new AbortController();
|
|
1528
|
-
bootstrapConfigAbortRef.current = abortController;
|
|
1529
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
1530
|
-
try {
|
|
1531
|
-
const res = await fetchWithTimeout('/api/taskforce/config', undefined, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
|
|
1532
|
-
signal: abortController.signal
|
|
1533
|
-
});
|
|
1534
|
-
if (isWorkspaceEpochStale(requestState))
|
|
1535
|
-
return;
|
|
1536
|
-
if (res.status === 401) {
|
|
1537
|
-
handleUnauthorized();
|
|
1538
|
-
setConfigLoaded(true);
|
|
1539
|
-
return;
|
|
1540
|
-
}
|
|
1541
|
-
let configResolvedWorkspaceId = '';
|
|
1542
|
-
if (res.ok) {
|
|
1543
|
-
const data = await res.json();
|
|
1544
|
-
if (isWorkspaceEpochStale(requestState))
|
|
1545
|
-
return;
|
|
1546
|
-
configResolvedWorkspaceId = typeof data?.workspaceId === 'string' && data.workspaceId.trim().length > 0
|
|
1547
|
-
? data.workspaceId.trim()
|
|
1548
|
-
: '';
|
|
1549
|
-
applyBootstrapConfig(data);
|
|
1550
|
-
}
|
|
1551
|
-
if (isWorkspaceEpochStale(requestState)) {
|
|
1552
|
-
const liveWorkspaceId = String(currentWorkspaceIdRef.current || 'default').trim() || 'default';
|
|
1553
|
-
if (!configResolvedWorkspaceId || liveWorkspaceId !== configResolvedWorkspaceId) {
|
|
1554
|
-
return;
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
if (!hasLoadedUiStateRef.current) {
|
|
1558
|
-
const uiStateWorkspaceId = configResolvedWorkspaceId || requestState.workspaceId;
|
|
1559
|
-
hasLoadedUiStateRef.current = true;
|
|
1560
|
-
await loadPersistedUiState(uiStateWorkspaceId);
|
|
1561
|
-
}
|
|
1562
|
-
if (isWorkspaceEpochStale(requestState)) {
|
|
1563
|
-
const liveWorkspaceId = String(currentWorkspaceIdRef.current || 'default').trim() || 'default';
|
|
1564
|
-
if (!configResolvedWorkspaceId || liveWorkspaceId !== configResolvedWorkspaceId) {
|
|
1565
|
-
return;
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
setConfigLoaded(true);
|
|
1569
|
-
markBootstrapPhase('ready');
|
|
1570
|
-
logBootstrapDebug('bootstrap_config_loaded', {
|
|
1571
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
1572
|
-
});
|
|
1573
|
-
void fetchBuildInfo();
|
|
1574
|
-
}
|
|
1575
|
-
catch (error) {
|
|
1576
|
-
if (isAbortError(error))
|
|
1577
|
-
return;
|
|
1578
|
-
if (isWorkspaceEpochStale(requestState))
|
|
1579
|
-
return;
|
|
1580
|
-
if (!hasLoadedUiStateRef.current) {
|
|
1581
|
-
hasLoadedUiStateRef.current = true;
|
|
1582
|
-
await loadPersistedUiState(requestState.workspaceId);
|
|
1583
|
-
}
|
|
1584
|
-
if (isBootstrapTimeoutError(error)) {
|
|
1585
|
-
markBootstrapStalled('Startup checks timed out.');
|
|
1586
|
-
}
|
|
1587
|
-
else {
|
|
1588
|
-
markBootstrapStalled('Unable to finish startup checks.');
|
|
1589
|
-
}
|
|
1590
|
-
setConfigLoaded(true);
|
|
1591
|
-
}
|
|
1592
|
-
finally {
|
|
1593
|
-
if (bootstrapConfigAbortRef.current === abortController) {
|
|
1594
|
-
bootstrapConfigAbortRef.current = null;
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
}, [
|
|
1598
|
-
applyBootstrapConfig,
|
|
1599
|
-
fetchBuildInfo,
|
|
1600
|
-
fetchWithTimeout,
|
|
1322
|
+
shouldBlockProtectedApiCalls,
|
|
1601
1323
|
getWorkspaceRequestState,
|
|
1602
|
-
|
|
1324
|
+
isWorkspaceRequestStale,
|
|
1325
|
+
isWorkspaceEpochStale,
|
|
1603
1326
|
isAbortError,
|
|
1604
1327
|
isBootstrapTimeoutError,
|
|
1605
|
-
|
|
1606
|
-
|
|
1328
|
+
fetchWithTimeout,
|
|
1329
|
+
handleUnauthorized,
|
|
1330
|
+
applyBootstrapConfig,
|
|
1607
1331
|
loadPersistedUiState,
|
|
1332
|
+
fetchBuildInfo,
|
|
1608
1333
|
markBootstrapPhase,
|
|
1609
1334
|
markBootstrapStalled,
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1335
|
+
fetchWorkflows,
|
|
1336
|
+
fetchInitiativeTemplates,
|
|
1337
|
+
loadAvailableEnvironments,
|
|
1338
|
+
setReferenceDataLoaded,
|
|
1339
|
+
setCustomCategories,
|
|
1340
|
+
setCustomTypes,
|
|
1341
|
+
setPriorities,
|
|
1342
|
+
setTaxonomies,
|
|
1343
|
+
setTaxonomyDisplayLabels,
|
|
1344
|
+
setConfigLoaded,
|
|
1345
|
+
hasLoadedUiStateRef,
|
|
1346
|
+
currentWorkspaceIdRef,
|
|
1347
|
+
referenceDataRequestRef,
|
|
1348
|
+
referenceDataAbortRef,
|
|
1349
|
+
bootstrapConfigAbortRef,
|
|
1350
|
+
workspaceListAbortRef,
|
|
1351
|
+
assigneeOptionsAbortRef,
|
|
1352
|
+
workspaceListHydratedRef,
|
|
1353
|
+
setAvailableWorkspaces,
|
|
1354
|
+
runtimeMode,
|
|
1355
|
+
applyResolvedWorkspaceId,
|
|
1356
|
+
workspaceSwitchingEnabled,
|
|
1357
|
+
authSessionResolved,
|
|
1358
|
+
isAuthenticated,
|
|
1359
|
+
authUserId,
|
|
1360
|
+
setHydratedWorkspaceRole,
|
|
1361
|
+
setAssigneeOptions,
|
|
1362
|
+
setAssigneeOptionsLoaded,
|
|
1363
|
+
commitCurrentWorkspaceId: (workspaceId, options) => {
|
|
1364
|
+
explicitWorkspaceSelectionRef.current = workspaceId;
|
|
1365
|
+
commitCurrentWorkspaceId(workspaceId, options);
|
|
1366
|
+
if (options?.clearExplicitSelection !== false) {
|
|
1367
|
+
explicitWorkspaceSelectionRef.current = null;
|
|
1368
|
+
}
|
|
1369
|
+
},
|
|
1370
|
+
currentWorkspaceId,
|
|
1371
|
+
workspaceSelectionScope,
|
|
1372
|
+
abortWorkspaceScopedRequests,
|
|
1373
|
+
setWorkspaceBootstrapPending,
|
|
1374
|
+
checkAuthSession,
|
|
1375
|
+
lastConfigRefreshKeyRef,
|
|
1376
|
+
lastAssigneeOptionsLoadKeyRef,
|
|
1377
|
+
lastWorkspaceSyncStateLoadKeyRef,
|
|
1378
|
+
lastTaskSurfaceLoadKeyRef,
|
|
1379
|
+
lastSettingsSurfaceLoadKeyRef,
|
|
1380
|
+
isOpen,
|
|
1381
|
+
activeTab,
|
|
1382
|
+
showArchive,
|
|
1383
|
+
taskScope,
|
|
1384
|
+
settingsSection,
|
|
1385
|
+
fetchTasks,
|
|
1386
|
+
fetchArchive,
|
|
1387
|
+
fetchPlanningEntities,
|
|
1388
|
+
refreshTaskCollections,
|
|
1389
|
+
loadWorkspaceSyncState,
|
|
1390
|
+
archiveBootstrapTimerRef,
|
|
1391
|
+
hasBootstrappedDataRef
|
|
1392
|
+
});
|
|
1617
1393
|
const refreshSetupContext = useCallback(async () => {
|
|
1618
1394
|
await checkAuthSession();
|
|
1619
1395
|
await fetchConfig();
|
|
1620
1396
|
}, [checkAuthSession, fetchConfig]);
|
|
1621
|
-
const fetchWorkspaces = useCallback(async () => {
|
|
1622
|
-
if (!workspaceSwitchingEnabled) {
|
|
1623
|
-
setAvailableWorkspaces([]);
|
|
1624
|
-
workspaceListHydratedRef.current = false;
|
|
1625
|
-
return { success: true, workspaces: [] };
|
|
1626
|
-
}
|
|
1627
|
-
const requestState = getWorkspaceRequestState();
|
|
1628
|
-
const abortController = new AbortController();
|
|
1629
|
-
workspaceListAbortRef.current = abortController;
|
|
1630
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
1631
|
-
try {
|
|
1632
|
-
const res = await fetchWithTimeout('/api/taskforce/workspaces', {
|
|
1633
|
-
method: 'GET',
|
|
1634
|
-
credentials: 'include'
|
|
1635
|
-
}, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
|
|
1636
|
-
signal: abortController.signal
|
|
1637
|
-
});
|
|
1638
|
-
if (isWorkspaceEpochStale(requestState)) {
|
|
1639
|
-
return { success: false, error: 'Workspace changed while loading workspaces.' };
|
|
1640
|
-
}
|
|
1641
|
-
if (res.status === 401) {
|
|
1642
|
-
setAvailableWorkspaces([]);
|
|
1643
|
-
workspaceListHydratedRef.current = false;
|
|
1644
|
-
return { success: false, error: 'Authentication required.' };
|
|
1645
|
-
}
|
|
1646
|
-
const data = await res.json().catch(() => ({}));
|
|
1647
|
-
if (isWorkspaceEpochStale(requestState)) {
|
|
1648
|
-
return { success: false, error: 'Workspace changed while loading workspaces.' };
|
|
1649
|
-
}
|
|
1650
|
-
if (!res.ok || data?.success === false) {
|
|
1651
|
-
setAvailableWorkspaces([]);
|
|
1652
|
-
workspaceListHydratedRef.current = false;
|
|
1653
|
-
return { success: false, error: data?.error || `Failed to load workspaces (${res.status})` };
|
|
1654
|
-
}
|
|
1655
|
-
const workspaces = Array.isArray(data?.workspaces) ? data.workspaces : [];
|
|
1656
|
-
setAvailableWorkspaces(workspaces);
|
|
1657
|
-
workspaceListHydratedRef.current = true;
|
|
1658
|
-
if (runtimeMode !== 'local' && typeof data?.currentWorkspaceId === 'string' && data.currentWorkspaceId.trim().length > 0) {
|
|
1659
|
-
applyResolvedWorkspaceId(data.currentWorkspaceId.trim(), { preserveNonDefaultDefault: true });
|
|
1660
|
-
}
|
|
1661
|
-
logBootstrapDebug('workspace_list_loaded', {
|
|
1662
|
-
workspaceCount: workspaces.length,
|
|
1663
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
1664
|
-
});
|
|
1665
|
-
return { success: true, workspaces };
|
|
1666
|
-
}
|
|
1667
|
-
catch (error) {
|
|
1668
|
-
if (isAbortError(error)) {
|
|
1669
|
-
return { success: false, error: 'Workspace list request aborted.' };
|
|
1670
|
-
}
|
|
1671
|
-
if (isBootstrapTimeoutError(error)) {
|
|
1672
|
-
markBootstrapStalled('Startup checks timed out.');
|
|
1673
|
-
}
|
|
1674
|
-
setAvailableWorkspaces([]);
|
|
1675
|
-
workspaceListHydratedRef.current = false;
|
|
1676
|
-
return { success: false, error: 'Failed to load workspaces.' };
|
|
1677
|
-
}
|
|
1678
|
-
finally {
|
|
1679
|
-
if (workspaceListAbortRef.current === abortController) {
|
|
1680
|
-
workspaceListAbortRef.current = null;
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
}, [
|
|
1684
|
-
applyResolvedWorkspaceId,
|
|
1685
|
-
fetchWithTimeout,
|
|
1686
|
-
getWorkspaceRequestState,
|
|
1687
|
-
isAbortError,
|
|
1688
|
-
isWorkspaceEpochStale,
|
|
1689
|
-
isBootstrapTimeoutError,
|
|
1690
|
-
isWorkspaceRequestStale,
|
|
1691
|
-
markBootstrapStalled,
|
|
1692
|
-
runtimeMode,
|
|
1693
|
-
workspaceSwitchingEnabled
|
|
1694
|
-
]);
|
|
1695
|
-
const fetchAssigneeOptions = useCallback(async () => {
|
|
1696
|
-
const requestState = getWorkspaceRequestState();
|
|
1697
|
-
const abortController = new AbortController();
|
|
1698
|
-
assigneeOptionsAbortRef.current = abortController;
|
|
1699
|
-
setAssigneeOptionsLoaded(false);
|
|
1700
|
-
try {
|
|
1701
|
-
const [assigneeResult, workspaceMembersResult] = await Promise.all([
|
|
1702
|
-
fetchWithTimeout('/api/taskforce/workspace/assignee-options', {
|
|
1703
|
-
method: 'GET',
|
|
1704
|
-
credentials: 'include'
|
|
1705
|
-
}, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
|
|
1706
|
-
signal: abortController.signal
|
|
1707
|
-
}),
|
|
1708
|
-
(authSessionResolved && isAuthenticated)
|
|
1709
|
-
? fetchWithTimeout('/api/taskforce/auth/workspace-members', {
|
|
1710
|
-
method: 'GET',
|
|
1711
|
-
credentials: 'include'
|
|
1712
|
-
}, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
|
|
1713
|
-
signal: abortController.signal
|
|
1714
|
-
}).catch(() => null)
|
|
1715
|
-
: Promise.resolve(null)
|
|
1716
|
-
]);
|
|
1717
|
-
if (isWorkspaceRequestStale(requestState)) {
|
|
1718
|
-
return { success: false, error: 'Workspace changed while loading assignee options.' };
|
|
1719
|
-
}
|
|
1720
|
-
if (assigneeResult.status === 401) {
|
|
1721
|
-
setAssigneeOptions(createDefaultAssigneeOptions());
|
|
1722
|
-
setAssigneeOptionsLoaded(true);
|
|
1723
|
-
return { success: false, error: 'Authentication required.' };
|
|
1724
|
-
}
|
|
1725
|
-
const data = await assigneeResult.json().catch(() => ({}));
|
|
1726
|
-
if (isWorkspaceRequestStale(requestState)) {
|
|
1727
|
-
return { success: false, error: 'Workspace changed while loading assignee options.' };
|
|
1728
|
-
}
|
|
1729
|
-
if (!assigneeResult.ok) {
|
|
1730
|
-
setAssigneeOptions(createDefaultAssigneeOptions());
|
|
1731
|
-
setAssigneeOptionsLoaded(true);
|
|
1732
|
-
return { success: false, error: data?.error || `Failed to load assignee options (${assigneeResult.status})` };
|
|
1733
|
-
}
|
|
1734
|
-
const assignees = Array.isArray(data?.assignees) ? data.assignees : [];
|
|
1735
|
-
const directOptions = assignees
|
|
1736
|
-
.map((entry) => {
|
|
1737
|
-
const value = String(entry?.value || '').trim();
|
|
1738
|
-
const kindRaw = String(entry?.kind || '').trim().toLowerCase();
|
|
1739
|
-
if (!value)
|
|
1740
|
-
return null;
|
|
1741
|
-
if (kindRaw === 'agent' || kindRaw === 'ai') {
|
|
1742
|
-
return {
|
|
1743
|
-
value,
|
|
1744
|
-
label: String(entry?.label || value).trim() || value,
|
|
1745
|
-
icon: String(entry?.icon || 'Bot'),
|
|
1746
|
-
color: String(entry?.color || '#8b5cf6'),
|
|
1747
|
-
kind: 'agent'
|
|
1748
|
-
};
|
|
1749
|
-
}
|
|
1750
|
-
if (kindRaw === 'member' || kindRaw === 'human') {
|
|
1751
|
-
return {
|
|
1752
|
-
value,
|
|
1753
|
-
label: String(entry?.label || value).trim() || value,
|
|
1754
|
-
icon: String(entry?.icon || 'User'),
|
|
1755
|
-
color: String(entry?.color || '#22c55e'),
|
|
1756
|
-
kind: 'member'
|
|
1757
|
-
};
|
|
1758
|
-
}
|
|
1759
|
-
return null;
|
|
1760
|
-
})
|
|
1761
|
-
.filter((candidate) => Boolean(candidate));
|
|
1762
|
-
const fallbackCandidates = assignees
|
|
1763
|
-
.map((candidate) => ({
|
|
1764
|
-
userId: String(candidate?.userId || '').trim(),
|
|
1765
|
-
email: String(candidate?.email || '').trim().toLowerCase(),
|
|
1766
|
-
displayName: typeof candidate?.displayName === 'string' ? candidate.displayName : null,
|
|
1767
|
-
status: typeof candidate?.status === 'string' ? candidate.status : null,
|
|
1768
|
-
disabled: candidate?.disabled === true,
|
|
1769
|
-
}))
|
|
1770
|
-
.filter((candidate) => candidate.userId
|
|
1771
|
-
&& String(candidate.status || 'active').trim().toLowerCase() === 'active'
|
|
1772
|
-
&& candidate.disabled !== true);
|
|
1773
|
-
let memberCandidates = fallbackCandidates;
|
|
1774
|
-
if (workspaceMembersResult?.ok) {
|
|
1775
|
-
const workspaceMembersData = await workspaceMembersResult.json().catch(() => ({}));
|
|
1776
|
-
const users = Array.isArray(workspaceMembersData?.users) ? workspaceMembersData.users : [];
|
|
1777
|
-
const normalizedAuthUserId = String(authUserId || '').trim();
|
|
1778
|
-
const currentMember = users.find((candidate) => String(candidate?.userId || '').trim() === normalizedAuthUserId);
|
|
1779
|
-
const currentMemberRole = String(currentMember?.role || '').trim().toLowerCase();
|
|
1780
|
-
if (currentMemberRole === 'owner' || currentMemberRole === 'admin' || currentMemberRole === 'member' || currentMemberRole === 'read-only') {
|
|
1781
|
-
setHydratedWorkspaceRole(currentMemberRole);
|
|
1782
|
-
}
|
|
1783
|
-
else if (!workspaceSwitchingEnabled && users.length === 0) {
|
|
1784
|
-
setHydratedWorkspaceRole(null);
|
|
1785
|
-
}
|
|
1786
|
-
const workspaceMemberCandidates = users
|
|
1787
|
-
.map((candidate) => ({
|
|
1788
|
-
userId: String(candidate?.userId || '').trim(),
|
|
1789
|
-
email: String(candidate?.email || '').trim().toLowerCase(),
|
|
1790
|
-
displayName: typeof candidate?.displayName === 'string' ? candidate.displayName : null,
|
|
1791
|
-
status: typeof candidate?.status === 'string' ? candidate.status : null,
|
|
1792
|
-
disabled: candidate?.disabled === true,
|
|
1793
|
-
}))
|
|
1794
|
-
.filter((candidate) => candidate.userId
|
|
1795
|
-
&& String(candidate.status || 'active').trim().toLowerCase() === 'active'
|
|
1796
|
-
&& candidate.disabled !== true)
|
|
1797
|
-
.map((candidate) => ({
|
|
1798
|
-
userId: candidate.userId,
|
|
1799
|
-
email: candidate.email,
|
|
1800
|
-
displayName: candidate.displayName ?? null
|
|
1801
|
-
}));
|
|
1802
|
-
memberCandidates = Array.from(new Map([...fallbackCandidates, ...workspaceMemberCandidates].map((candidate) => [
|
|
1803
|
-
String(candidate.userId || '').trim(),
|
|
1804
|
-
candidate
|
|
1805
|
-
])).values()).filter((candidate) => String(candidate.userId || '').trim().length > 0);
|
|
1806
|
-
}
|
|
1807
|
-
else if (!workspaceSwitchingEnabled && !fallbackCandidates.length) {
|
|
1808
|
-
setHydratedWorkspaceRole(null);
|
|
1809
|
-
}
|
|
1810
|
-
const mergedOptions = mergeAssigneeOptions([
|
|
1811
|
-
...directOptions,
|
|
1812
|
-
...createWorkspaceAssigneeOptions(memberCandidates).filter((option) => option.kind === 'member')
|
|
1813
|
-
]);
|
|
1814
|
-
if (isWorkspaceRequestStale(requestState)) {
|
|
1815
|
-
return { success: false, error: 'Workspace changed while loading assignee options.' };
|
|
1816
|
-
}
|
|
1817
|
-
setAssigneeOptions(mergedOptions);
|
|
1818
|
-
setAssigneeOptionsLoaded(true);
|
|
1819
|
-
return { success: true };
|
|
1820
|
-
}
|
|
1821
|
-
catch (error) {
|
|
1822
|
-
if (isAbortError(error)) {
|
|
1823
|
-
return { success: false, error: 'Assignee options request aborted.' };
|
|
1824
|
-
}
|
|
1825
|
-
if (!isWorkspaceRequestStale(requestState)) {
|
|
1826
|
-
setAssigneeOptions(createDefaultAssigneeOptions());
|
|
1827
|
-
setAssigneeOptionsLoaded(true);
|
|
1828
|
-
}
|
|
1829
|
-
return { success: false, error: 'Failed to load assignee options.' };
|
|
1830
|
-
}
|
|
1831
|
-
finally {
|
|
1832
|
-
if (assigneeOptionsAbortRef.current === abortController) {
|
|
1833
|
-
assigneeOptionsAbortRef.current = null;
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
}, [authSessionResolved, authUserId, fetchWithTimeout, getWorkspaceRequestState, isAbortError, isAuthenticated, isWorkspaceRequestStale, workspaceSwitchingEnabled]);
|
|
1837
1397
|
useEffect(() => {
|
|
1838
1398
|
if (!configLoaded || workspaceBootstrapPending)
|
|
1839
1399
|
return;
|
|
@@ -1855,162 +1415,44 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1855
1415
|
setHydratedWorkspaceRole(null);
|
|
1856
1416
|
}
|
|
1857
1417
|
}, [isAuthenticated]);
|
|
1858
|
-
const
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
return { success: false, error: '
|
|
1865
|
-
try {
|
|
1866
|
-
const res = await fetch('/api/taskforce/session/workspace', {
|
|
1867
|
-
method: 'POST',
|
|
1868
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1869
|
-
credentials: 'include',
|
|
1870
|
-
body: JSON.stringify({ workspaceId: normalizedWorkspaceId })
|
|
1871
|
-
});
|
|
1872
|
-
const data = await res.json().catch(() => ({}));
|
|
1873
|
-
if (!res.ok || data?.success === false) {
|
|
1874
|
-
return { success: false, error: data?.error || `Failed to switch workspace (${res.status})`, code: data?.code };
|
|
1875
|
-
}
|
|
1876
|
-
explicitWorkspaceSelectionRef.current = normalizedWorkspaceId;
|
|
1877
|
-
commitCurrentWorkspaceId(normalizedWorkspaceId, { clearExplicitSelection: false });
|
|
1878
|
-
abortWorkspaceScopedRequests();
|
|
1879
|
-
setWorkspaceBootstrapPending(true);
|
|
1880
|
-
markBootstrapPhase('workspace');
|
|
1881
|
-
const authenticated = await checkAuthSession({ force: true });
|
|
1882
|
-
if (options?.hydrate === false) {
|
|
1883
|
-
await fetchWorkspaces();
|
|
1884
|
-
}
|
|
1885
|
-
else {
|
|
1886
|
-
lastConfigRefreshKeyRef.current = `${runtimeMode}:${normalizedWorkspaceId}:${workspaceSelectionScope}`;
|
|
1887
|
-
lastAssigneeOptionsLoadKeyRef.current = `${runtimeMode}:${authenticated ? 'auth' : 'guest'}:${normalizedWorkspaceId}`;
|
|
1888
|
-
lastWorkspaceSyncStateLoadKeyRef.current = normalizedWorkspaceId;
|
|
1889
|
-
if (isOpen && activeTab === 'tasks') {
|
|
1890
|
-
lastTaskSurfaceLoadKeyRef.current = `${normalizedWorkspaceId}:${activeTab}:${showArchive ? 'archive' : 'active'}:${taskScope}`;
|
|
1891
|
-
}
|
|
1892
|
-
if (isOpen && activeTab === 'settings') {
|
|
1893
|
-
lastSettingsSurfaceLoadKeyRef.current = `${normalizedWorkspaceId}:${activeTab}:${settingsSection}`;
|
|
1894
|
-
}
|
|
1895
|
-
await Promise.all([
|
|
1896
|
-
fetchConfig(),
|
|
1897
|
-
fetchWorkspaces(),
|
|
1898
|
-
refreshTaskCollections({ isSilent: false }),
|
|
1899
|
-
fetchAssigneeOptions(),
|
|
1900
|
-
loadWorkspaceSyncState()
|
|
1901
|
-
]);
|
|
1902
|
-
}
|
|
1903
|
-
if (explicitWorkspaceSelectionRef.current === normalizedWorkspaceId) {
|
|
1904
|
-
explicitWorkspaceSelectionRef.current = null;
|
|
1905
|
-
}
|
|
1906
|
-
return { success: true };
|
|
1907
|
-
}
|
|
1908
|
-
catch {
|
|
1909
|
-
if (explicitWorkspaceSelectionRef.current === normalizedWorkspaceId) {
|
|
1910
|
-
explicitWorkspaceSelectionRef.current = null;
|
|
1911
|
-
}
|
|
1912
|
-
return { success: false, error: 'Failed to switch workspace.' };
|
|
1913
|
-
}
|
|
1914
|
-
finally {
|
|
1915
|
-
setWorkspaceBootstrapPending(false);
|
|
1418
|
+
const continueAfterCommercialOnboarding = useCallback(async () => {
|
|
1419
|
+
setBootstrapError(null);
|
|
1420
|
+
markBootstrapPhase('auth');
|
|
1421
|
+
const authenticated = await checkAuthSession({ force: true });
|
|
1422
|
+
if (!authenticated) {
|
|
1423
|
+
await fetchConfig({ ignoreAuthGuard: true });
|
|
1424
|
+
return { success: false, destination: 'login', error: 'Sign in required.', code: 'AUTH_REQUIRED' };
|
|
1916
1425
|
}
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
1922
|
-
try {
|
|
1923
|
-
if (!workspaceSwitchingEnabled) {
|
|
1924
|
-
const localWorkspaceId = String(currentWorkspaceId || '').trim() || 'default';
|
|
1925
|
-
commitCurrentWorkspaceId(localWorkspaceId);
|
|
1926
|
-
logBootstrapDebug('workspace_resolved', {
|
|
1927
|
-
workspaceId: localWorkspaceId,
|
|
1928
|
-
workspaceSetupRequired: false,
|
|
1929
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
1930
|
-
});
|
|
1931
|
-
return { success: true, workspaceSetupRequired: false, workspaceId: localWorkspaceId };
|
|
1932
|
-
}
|
|
1933
|
-
const preferredWorkspaceId = String(options?.preferredWorkspaceId || '').trim();
|
|
1934
|
-
const persistedWorkspaceId = readPersistedWorkspaceId(workspaceSelectionScope);
|
|
1935
|
-
const workspacesResult = await fetchWorkspaces();
|
|
1936
|
-
if (!workspacesResult.success) {
|
|
1426
|
+
if (workspaceSwitchingEnabled) {
|
|
1427
|
+
const workspaceResolution = await resolveWorkspaceAfterAuth();
|
|
1428
|
+
if (!workspaceResolution.success) {
|
|
1429
|
+
await fetchConfig({ ignoreAuthGuard: true });
|
|
1937
1430
|
return {
|
|
1938
1431
|
success: false,
|
|
1939
|
-
|
|
1940
|
-
error:
|
|
1432
|
+
destination: 'app',
|
|
1433
|
+
error: workspaceResolution.error || 'Unable to resolve workspace after plan selection.',
|
|
1434
|
+
code: workspaceResolution.code
|
|
1941
1435
|
};
|
|
1942
1436
|
}
|
|
1943
|
-
|
|
1944
|
-
if (workspaces.length === 0) {
|
|
1945
|
-
commitCurrentWorkspaceId('default');
|
|
1946
|
-
logBootstrapDebug('workspace_resolved', {
|
|
1947
|
-
workspaceId: 'default',
|
|
1948
|
-
workspaceSetupRequired: true,
|
|
1949
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
1950
|
-
});
|
|
1951
|
-
return { success: true, workspaceSetupRequired: true };
|
|
1952
|
-
}
|
|
1953
|
-
const ids = new Set(workspaces.map((workspace) => String(workspace.id || '').trim()).filter(Boolean));
|
|
1954
|
-
const selectedWorkspaceId = ((preferredWorkspaceId && ids.has(preferredWorkspaceId) ? preferredWorkspaceId : '')
|
|
1955
|
-
|| (persistedWorkspaceId && ids.has(persistedWorkspaceId) ? persistedWorkspaceId : '')
|
|
1956
|
-
|| String(workspaces[0]?.id || '').trim());
|
|
1957
|
-
if (!selectedWorkspaceId) {
|
|
1958
|
-
commitCurrentWorkspaceId('default');
|
|
1959
|
-
return { success: true, workspaceSetupRequired: true };
|
|
1960
|
-
}
|
|
1961
|
-
const current = String(currentWorkspaceIdRef.current || '').trim();
|
|
1962
|
-
if (current !== selectedWorkspaceId) {
|
|
1963
|
-
const switched = await switchWorkspace(selectedWorkspaceId, { hydrate: false });
|
|
1964
|
-
if (!switched.success) {
|
|
1965
|
-
return {
|
|
1966
|
-
success: false,
|
|
1967
|
-
workspaceSetupRequired: false,
|
|
1968
|
-
error: switched.error || 'Failed to set workspace after authentication.',
|
|
1969
|
-
code: switched.code
|
|
1970
|
-
};
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
else {
|
|
1974
|
-
commitCurrentWorkspaceId(selectedWorkspaceId);
|
|
1975
|
-
}
|
|
1976
|
-
logBootstrapDebug('workspace_resolved', {
|
|
1977
|
-
workspaceId: selectedWorkspaceId,
|
|
1978
|
-
workspaceSetupRequired: false,
|
|
1979
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
1980
|
-
});
|
|
1981
|
-
return { success: true, workspaceSetupRequired: false, workspaceId: selectedWorkspaceId };
|
|
1982
|
-
}
|
|
1983
|
-
catch {
|
|
1984
|
-
markBootstrapStalled('Unable to resolve workspace access.');
|
|
1985
|
-
return { success: false, workspaceSetupRequired: false, error: 'Failed to resolve workspace access.' };
|
|
1986
|
-
}
|
|
1987
|
-
finally {
|
|
1988
|
-
setWorkspaceBootstrapPending(false);
|
|
1989
|
-
}
|
|
1990
|
-
}, [
|
|
1991
|
-
commitCurrentWorkspaceId,
|
|
1992
|
-
fetchWorkspaces,
|
|
1993
|
-
markBootstrapPhase,
|
|
1994
|
-
markBootstrapStalled,
|
|
1995
|
-
workspaceSelectionScope,
|
|
1996
|
-
switchWorkspace,
|
|
1997
|
-
workspaceSwitchingEnabled
|
|
1998
|
-
]);
|
|
1999
|
-
const retryBootstrapChecks = useCallback(async () => {
|
|
2000
|
-
setBootstrapError(null);
|
|
2001
|
-
markBootstrapPhase('auth');
|
|
2002
|
-
const authenticated = await checkAuthSession();
|
|
2003
|
-
if (workspaceSwitchingEnabled && authenticated) {
|
|
2004
|
-
const workspaceResolution = await resolveWorkspaceAfterAuth();
|
|
2005
|
-
if (!workspaceResolution.success || workspaceResolution.workspaceSetupRequired) {
|
|
1437
|
+
if (workspaceResolution.workspaceSetupRequired) {
|
|
2006
1438
|
await fetchConfig({ ignoreAuthGuard: true });
|
|
2007
|
-
return;
|
|
1439
|
+
return { success: true, destination: 'setup' };
|
|
2008
1440
|
}
|
|
2009
1441
|
}
|
|
2010
|
-
await
|
|
1442
|
+
await fetchBootstrapConfig();
|
|
1443
|
+
await Promise.all([
|
|
1444
|
+
fetchTasks(true, { ignoreAuthGuard: true }),
|
|
1445
|
+
fetchReferenceData(),
|
|
1446
|
+
fetchPlanningEntities()
|
|
1447
|
+
]);
|
|
1448
|
+
return { success: true, destination: 'app' };
|
|
2011
1449
|
}, [
|
|
2012
1450
|
checkAuthSession,
|
|
1451
|
+
fetchBootstrapConfig,
|
|
2013
1452
|
fetchConfig,
|
|
1453
|
+
fetchPlanningEntities,
|
|
1454
|
+
fetchReferenceData,
|
|
1455
|
+
fetchTasks,
|
|
2014
1456
|
markBootstrapPhase,
|
|
2015
1457
|
resolveWorkspaceAfterAuth,
|
|
2016
1458
|
workspaceSwitchingEnabled
|
|
@@ -2066,7 +1508,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2066
1508
|
if (!res.ok || data?.success === false) {
|
|
2067
1509
|
return { success: false, error: data?.error || `Failed to delete workspace (${res.status})`, code: data?.code };
|
|
2068
1510
|
}
|
|
2069
|
-
await checkAuthSession();
|
|
1511
|
+
await checkAuthSession({ force: true });
|
|
2070
1512
|
await Promise.all([fetchConfig(), fetchWorkspaces()]);
|
|
2071
1513
|
const nextWorkspaceId = typeof data?.nextWorkspaceId === 'string' ? data.nextWorkspaceId.trim() : '';
|
|
2072
1514
|
if (nextWorkspaceId) {
|
|
@@ -2104,6 +1546,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2104
1546
|
}, [refreshSetupContext]);
|
|
2105
1547
|
const saveWorkspaceProfile = useCallback(async (input) => {
|
|
2106
1548
|
const name = String(input?.name || '').trim();
|
|
1549
|
+
const requestedWorkspaceId = typeof input?.workspaceId === 'string'
|
|
1550
|
+
? input.workspaceId.trim()
|
|
1551
|
+
: '';
|
|
2107
1552
|
if (!name)
|
|
2108
1553
|
return { success: false, error: 'Workspace name is required.', code: 'WORKSPACE_NAME_REQUIRED' };
|
|
2109
1554
|
try {
|
|
@@ -2115,7 +1560,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2115
1560
|
},
|
|
2116
1561
|
credentials: 'include',
|
|
2117
1562
|
body: JSON.stringify({
|
|
2118
|
-
workspaceId:
|
|
1563
|
+
workspaceId: requestedWorkspaceId && requestedWorkspaceId !== 'default'
|
|
1564
|
+
? requestedWorkspaceId
|
|
1565
|
+
: undefined,
|
|
2119
1566
|
name,
|
|
2120
1567
|
description: typeof input.description === 'string' ? input.description : undefined,
|
|
2121
1568
|
allowCreate: true
|
|
@@ -2168,7 +1615,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2168
1615
|
return { success: false, error: data?.error || 'Sign in failed.', code: data?.code };
|
|
2169
1616
|
}
|
|
2170
1617
|
setError('');
|
|
2171
|
-
await checkAuthSession();
|
|
1618
|
+
await checkAuthSession({ force: true });
|
|
2172
1619
|
const workspaceResolution = await resolveWorkspaceAfterAuth();
|
|
2173
1620
|
if (!workspaceResolution.success) {
|
|
2174
1621
|
return {
|
|
@@ -2178,6 +1625,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2178
1625
|
};
|
|
2179
1626
|
}
|
|
2180
1627
|
if (workspaceResolution.workspaceSetupRequired) {
|
|
1628
|
+
await fetchConfig({ ignoreAuthGuard: true });
|
|
2181
1629
|
return { success: true, workspaceSetupRequired: true };
|
|
2182
1630
|
}
|
|
2183
1631
|
await Promise.all([
|
|
@@ -2224,12 +1672,64 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2224
1672
|
});
|
|
2225
1673
|
const data = await res.json().catch(() => ({}));
|
|
2226
1674
|
if (!res.ok || !data?.success) {
|
|
2227
|
-
return {
|
|
1675
|
+
return {
|
|
1676
|
+
success: false,
|
|
1677
|
+
error: data?.error || 'Create account failed.',
|
|
1678
|
+
code: data?.code,
|
|
1679
|
+
verificationRequired: Boolean(data?.verificationRequired),
|
|
1680
|
+
verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
|
|
1681
|
+
emailSent: data?.emailSent !== false,
|
|
1682
|
+
emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
const planSelectionRequired = data?.planSelectionRequired === true;
|
|
1686
|
+
const checkoutPending = data?.checkoutPending === true;
|
|
1687
|
+
const commercialState = typeof data?.commercialState === 'string' ? data.commercialState : null;
|
|
1688
|
+
const verificationRequired = Boolean(data?.verificationRequired);
|
|
1689
|
+
if (!verificationRequired) {
|
|
1690
|
+
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
|
+
const hasCommercialOnboardingGate = Boolean(planSelectionRequired
|
|
1701
|
+
|| checkoutPending
|
|
1702
|
+
|| commercialState === 'pending_plan_selection'
|
|
1703
|
+
|| commercialState === 'checkout_pending');
|
|
1704
|
+
if (workspaceResolution.workspaceSetupRequired || hasCommercialOnboardingGate) {
|
|
1705
|
+
await fetchConfig({ ignoreAuthGuard: true });
|
|
1706
|
+
}
|
|
1707
|
+
else {
|
|
1708
|
+
await Promise.all([
|
|
1709
|
+
fetchConfig({ ignoreAuthGuard: true }),
|
|
1710
|
+
fetchTasks(true, { ignoreAuthGuard: true })
|
|
1711
|
+
]);
|
|
1712
|
+
void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
|
|
1713
|
+
}
|
|
1714
|
+
return {
|
|
1715
|
+
success: true,
|
|
1716
|
+
workspaceSetupRequired: workspaceResolution.workspaceSetupRequired,
|
|
1717
|
+
planSelectionRequired,
|
|
1718
|
+
checkoutPending,
|
|
1719
|
+
commercialState,
|
|
1720
|
+
verificationRequired,
|
|
1721
|
+
verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
|
|
1722
|
+
emailSent: data?.emailSent !== false,
|
|
1723
|
+
emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
|
|
1724
|
+
};
|
|
2228
1725
|
}
|
|
2229
1726
|
return {
|
|
2230
1727
|
success: true,
|
|
2231
1728
|
workspaceSetupRequired: Boolean(data?.workspaceSetupRequired),
|
|
2232
|
-
|
|
1729
|
+
planSelectionRequired,
|
|
1730
|
+
checkoutPending,
|
|
1731
|
+
commercialState,
|
|
1732
|
+
verificationRequired,
|
|
2233
1733
|
verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
|
|
2234
1734
|
emailSent: data?.emailSent !== false,
|
|
2235
1735
|
emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
|
|
@@ -2238,7 +1738,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2238
1738
|
catch {
|
|
2239
1739
|
return { success: false, error: 'Create account failed.' };
|
|
2240
1740
|
}
|
|
2241
|
-
}, [resolveCloudAuthUrl, cloudAuthConfigured]);
|
|
1741
|
+
}, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
|
|
2242
1742
|
const updateCurrentUserProfile = useCallback(async (input) => {
|
|
2243
1743
|
if (!cloudAuthConfigured) {
|
|
2244
1744
|
return { success: false, error: 'Cloud auth endpoint is not configured.' };
|
|
@@ -2443,7 +1943,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2443
1943
|
return { success: false, error: data?.error || 'Invite acceptance failed.', code: data?.code };
|
|
2444
1944
|
}
|
|
2445
1945
|
setError('');
|
|
2446
|
-
await checkAuthSession();
|
|
1946
|
+
await checkAuthSession({ force: true });
|
|
2447
1947
|
const preferredWorkspaceId = typeof data?.workspaceId === 'string' ? data.workspaceId : null;
|
|
2448
1948
|
const workspaceResolution = await resolveWorkspaceAfterAuth({ preferredWorkspaceId });
|
|
2449
1949
|
if (!workspaceResolution.success) {
|
|
@@ -2511,6 +2011,34 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2511
2011
|
return { success: false, error: 'Invite join failed.' };
|
|
2512
2012
|
}
|
|
2513
2013
|
}, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
|
|
2014
|
+
const resetCloudLogoutClientState = useCallback(() => {
|
|
2015
|
+
abortWorkspaceScopedRequests();
|
|
2016
|
+
referenceDataRequestRef.current = null;
|
|
2017
|
+
lastLoadedUiStateKeyRef.current = '';
|
|
2018
|
+
lastAssigneeOptionsLoadKeyRef.current = '';
|
|
2019
|
+
lastConfigRefreshKeyRef.current = '';
|
|
2020
|
+
lastWorkspaceSyncStateLoadKeyRef.current = '';
|
|
2021
|
+
lastTaskSurfaceLoadKeyRef.current = '';
|
|
2022
|
+
lastSettingsSurfaceLoadKeyRef.current = '';
|
|
2023
|
+
setUiStateReady(false);
|
|
2024
|
+
setReferenceDataLoaded(false);
|
|
2025
|
+
setAssigneeOptions(createDefaultAssigneeOptions());
|
|
2026
|
+
setAssigneeOptionsLoaded(false);
|
|
2027
|
+
setDeletedTasks([]);
|
|
2028
|
+
setTasks([]);
|
|
2029
|
+
setArchivedTasks([]);
|
|
2030
|
+
setInitiatives([]);
|
|
2031
|
+
setWorkstreams([]);
|
|
2032
|
+
setLoadingTasks(false);
|
|
2033
|
+
if (cloudRealtimeFollowUpRefreshTimerRef.current !== null && typeof window !== 'undefined') {
|
|
2034
|
+
window.clearTimeout(cloudRealtimeFollowUpRefreshTimerRef.current);
|
|
2035
|
+
cloudRealtimeFollowUpRefreshTimerRef.current = null;
|
|
2036
|
+
}
|
|
2037
|
+
if (archiveBootstrapTimerRef.current !== null && typeof window !== 'undefined') {
|
|
2038
|
+
window.clearTimeout(archiveBootstrapTimerRef.current);
|
|
2039
|
+
archiveBootstrapTimerRef.current = null;
|
|
2040
|
+
}
|
|
2041
|
+
}, [abortWorkspaceScopedRequests, setArchivedTasks, setTasks]);
|
|
2514
2042
|
const logout = useCallback(async () => {
|
|
2515
2043
|
try {
|
|
2516
2044
|
if (cloudAuthConfigured) {
|
|
@@ -2524,6 +2052,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2524
2052
|
// no-op
|
|
2525
2053
|
}
|
|
2526
2054
|
finally {
|
|
2055
|
+
abortWorkspaceScopedRequests();
|
|
2056
|
+
authSessionEpochRef.current += 1;
|
|
2527
2057
|
authSessionRequestRef.current = null;
|
|
2528
2058
|
authSessionLastResultRef.current = false;
|
|
2529
2059
|
authSessionLastCheckedAtRef.current = Date.now();
|
|
@@ -2538,19 +2068,18 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2538
2068
|
setUserGlobalSyncStatus('disconnected');
|
|
2539
2069
|
setUserGlobalSyncError(null);
|
|
2540
2070
|
setHasBetaAccess(true);
|
|
2071
|
+
setProjectName('');
|
|
2072
|
+
setConfigLoaded(false);
|
|
2073
|
+
setReferenceDataLoaded(false);
|
|
2074
|
+
setSetupState(null);
|
|
2075
|
+
setBootstrapPhase('idle');
|
|
2076
|
+
setBootstrapError(null);
|
|
2077
|
+
hasLoadedUiStateRef.current = false;
|
|
2078
|
+
if (runtimeMode !== 'local') {
|
|
2079
|
+
clearPersistedWorkspaceId(workspaceSelectionScope);
|
|
2080
|
+
}
|
|
2541
2081
|
setAuthBlocked(runtimeMode === 'cloud' && authRequiredForApi);
|
|
2542
|
-
|
|
2543
|
-
const existing = String(prev || '').trim();
|
|
2544
|
-
let resolvedWorkspaceId = 'default';
|
|
2545
|
-
if (existing && existing.toLowerCase() !== 'default') {
|
|
2546
|
-
resolvedWorkspaceId = existing;
|
|
2547
|
-
}
|
|
2548
|
-
else if (runtimeMode !== 'local') {
|
|
2549
|
-
resolvedWorkspaceId = readPersistedWorkspaceId(workspaceSelectionScope) || 'default';
|
|
2550
|
-
}
|
|
2551
|
-
currentWorkspaceIdRef.current = resolvedWorkspaceId;
|
|
2552
|
-
return resolvedWorkspaceId;
|
|
2553
|
-
});
|
|
2082
|
+
commitCurrentWorkspaceId('default');
|
|
2554
2083
|
setAvailableWorkspaces([]);
|
|
2555
2084
|
workspaceListHydratedRef.current = false;
|
|
2556
2085
|
setHydratedWorkspaceRole(null);
|
|
@@ -2563,12 +2092,14 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2563
2092
|
]);
|
|
2564
2093
|
return;
|
|
2565
2094
|
}
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
setWorkstreams([]);
|
|
2095
|
+
hasBootstrappedDataRef.current = false;
|
|
2096
|
+
resetCloudLogoutClientState();
|
|
2569
2097
|
}
|
|
2570
2098
|
}, [
|
|
2099
|
+
abortWorkspaceScopedRequests,
|
|
2571
2100
|
authRequiredForApi,
|
|
2101
|
+
clearPersistedWorkspaceId,
|
|
2102
|
+
commitCurrentWorkspaceId,
|
|
2572
2103
|
resolveCloudAuthUrl,
|
|
2573
2104
|
cloudAuthConfigured,
|
|
2574
2105
|
workspaceSelectionScope,
|
|
@@ -2576,7 +2107,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2576
2107
|
fetchConfig,
|
|
2577
2108
|
fetchPlanningEntities,
|
|
2578
2109
|
fetchReferenceData,
|
|
2579
|
-
fetchTasks
|
|
2110
|
+
fetchTasks,
|
|
2111
|
+
resetCloudLogoutClientState
|
|
2580
2112
|
]);
|
|
2581
2113
|
const createInitiativeFromTemplate = useCallback(async (payload) => {
|
|
2582
2114
|
try {
|
|
@@ -2694,7 +2226,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2694
2226
|
}, [fetchPlanningEntities]);
|
|
2695
2227
|
// Initialize auth once, then bootstrap protected data once auth gates are settled.
|
|
2696
2228
|
const hasInitializedAuthRef = useRef(false);
|
|
2697
|
-
const hasBootstrappedDataRef = useRef(false);
|
|
2698
2229
|
useEffect(() => {
|
|
2699
2230
|
setIsOpen(true); // Always open in standalone-first
|
|
2700
2231
|
if (hasInitializedAuthRef.current)
|
|
@@ -2953,73 +2484,19 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2953
2484
|
userId: normalizedRealtimeUserId || undefined
|
|
2954
2485
|
});
|
|
2955
2486
|
// Fallback sync for cross-process writes (e.g., MCP tools) that may miss watcher/websocket events.
|
|
2956
|
-
|
|
2957
|
-
if (typeof window === 'undefined' || typeof document === 'undefined')
|
|
2958
|
-
return;
|
|
2959
|
-
if (shouldSkipDataVersionPolling({
|
|
2960
|
-
isOpen,
|
|
2961
|
-
authBlocked,
|
|
2962
|
-
authRequiredForApi,
|
|
2963
|
-
isAuthenticated,
|
|
2964
|
-
canCallProtectedApi: authGuardPolicy.canCallProtectedApi
|
|
2965
|
-
}))
|
|
2966
|
-
return;
|
|
2967
|
-
let cancelled = false;
|
|
2968
|
-
const checkDataVersion = async () => {
|
|
2969
|
-
if (cancelled || document.visibilityState !== 'visible')
|
|
2970
|
-
return;
|
|
2971
|
-
if (shouldSkipDataVersionTick({
|
|
2972
|
-
shouldGateProtectedApiCalls,
|
|
2973
|
-
authSessionResolved: authSessionResolvedRef.current,
|
|
2974
|
-
authRequiredForApi: authRequiredForApiRef.current,
|
|
2975
|
-
isAuthenticated: isAuthenticatedRef.current
|
|
2976
|
-
}))
|
|
2977
|
-
return;
|
|
2978
|
-
try {
|
|
2979
|
-
const res = await fetch('/api/taskforce/data-version');
|
|
2980
|
-
if (!res.ok)
|
|
2981
|
-
return;
|
|
2982
|
-
const data = await res.json();
|
|
2983
|
-
const version = Number(data?.dataVersion);
|
|
2984
|
-
if (!Number.isFinite(version))
|
|
2985
|
-
return;
|
|
2986
|
-
if (dataVersionRef.current === null) {
|
|
2987
|
-
dataVersionRef.current = version;
|
|
2988
|
-
return;
|
|
2989
|
-
}
|
|
2990
|
-
if (version !== dataVersionRef.current) {
|
|
2991
|
-
dataVersionRef.current = version;
|
|
2992
|
-
await refreshTaskCollectionsForDataVersion();
|
|
2993
|
-
}
|
|
2994
|
-
}
|
|
2995
|
-
catch {
|
|
2996
|
-
// Keep polling best-effort and silent.
|
|
2997
|
-
}
|
|
2998
|
-
};
|
|
2999
|
-
const onVisibilityChange = () => {
|
|
3000
|
-
if (document.visibilityState === 'visible') {
|
|
3001
|
-
checkDataVersion();
|
|
3002
|
-
}
|
|
3003
|
-
};
|
|
3004
|
-
const intervalId = window.setInterval(checkDataVersion, 3000);
|
|
3005
|
-
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
3006
|
-
checkDataVersion();
|
|
3007
|
-
return () => {
|
|
3008
|
-
cancelled = true;
|
|
3009
|
-
window.clearInterval(intervalId);
|
|
3010
|
-
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
3011
|
-
};
|
|
3012
|
-
}, [
|
|
2487
|
+
useDataVersionRefresh({
|
|
3013
2488
|
isOpen,
|
|
3014
|
-
fetchTasks,
|
|
3015
|
-
fetchArchive,
|
|
3016
2489
|
authBlocked,
|
|
3017
2490
|
authRequiredForApi,
|
|
3018
2491
|
isAuthenticated,
|
|
3019
|
-
authGuardPolicy,
|
|
2492
|
+
canCallProtectedApi: authGuardPolicy.canCallProtectedApi,
|
|
3020
2493
|
shouldGateProtectedApiCalls,
|
|
2494
|
+
authSessionResolvedRef,
|
|
2495
|
+
authRequiredForApiRef,
|
|
2496
|
+
isAuthenticatedRef,
|
|
2497
|
+
dataVersionRef,
|
|
3021
2498
|
refreshTaskCollectionsForDataVersion
|
|
3022
|
-
|
|
2499
|
+
});
|
|
3023
2500
|
const relationshipTasks = useMemo(() => {
|
|
3024
2501
|
const byId = new Map();
|
|
3025
2502
|
for (const task of archivedTasks)
|
|
@@ -3543,6 +3020,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
3543
3020
|
runtimeCapabilities,
|
|
3544
3021
|
refreshSetupContext,
|
|
3545
3022
|
retryBootstrapChecks,
|
|
3023
|
+
continueAfterCommercialOnboarding,
|
|
3546
3024
|
saveWorkspaceProfile,
|
|
3547
3025
|
fetchWorkspaces,
|
|
3548
3026
|
createWorkspace,
|