@taskforcehq/taskforce 0.3.303 → 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 +102 -5
- package/dist/TaskforceCore.js +514 -212
- package/dist/TaskforceCore.test.js +1172 -133
- 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/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 +5 -1
- package/dist/components/views/PlansPage.js +39 -12
- package/dist/components/views/PlansPage.test.js +59 -2
- package/dist/components/views/StandaloneLayout.js +113 -38
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/PlanEntitlementService.d.ts +3 -1
- package/dist/core/PlanEntitlementService.js +61 -11
- package/dist/core/SignupMonetizationSettings.test.js +37 -2
- 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 +15 -11
- package/dist/core/Taskforce.js +43 -2
- package/dist/core/WorkspacePlanMode.test.js +65 -0
- 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 +314 -873
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +568 -3
- package/dist/hooks/useTaskMutations.js +45 -6
- package/dist/hooks/useTaskMutations.test.js +147 -0
- package/dist/hooks/useTaskforce.d.ts +8 -2
- package/dist/hooks/useTaskforce.js +285 -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 +174 -476
- package/dist/mcp/runtime.test.js +81 -8
- 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/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +46 -3
- package/dist/server/index.test.js +12 -1
- package/dist/server/routes/admin.js +13 -19
- package/dist/server/routes/auth.js +37 -2
- package/dist/server/routes/billing.js +18 -67
- package/dist/server/routes/billing.test.js +43 -0
- package/dist/server/routes/shared.d.ts +30 -0
- package/dist/server/routes/shared.js +26 -0
- package/dist/server/routes/sync.integration.test.js +20 -0
- package/dist/server/routes/sync.js +110 -237
- package/dist/server/routes/tasks.js +49 -7
- package/dist/server/routes.js +8 -19
- package/dist/server/routes.test.js +243 -5
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +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.js +12 -10
- 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 +8 -1
- package/dist/sync/syncService.js +36 -2
- package/dist/sync/syncService.test.js +48 -0
- package/dist/sync/workspacePullFeed.d.ts +1 -0
- package/dist/sync/workspacePullFeed.js +66 -59
- package/dist/sync/workspacePullFeed.test.js +72 -4
- 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-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-C3GyzrWm.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.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-CGk9s3NJ.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-pWocEipT.js → vendor-icons-I7ZwG1z_.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +3 -1
- package/scripts/playwright-auth.sh +4 -0
- package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
- package/dist/ui/assets/index-CLIMgR6g.js +0 -6
- package/dist/ui/assets/index-DneETxcu.css +0 -1
|
@@ -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);
|
|
@@ -345,6 +347,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
345
347
|
const [bootstrapError, setBootstrapError] = useState(null);
|
|
346
348
|
const [bootstrapStartedAt, setBootstrapStartedAt] = useState(null);
|
|
347
349
|
const authSessionRequestRef = useRef(null);
|
|
350
|
+
const authSessionEpochRef = useRef(0);
|
|
348
351
|
const authSessionLastCheckedAtRef = useRef(0);
|
|
349
352
|
const authSessionLastResultRef = useRef(hasOptimisticLocalAuth);
|
|
350
353
|
const currentWorkspaceIdRef = useRef(currentWorkspaceId);
|
|
@@ -375,25 +378,16 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
375
378
|
useEffect(() => {
|
|
376
379
|
currentWorkspaceIdRef.current = currentWorkspaceId;
|
|
377
380
|
}, [currentWorkspaceId]);
|
|
378
|
-
const applyResolvedWorkspaceId = useCallback((nextWorkspaceIdRaw
|
|
381
|
+
const applyResolvedWorkspaceId = useCallback((nextWorkspaceIdRaw) => {
|
|
379
382
|
const nextWorkspaceId = String(nextWorkspaceIdRaw || '').trim();
|
|
380
383
|
if (!nextWorkspaceId)
|
|
381
384
|
return;
|
|
382
|
-
setCurrentWorkspaceId((
|
|
385
|
+
setCurrentWorkspaceId(() => {
|
|
383
386
|
const explicitWorkspaceId = String(explicitWorkspaceSelectionRef.current || '').trim();
|
|
384
387
|
let resolvedWorkspaceId = nextWorkspaceId;
|
|
385
388
|
if (explicitWorkspaceId && explicitWorkspaceId !== nextWorkspaceId) {
|
|
386
389
|
resolvedWorkspaceId = explicitWorkspaceId;
|
|
387
390
|
}
|
|
388
|
-
else {
|
|
389
|
-
const prevValue = String(prev || '').trim();
|
|
390
|
-
if (options?.preserveNonDefaultDefault
|
|
391
|
-
&& nextWorkspaceId === 'default'
|
|
392
|
-
&& prevValue
|
|
393
|
-
&& prevValue.toLowerCase() !== 'default') {
|
|
394
|
-
resolvedWorkspaceId = prevValue;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
391
|
currentWorkspaceIdRef.current = resolvedWorkspaceId;
|
|
398
392
|
return resolvedWorkspaceId;
|
|
399
393
|
});
|
|
@@ -477,7 +471,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
477
471
|
shouldBlockProtectedApiCalls
|
|
478
472
|
});
|
|
479
473
|
const { zenMode, setZenModeState, toggleZenMode } = useZenMode();
|
|
480
|
-
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
|
+
});
|
|
481
479
|
// Unsaved Changes Modal State
|
|
482
480
|
const [unsavedModalOpen, setUnsavedModalOpen] = useState(false);
|
|
483
481
|
const [pendingNavigation, setPendingNavigation] = useState(null);
|
|
@@ -813,158 +811,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
813
811
|
await fetchDeletedTasks(true);
|
|
814
812
|
await fetchPlanningEntities();
|
|
815
813
|
}, [fetchDeletedTasks, fetchPlanningEntities, refreshTaskCollectionsFromInvalidationBase]);
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
// Keep prior auth identity during bootstrap until runtime auth config is resolved.
|
|
821
|
-
// This avoids transient "signed out" UI flicker on refresh.
|
|
822
|
-
return authSessionLastResultRef.current;
|
|
823
|
-
}
|
|
824
|
-
if (!shouldProbeCloudAuth) {
|
|
825
|
-
setRuntimeMode('local');
|
|
826
|
-
setAuthRequiredForApi(false);
|
|
827
|
-
setIsAuthenticated(false);
|
|
828
|
-
setAuthUserId('anonymous');
|
|
829
|
-
setAuthWorkspaceId('');
|
|
830
|
-
persistLocalMutationActorUserId(null);
|
|
831
|
-
setAuthUserEmail('');
|
|
832
|
-
setAuthUserDisplayName('');
|
|
833
|
-
setAuthUserAvatarUrl('');
|
|
834
|
-
setUserGlobalSyncStatus('disconnected');
|
|
835
|
-
setUserGlobalSyncError(null);
|
|
836
|
-
setHasBetaAccess(true);
|
|
837
|
-
setAvailableWorkspaces([]);
|
|
838
|
-
setHydratedWorkspaceRole(null);
|
|
839
|
-
setAssigneeOptions(createDefaultAssigneeOptions());
|
|
840
|
-
setAuthBlocked(false);
|
|
841
|
-
setAuthSessionResolved(true);
|
|
842
|
-
authSessionLastResultRef.current = false;
|
|
843
|
-
authSessionLastCheckedAtRef.current = Date.now();
|
|
844
|
-
return false;
|
|
845
|
-
}
|
|
846
|
-
const sessionUrl = resolveCloudAuthUrl('/api/taskforce/auth/session');
|
|
847
|
-
const now = Date.now();
|
|
848
|
-
if (!force && authSessionRequestRef.current)
|
|
849
|
-
return authSessionRequestRef.current;
|
|
850
|
-
if (!force && authSessionResolved && (now - authSessionLastCheckedAtRef.current) < 1500) {
|
|
851
|
-
return authSessionLastResultRef.current;
|
|
852
|
-
}
|
|
853
|
-
const request = (async () => {
|
|
854
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
855
|
-
const abortController = new AbortController();
|
|
856
|
-
const timeoutId = typeof window !== 'undefined'
|
|
857
|
-
? window.setTimeout(() => abortController.abort(), 8000)
|
|
858
|
-
: null;
|
|
859
|
-
try {
|
|
860
|
-
const res = await fetch(sessionUrl, {
|
|
861
|
-
method: 'GET',
|
|
862
|
-
credentials: 'include',
|
|
863
|
-
mode: 'cors',
|
|
864
|
-
signal: abortController.signal
|
|
865
|
-
});
|
|
866
|
-
if (res.status === 429) {
|
|
867
|
-
setAuthSessionResolved(true);
|
|
868
|
-
return authSessionLastResultRef.current;
|
|
869
|
-
}
|
|
870
|
-
if (!res.ok) {
|
|
871
|
-
if (isLoopbackHost)
|
|
872
|
-
persistLocalMutationActorUserId(null);
|
|
873
|
-
setAuthSessionResolved(true);
|
|
874
|
-
authSessionLastResultRef.current = false;
|
|
875
|
-
return false;
|
|
876
|
-
}
|
|
877
|
-
const data = await res.json();
|
|
878
|
-
const required = Boolean(data.authRequiredForApi);
|
|
879
|
-
const authenticated = Boolean(data.authenticated);
|
|
880
|
-
const betaAccess = authenticated ? (data.betaAccess !== false) : true;
|
|
881
|
-
const workspaceId = typeof data.workspaceId === 'string' && data.workspaceId.trim().length > 0
|
|
882
|
-
? data.workspaceId.trim()
|
|
883
|
-
: '';
|
|
884
|
-
const userId = typeof data.userId === 'string' && data.userId.trim().length > 0
|
|
885
|
-
? data.userId.trim()
|
|
886
|
-
: 'anonymous';
|
|
887
|
-
const userEmail = typeof data.email === 'string' && data.email.trim().length > 0
|
|
888
|
-
? data.email.trim().toLowerCase()
|
|
889
|
-
: '';
|
|
890
|
-
const userDisplayName = typeof data.displayName === 'string'
|
|
891
|
-
? data.displayName.trim()
|
|
892
|
-
: '';
|
|
893
|
-
const userAvatarUrl = typeof data.avatarUrl === 'string'
|
|
894
|
-
? data.avatarUrl.trim()
|
|
895
|
-
: '';
|
|
896
|
-
const isLocalRuntime = runtimeMode === 'local';
|
|
897
|
-
const keepLocalWorkspaceContext = runtimeMode === 'local' || authOnlyCloudMode;
|
|
898
|
-
setAuthRequiredForApi(authOnlyCloudMode ? false : (isLocalRuntime ? false : required));
|
|
899
|
-
setIsAuthenticated(authenticated);
|
|
900
|
-
setAuthUserId(authenticated ? userId : 'anonymous');
|
|
901
|
-
setAuthWorkspaceId(authenticated ? workspaceId : '');
|
|
902
|
-
if (isLoopbackHost) {
|
|
903
|
-
persistLocalMutationActorUserId(authenticated ? userId : null);
|
|
904
|
-
}
|
|
905
|
-
setAuthUserEmail(authenticated ? userEmail : '');
|
|
906
|
-
setAuthUserDisplayName(authenticated ? userDisplayName : '');
|
|
907
|
-
setAuthUserAvatarUrl(authenticated ? userAvatarUrl : '');
|
|
908
|
-
if (authenticated) {
|
|
909
|
-
setError((prev) => prev === AUTH_REQUIRED_ERROR ? '' : prev);
|
|
910
|
-
}
|
|
911
|
-
setUserGlobalSyncStatus(authenticated ? 'idle' : 'disconnected');
|
|
912
|
-
setUserGlobalSyncError(null);
|
|
913
|
-
setHasBetaAccess(betaAccess);
|
|
914
|
-
if (keepLocalWorkspaceContext) {
|
|
915
|
-
// Local runtime resolves workspace identity from local DB via /api/taskforce/config.
|
|
916
|
-
// Do not override it from cloud auth session payloads.
|
|
917
|
-
}
|
|
918
|
-
else {
|
|
919
|
-
applyResolvedWorkspaceId(workspaceId || readPersistedWorkspaceId(workspaceSelectionScope) || 'default');
|
|
920
|
-
}
|
|
921
|
-
setAuthBlocked(authOnlyCloudMode ? false : (isLocalRuntime ? false : (required && !authenticated)));
|
|
922
|
-
// Runtime mode should be sourced from /api/taskforce/config, not
|
|
923
|
-
// from auth-session responses which may come from a separate cloud-auth origin.
|
|
924
|
-
setAuthSessionResolved(true);
|
|
925
|
-
authSessionLastResultRef.current = authenticated;
|
|
926
|
-
logBootstrapDebug('auth_session_resolved', {
|
|
927
|
-
authenticated,
|
|
928
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
929
|
-
});
|
|
930
|
-
if (authenticated)
|
|
931
|
-
markBootstrapPhase('ready');
|
|
932
|
-
return authenticated;
|
|
933
|
-
}
|
|
934
|
-
catch {
|
|
935
|
-
if (isLoopbackHost)
|
|
936
|
-
persistLocalMutationActorUserId(null);
|
|
937
|
-
setAuthSessionResolved(true);
|
|
938
|
-
authSessionLastResultRef.current = false;
|
|
939
|
-
logBootstrapDebug('auth_session_failed', {
|
|
940
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
941
|
-
});
|
|
942
|
-
markBootstrapStalled('Unable to verify your session.');
|
|
943
|
-
return authSessionLastResultRef.current;
|
|
944
|
-
}
|
|
945
|
-
finally {
|
|
946
|
-
if (timeoutId !== null && typeof window !== 'undefined') {
|
|
947
|
-
window.clearTimeout(timeoutId);
|
|
948
|
-
}
|
|
949
|
-
authSessionLastCheckedAtRef.current = Date.now();
|
|
950
|
-
authSessionRequestRef.current = null;
|
|
951
|
-
}
|
|
952
|
-
})();
|
|
953
|
-
authSessionRequestRef.current = request;
|
|
954
|
-
return request;
|
|
955
|
-
}, [
|
|
956
|
-
runtimeConfigReady,
|
|
957
|
-
shouldProbeCloudAuth,
|
|
958
|
-
authSessionResolved,
|
|
959
|
-
resolveCloudAuthUrl,
|
|
960
|
-
authOnlyCloudMode,
|
|
961
|
-
AUTH_REQUIRED_ERROR,
|
|
962
|
-
runtimeMode,
|
|
963
|
-
applyResolvedWorkspaceId,
|
|
964
|
-
workspaceSelectionScope,
|
|
965
|
-
markBootstrapPhase,
|
|
966
|
-
markBootstrapStalled
|
|
967
|
-
]);
|
|
814
|
+
const setUserGlobalSyncStatusRef = useRef(() => undefined);
|
|
815
|
+
const setUserGlobalSyncErrorRef = useRef(() => undefined);
|
|
816
|
+
const checkAuthSessionImplRef = useRef(async () => false);
|
|
817
|
+
const checkAuthSession = useCallback((options) => checkAuthSessionImplRef.current(options), []);
|
|
968
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({
|
|
969
819
|
currentWorkspaceId,
|
|
970
820
|
cloudAuthConfigured,
|
|
@@ -1046,6 +896,50 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1046
896
|
assigneeOptionsLoaded,
|
|
1047
897
|
assigneeOptions
|
|
1048
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;
|
|
1049
943
|
const [activeWorkspaceModule, setActiveWorkspaceModule] = useState('tasks');
|
|
1050
944
|
const [showComments, setShowComments] = useState(false);
|
|
1051
945
|
const [descriptionFocused, setDescriptionFocused] = useState(false);
|
|
@@ -1069,6 +963,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1069
963
|
const skipNextUiStatePersistRef = useRef(false);
|
|
1070
964
|
const referenceDataRequestRef = useRef(null);
|
|
1071
965
|
const workspaceListHydratedRef = useRef(false);
|
|
966
|
+
const hasBootstrappedDataRef = useRef(false);
|
|
1072
967
|
const [uiStateReady, setUiStateReady] = useState(false);
|
|
1073
968
|
const { handleSaveSettings, handleSaveTheme, handleSaveGlobalTheme, handleJsonBackupEnabledChange, handleSaveGlobalJsonBackupEnabled, handleSaveGlobalWeekStartsOn, handleSaveLocale, handleManualComplexityEnabledChange, handleChecklistDropdownEnabledChange, handleShowTaskCardStatusLabelChange, handleResetProjectToGlobal } = useSettingsPersistence({
|
|
1074
969
|
keyShortcut,
|
|
@@ -1236,15 +1131,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1236
1131
|
writePersistedAppUiState(uiStateSnapshot, workspaceUiStateScope);
|
|
1237
1132
|
const timeoutId = window.setTimeout(async () => {
|
|
1238
1133
|
try {
|
|
1239
|
-
await
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
body: JSON.stringify({
|
|
1244
|
-
stateKey: 'app',
|
|
1245
|
-
workspaceId: currentWorkspaceIdRef.current,
|
|
1246
|
-
patch: uiStateSnapshot
|
|
1247
|
-
})
|
|
1134
|
+
await persistRemoteUiState({
|
|
1135
|
+
stateKey: 'app',
|
|
1136
|
+
workspaceId: currentWorkspaceIdRef.current,
|
|
1137
|
+
patch: uiStateSnapshot
|
|
1248
1138
|
});
|
|
1249
1139
|
}
|
|
1250
1140
|
catch {
|
|
@@ -1281,13 +1171,15 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1281
1171
|
? data.userId.trim()
|
|
1282
1172
|
: 'anonymous';
|
|
1283
1173
|
const serverAuthenticated = nextRuntimeMode === 'cloud' && nextUserId !== 'anonymous';
|
|
1174
|
+
const knownAuthenticated = serverAuthenticated || isAuthenticatedRef.current;
|
|
1175
|
+
const authResolved = authSessionResolvedRef.current;
|
|
1284
1176
|
setRuntimeMode(nextRuntimeMode);
|
|
1285
1177
|
setAuthRequiredForApi(nextAuthRequiredForApi);
|
|
1286
1178
|
if (serverAuthenticated) {
|
|
1287
1179
|
setIsAuthenticated(true);
|
|
1288
1180
|
setAuthUserId(nextUserId);
|
|
1289
1181
|
setAuthBlocked(false);
|
|
1290
|
-
if (!
|
|
1182
|
+
if (!authResolved) {
|
|
1291
1183
|
setAuthSessionResolved(true);
|
|
1292
1184
|
}
|
|
1293
1185
|
}
|
|
@@ -1297,11 +1189,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1297
1189
|
commitCurrentWorkspaceId(nextWorkspaceId);
|
|
1298
1190
|
}
|
|
1299
1191
|
else {
|
|
1300
|
-
applyResolvedWorkspaceId(nextWorkspaceId
|
|
1192
|
+
applyResolvedWorkspaceId(nextWorkspaceId);
|
|
1301
1193
|
}
|
|
1302
1194
|
}
|
|
1303
1195
|
if (nextRuntimeMode === 'cloud' && nextAuthRequiredForApi) {
|
|
1304
|
-
setAuthBlocked(!
|
|
1196
|
+
setAuthBlocked(!knownAuthenticated);
|
|
1305
1197
|
}
|
|
1306
1198
|
else {
|
|
1307
1199
|
setAuthBlocked(false);
|
|
@@ -1399,7 +1291,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1399
1291
|
else {
|
|
1400
1292
|
setShowTaskCardStatusLabel(true);
|
|
1401
1293
|
}
|
|
1402
|
-
}, [applyResolvedWorkspaceId,
|
|
1294
|
+
}, [applyResolvedWorkspaceId, commitCurrentWorkspaceId]);
|
|
1403
1295
|
const fetchBuildInfo = useCallback(async () => {
|
|
1404
1296
|
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
1405
1297
|
try {
|
|
@@ -1425,417 +1317,83 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1425
1317
|
}
|
|
1426
1318
|
}
|
|
1427
1319
|
}, [fetchWithTimeout, isBootstrapTimeoutError, markBootstrapStalled]);
|
|
1428
|
-
const fetchReferenceData
|
|
1429
|
-
const ignoreAuthGuard = options?.ignoreAuthGuard === true;
|
|
1430
|
-
const force = options?.force === true;
|
|
1431
|
-
if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
|
|
1432
|
-
return;
|
|
1433
|
-
const requestState = getWorkspaceRequestState();
|
|
1434
|
-
if (!force && referenceDataRequestRef.current?.workspaceId === requestState.workspaceId) {
|
|
1435
|
-
await referenceDataRequestRef.current.promise;
|
|
1436
|
-
return;
|
|
1437
|
-
}
|
|
1438
|
-
setReferenceDataLoaded(false);
|
|
1439
|
-
const abortController = new AbortController();
|
|
1440
|
-
referenceDataAbortRef.current = abortController;
|
|
1441
|
-
const request = (async () => {
|
|
1442
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
1443
|
-
await Promise.allSettled([
|
|
1444
|
-
(async () => {
|
|
1445
|
-
const taxonomyRes = await fetch('/api/taskforce/taxonomy-state', { signal: abortController.signal });
|
|
1446
|
-
if (!taxonomyRes.ok)
|
|
1447
|
-
return;
|
|
1448
|
-
const taxonomyData = await taxonomyRes.json().catch(() => ({}));
|
|
1449
|
-
const hasUnifiedPayload = hasWorkspaceTaxonomyStatePayload(taxonomyData);
|
|
1450
|
-
if (!hasUnifiedPayload) {
|
|
1451
|
-
const [catRes, typeRes, priorityRes, taxRes] = await Promise.all([
|
|
1452
|
-
fetch('/api/taskforce/categories', { signal: abortController.signal }),
|
|
1453
|
-
fetch('/api/taskforce/types', { signal: abortController.signal }),
|
|
1454
|
-
fetch('/api/taskforce/priorities', { signal: abortController.signal }),
|
|
1455
|
-
fetch('/api/taskforce/taxonomies', { signal: abortController.signal })
|
|
1456
|
-
]);
|
|
1457
|
-
const [catData, typeData, priorityData, taxData] = await Promise.all([
|
|
1458
|
-
catRes.ok ? catRes.json().catch(() => ({})) : Promise.resolve({}),
|
|
1459
|
-
typeRes.ok ? typeRes.json().catch(() => ({})) : Promise.resolve({}),
|
|
1460
|
-
priorityRes.ok ? priorityRes.json().catch(() => ({})) : Promise.resolve({}),
|
|
1461
|
-
taxRes.ok ? taxRes.json().catch(() => ({})) : Promise.resolve({})
|
|
1462
|
-
]);
|
|
1463
|
-
taxonomyData.categories = catData.categories;
|
|
1464
|
-
taxonomyData.types = typeData.types;
|
|
1465
|
-
taxonomyData.priorities = priorityData.priorities;
|
|
1466
|
-
taxonomyData.taxonomies = taxData.taxonomies;
|
|
1467
|
-
}
|
|
1468
|
-
const normalizedTaxonomyState = normalizeWorkspaceTaxonomyStatePayload(taxonomyData);
|
|
1469
|
-
if (isWorkspaceRequestStale(requestState))
|
|
1470
|
-
return;
|
|
1471
|
-
if (normalizedTaxonomyState.categories.length > 0)
|
|
1472
|
-
setCustomCategories(normalizedTaxonomyState.categories);
|
|
1473
|
-
if (normalizedTaxonomyState.types.length > 0)
|
|
1474
|
-
setCustomTypes(normalizedTaxonomyState.types);
|
|
1475
|
-
if (normalizedTaxonomyState.priorities.length > 0)
|
|
1476
|
-
setPriorities(normalizedTaxonomyState.priorities);
|
|
1477
|
-
setTaxonomies(normalizedTaxonomyState.taxonomies);
|
|
1478
|
-
setTaxonomyDisplayLabels(normalizedTaxonomyState.displayLabels || {});
|
|
1479
|
-
})(),
|
|
1480
|
-
(async () => {
|
|
1481
|
-
await fetchWorkflows();
|
|
1482
|
-
})(),
|
|
1483
|
-
(async () => {
|
|
1484
|
-
await fetchInitiativeTemplates();
|
|
1485
|
-
})(),
|
|
1486
|
-
(async () => {
|
|
1487
|
-
await loadAvailableEnvironments();
|
|
1488
|
-
})(),
|
|
1489
|
-
]);
|
|
1490
|
-
if (isWorkspaceRequestStale(requestState))
|
|
1491
|
-
return;
|
|
1492
|
-
setReferenceDataLoaded(true);
|
|
1493
|
-
logBootstrapDebug('reference_data_loaded', {
|
|
1494
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
1495
|
-
});
|
|
1496
|
-
})();
|
|
1497
|
-
referenceDataRequestRef.current = { workspaceId: requestState.workspaceId, promise: request };
|
|
1498
|
-
try {
|
|
1499
|
-
await request;
|
|
1500
|
-
}
|
|
1501
|
-
catch (error) {
|
|
1502
|
-
if (!isAbortError(error))
|
|
1503
|
-
throw error;
|
|
1504
|
-
}
|
|
1505
|
-
finally {
|
|
1506
|
-
if (referenceDataRequestRef.current?.promise === request) {
|
|
1507
|
-
referenceDataRequestRef.current = null;
|
|
1508
|
-
}
|
|
1509
|
-
if (referenceDataAbortRef.current === abortController) {
|
|
1510
|
-
referenceDataAbortRef.current = null;
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
}, [
|
|
1514
|
-
fetchWorkflows,
|
|
1515
|
-
fetchInitiativeTemplates,
|
|
1516
|
-
getWorkspaceRequestState,
|
|
1517
|
-
isAbortError,
|
|
1518
|
-
isWorkspaceRequestStale,
|
|
1519
|
-
loadAvailableEnvironments,
|
|
1320
|
+
const { fetchReferenceData, fetchBootstrapConfig, fetchConfig, fetchWorkspaces, fetchAssigneeOptions, switchWorkspace, resolveWorkspaceAfterAuth, retryBootstrapChecks } = useTaskforceWorkspaceBootstrap({
|
|
1520
1321
|
shouldDeferProtectedApiCalls,
|
|
1521
|
-
shouldBlockProtectedApiCalls
|
|
1522
|
-
]);
|
|
1523
|
-
const fetchBootstrapConfig = useCallback(async (options) => {
|
|
1524
|
-
const ignoreAuthGuard = options?.ignoreAuthGuard === true;
|
|
1525
|
-
if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
|
|
1526
|
-
return;
|
|
1527
|
-
markBootstrapPhase('config');
|
|
1528
|
-
const requestState = getWorkspaceRequestState();
|
|
1529
|
-
const abortController = new AbortController();
|
|
1530
|
-
bootstrapConfigAbortRef.current = abortController;
|
|
1531
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
1532
|
-
try {
|
|
1533
|
-
const res = await fetchWithTimeout('/api/taskforce/config', undefined, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
|
|
1534
|
-
signal: abortController.signal
|
|
1535
|
-
});
|
|
1536
|
-
if (isWorkspaceEpochStale(requestState))
|
|
1537
|
-
return;
|
|
1538
|
-
if (res.status === 401) {
|
|
1539
|
-
handleUnauthorized();
|
|
1540
|
-
setConfigLoaded(true);
|
|
1541
|
-
return;
|
|
1542
|
-
}
|
|
1543
|
-
let configResolvedWorkspaceId = '';
|
|
1544
|
-
if (res.ok) {
|
|
1545
|
-
const data = await res.json();
|
|
1546
|
-
if (isWorkspaceEpochStale(requestState))
|
|
1547
|
-
return;
|
|
1548
|
-
configResolvedWorkspaceId = typeof data?.workspaceId === 'string' && data.workspaceId.trim().length > 0
|
|
1549
|
-
? data.workspaceId.trim()
|
|
1550
|
-
: '';
|
|
1551
|
-
applyBootstrapConfig(data);
|
|
1552
|
-
}
|
|
1553
|
-
if (isWorkspaceEpochStale(requestState)) {
|
|
1554
|
-
const liveWorkspaceId = String(currentWorkspaceIdRef.current || 'default').trim() || 'default';
|
|
1555
|
-
if (!configResolvedWorkspaceId || liveWorkspaceId !== configResolvedWorkspaceId) {
|
|
1556
|
-
return;
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
if (!hasLoadedUiStateRef.current) {
|
|
1560
|
-
const uiStateWorkspaceId = configResolvedWorkspaceId || requestState.workspaceId;
|
|
1561
|
-
hasLoadedUiStateRef.current = true;
|
|
1562
|
-
await loadPersistedUiState(uiStateWorkspaceId);
|
|
1563
|
-
}
|
|
1564
|
-
if (isWorkspaceEpochStale(requestState)) {
|
|
1565
|
-
const liveWorkspaceId = String(currentWorkspaceIdRef.current || 'default').trim() || 'default';
|
|
1566
|
-
if (!configResolvedWorkspaceId || liveWorkspaceId !== configResolvedWorkspaceId) {
|
|
1567
|
-
return;
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
setConfigLoaded(true);
|
|
1571
|
-
markBootstrapPhase('ready');
|
|
1572
|
-
logBootstrapDebug('bootstrap_config_loaded', {
|
|
1573
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
1574
|
-
});
|
|
1575
|
-
void fetchBuildInfo();
|
|
1576
|
-
}
|
|
1577
|
-
catch (error) {
|
|
1578
|
-
if (isAbortError(error))
|
|
1579
|
-
return;
|
|
1580
|
-
if (isWorkspaceEpochStale(requestState))
|
|
1581
|
-
return;
|
|
1582
|
-
if (!hasLoadedUiStateRef.current) {
|
|
1583
|
-
hasLoadedUiStateRef.current = true;
|
|
1584
|
-
await loadPersistedUiState(requestState.workspaceId);
|
|
1585
|
-
}
|
|
1586
|
-
if (isBootstrapTimeoutError(error)) {
|
|
1587
|
-
markBootstrapStalled('Startup checks timed out.');
|
|
1588
|
-
}
|
|
1589
|
-
else {
|
|
1590
|
-
markBootstrapStalled('Unable to finish startup checks.');
|
|
1591
|
-
}
|
|
1592
|
-
setConfigLoaded(true);
|
|
1593
|
-
}
|
|
1594
|
-
finally {
|
|
1595
|
-
if (bootstrapConfigAbortRef.current === abortController) {
|
|
1596
|
-
bootstrapConfigAbortRef.current = null;
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
}, [
|
|
1600
|
-
applyBootstrapConfig,
|
|
1601
|
-
fetchBuildInfo,
|
|
1602
|
-
fetchWithTimeout,
|
|
1322
|
+
shouldBlockProtectedApiCalls,
|
|
1603
1323
|
getWorkspaceRequestState,
|
|
1604
|
-
|
|
1324
|
+
isWorkspaceRequestStale,
|
|
1325
|
+
isWorkspaceEpochStale,
|
|
1605
1326
|
isAbortError,
|
|
1606
1327
|
isBootstrapTimeoutError,
|
|
1607
|
-
|
|
1608
|
-
|
|
1328
|
+
fetchWithTimeout,
|
|
1329
|
+
handleUnauthorized,
|
|
1330
|
+
applyBootstrapConfig,
|
|
1609
1331
|
loadPersistedUiState,
|
|
1332
|
+
fetchBuildInfo,
|
|
1610
1333
|
markBootstrapPhase,
|
|
1611
1334
|
markBootstrapStalled,
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
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
|
+
});
|
|
1619
1393
|
const refreshSetupContext = useCallback(async () => {
|
|
1620
1394
|
await checkAuthSession();
|
|
1621
1395
|
await fetchConfig();
|
|
1622
1396
|
}, [checkAuthSession, fetchConfig]);
|
|
1623
|
-
const fetchWorkspaces = useCallback(async () => {
|
|
1624
|
-
if (!workspaceSwitchingEnabled) {
|
|
1625
|
-
setAvailableWorkspaces([]);
|
|
1626
|
-
workspaceListHydratedRef.current = false;
|
|
1627
|
-
return { success: true, workspaces: [] };
|
|
1628
|
-
}
|
|
1629
|
-
const requestState = getWorkspaceRequestState();
|
|
1630
|
-
const abortController = new AbortController();
|
|
1631
|
-
workspaceListAbortRef.current = abortController;
|
|
1632
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
1633
|
-
try {
|
|
1634
|
-
const res = await fetchWithTimeout('/api/taskforce/workspaces', {
|
|
1635
|
-
method: 'GET',
|
|
1636
|
-
credentials: 'include'
|
|
1637
|
-
}, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
|
|
1638
|
-
signal: abortController.signal
|
|
1639
|
-
});
|
|
1640
|
-
if (isWorkspaceEpochStale(requestState)) {
|
|
1641
|
-
return { success: false, error: 'Workspace changed while loading workspaces.' };
|
|
1642
|
-
}
|
|
1643
|
-
if (res.status === 401) {
|
|
1644
|
-
setAvailableWorkspaces([]);
|
|
1645
|
-
workspaceListHydratedRef.current = false;
|
|
1646
|
-
return { success: false, error: 'Authentication required.' };
|
|
1647
|
-
}
|
|
1648
|
-
const data = await res.json().catch(() => ({}));
|
|
1649
|
-
if (isWorkspaceEpochStale(requestState)) {
|
|
1650
|
-
return { success: false, error: 'Workspace changed while loading workspaces.' };
|
|
1651
|
-
}
|
|
1652
|
-
if (!res.ok || data?.success === false) {
|
|
1653
|
-
setAvailableWorkspaces([]);
|
|
1654
|
-
workspaceListHydratedRef.current = false;
|
|
1655
|
-
return { success: false, error: data?.error || `Failed to load workspaces (${res.status})` };
|
|
1656
|
-
}
|
|
1657
|
-
const workspaces = Array.isArray(data?.workspaces) ? data.workspaces : [];
|
|
1658
|
-
setAvailableWorkspaces(workspaces);
|
|
1659
|
-
workspaceListHydratedRef.current = true;
|
|
1660
|
-
if (runtimeMode !== 'local' && typeof data?.currentWorkspaceId === 'string' && data.currentWorkspaceId.trim().length > 0) {
|
|
1661
|
-
applyResolvedWorkspaceId(data.currentWorkspaceId.trim(), { preserveNonDefaultDefault: true });
|
|
1662
|
-
}
|
|
1663
|
-
logBootstrapDebug('workspace_list_loaded', {
|
|
1664
|
-
workspaceCount: workspaces.length,
|
|
1665
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
1666
|
-
});
|
|
1667
|
-
return { success: true, workspaces };
|
|
1668
|
-
}
|
|
1669
|
-
catch (error) {
|
|
1670
|
-
if (isAbortError(error)) {
|
|
1671
|
-
return { success: false, error: 'Workspace list request aborted.' };
|
|
1672
|
-
}
|
|
1673
|
-
if (isBootstrapTimeoutError(error)) {
|
|
1674
|
-
markBootstrapStalled('Startup checks timed out.');
|
|
1675
|
-
}
|
|
1676
|
-
setAvailableWorkspaces([]);
|
|
1677
|
-
workspaceListHydratedRef.current = false;
|
|
1678
|
-
return { success: false, error: 'Failed to load workspaces.' };
|
|
1679
|
-
}
|
|
1680
|
-
finally {
|
|
1681
|
-
if (workspaceListAbortRef.current === abortController) {
|
|
1682
|
-
workspaceListAbortRef.current = null;
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
}, [
|
|
1686
|
-
applyResolvedWorkspaceId,
|
|
1687
|
-
fetchWithTimeout,
|
|
1688
|
-
getWorkspaceRequestState,
|
|
1689
|
-
isAbortError,
|
|
1690
|
-
isWorkspaceEpochStale,
|
|
1691
|
-
isBootstrapTimeoutError,
|
|
1692
|
-
isWorkspaceRequestStale,
|
|
1693
|
-
markBootstrapStalled,
|
|
1694
|
-
runtimeMode,
|
|
1695
|
-
workspaceSwitchingEnabled
|
|
1696
|
-
]);
|
|
1697
|
-
const fetchAssigneeOptions = useCallback(async () => {
|
|
1698
|
-
const requestState = getWorkspaceRequestState();
|
|
1699
|
-
const abortController = new AbortController();
|
|
1700
|
-
assigneeOptionsAbortRef.current = abortController;
|
|
1701
|
-
setAssigneeOptionsLoaded(false);
|
|
1702
|
-
try {
|
|
1703
|
-
const [assigneeResult, workspaceMembersResult] = await Promise.all([
|
|
1704
|
-
fetchWithTimeout('/api/taskforce/workspace/assignee-options', {
|
|
1705
|
-
method: 'GET',
|
|
1706
|
-
credentials: 'include'
|
|
1707
|
-
}, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
|
|
1708
|
-
signal: abortController.signal
|
|
1709
|
-
}),
|
|
1710
|
-
(authSessionResolved && isAuthenticated)
|
|
1711
|
-
? fetchWithTimeout('/api/taskforce/auth/workspace-members', {
|
|
1712
|
-
method: 'GET',
|
|
1713
|
-
credentials: 'include'
|
|
1714
|
-
}, BOOTSTRAP_REQUEST_TIMEOUT_MS, {
|
|
1715
|
-
signal: abortController.signal
|
|
1716
|
-
}).catch(() => null)
|
|
1717
|
-
: Promise.resolve(null)
|
|
1718
|
-
]);
|
|
1719
|
-
if (isWorkspaceRequestStale(requestState)) {
|
|
1720
|
-
return { success: false, error: 'Workspace changed while loading assignee options.' };
|
|
1721
|
-
}
|
|
1722
|
-
if (assigneeResult.status === 401) {
|
|
1723
|
-
setAssigneeOptions(createDefaultAssigneeOptions());
|
|
1724
|
-
setAssigneeOptionsLoaded(true);
|
|
1725
|
-
return { success: false, error: 'Authentication required.' };
|
|
1726
|
-
}
|
|
1727
|
-
const data = await assigneeResult.json().catch(() => ({}));
|
|
1728
|
-
if (isWorkspaceRequestStale(requestState)) {
|
|
1729
|
-
return { success: false, error: 'Workspace changed while loading assignee options.' };
|
|
1730
|
-
}
|
|
1731
|
-
if (!assigneeResult.ok) {
|
|
1732
|
-
setAssigneeOptions(createDefaultAssigneeOptions());
|
|
1733
|
-
setAssigneeOptionsLoaded(true);
|
|
1734
|
-
return { success: false, error: data?.error || `Failed to load assignee options (${assigneeResult.status})` };
|
|
1735
|
-
}
|
|
1736
|
-
const assignees = Array.isArray(data?.assignees) ? data.assignees : [];
|
|
1737
|
-
const directOptions = assignees
|
|
1738
|
-
.map((entry) => {
|
|
1739
|
-
const value = String(entry?.value || '').trim();
|
|
1740
|
-
const kindRaw = String(entry?.kind || '').trim().toLowerCase();
|
|
1741
|
-
if (!value)
|
|
1742
|
-
return null;
|
|
1743
|
-
if (kindRaw === 'agent' || kindRaw === 'ai') {
|
|
1744
|
-
return {
|
|
1745
|
-
value,
|
|
1746
|
-
label: String(entry?.label || value).trim() || value,
|
|
1747
|
-
icon: String(entry?.icon || 'Bot'),
|
|
1748
|
-
color: String(entry?.color || '#8b5cf6'),
|
|
1749
|
-
kind: 'agent'
|
|
1750
|
-
};
|
|
1751
|
-
}
|
|
1752
|
-
if (kindRaw === 'member' || kindRaw === 'human') {
|
|
1753
|
-
return {
|
|
1754
|
-
value,
|
|
1755
|
-
label: String(entry?.label || value).trim() || value,
|
|
1756
|
-
icon: String(entry?.icon || 'User'),
|
|
1757
|
-
color: String(entry?.color || '#22c55e'),
|
|
1758
|
-
kind: 'member'
|
|
1759
|
-
};
|
|
1760
|
-
}
|
|
1761
|
-
return null;
|
|
1762
|
-
})
|
|
1763
|
-
.filter((candidate) => Boolean(candidate));
|
|
1764
|
-
const fallbackCandidates = assignees
|
|
1765
|
-
.map((candidate) => ({
|
|
1766
|
-
userId: String(candidate?.userId || '').trim(),
|
|
1767
|
-
email: String(candidate?.email || '').trim().toLowerCase(),
|
|
1768
|
-
displayName: typeof candidate?.displayName === 'string' ? candidate.displayName : null,
|
|
1769
|
-
status: typeof candidate?.status === 'string' ? candidate.status : null,
|
|
1770
|
-
disabled: candidate?.disabled === true,
|
|
1771
|
-
}))
|
|
1772
|
-
.filter((candidate) => candidate.userId
|
|
1773
|
-
&& String(candidate.status || 'active').trim().toLowerCase() === 'active'
|
|
1774
|
-
&& candidate.disabled !== true);
|
|
1775
|
-
let memberCandidates = fallbackCandidates;
|
|
1776
|
-
if (workspaceMembersResult?.ok) {
|
|
1777
|
-
const workspaceMembersData = await workspaceMembersResult.json().catch(() => ({}));
|
|
1778
|
-
const users = Array.isArray(workspaceMembersData?.users) ? workspaceMembersData.users : [];
|
|
1779
|
-
const normalizedAuthUserId = String(authUserId || '').trim();
|
|
1780
|
-
const currentMember = users.find((candidate) => String(candidate?.userId || '').trim() === normalizedAuthUserId);
|
|
1781
|
-
const currentMemberRole = String(currentMember?.role || '').trim().toLowerCase();
|
|
1782
|
-
if (currentMemberRole === 'owner' || currentMemberRole === 'admin' || currentMemberRole === 'member' || currentMemberRole === 'read-only') {
|
|
1783
|
-
setHydratedWorkspaceRole(currentMemberRole);
|
|
1784
|
-
}
|
|
1785
|
-
else if (!workspaceSwitchingEnabled && users.length === 0) {
|
|
1786
|
-
setHydratedWorkspaceRole(null);
|
|
1787
|
-
}
|
|
1788
|
-
const workspaceMemberCandidates = users
|
|
1789
|
-
.map((candidate) => ({
|
|
1790
|
-
userId: String(candidate?.userId || '').trim(),
|
|
1791
|
-
email: String(candidate?.email || '').trim().toLowerCase(),
|
|
1792
|
-
displayName: typeof candidate?.displayName === 'string' ? candidate.displayName : null,
|
|
1793
|
-
status: typeof candidate?.status === 'string' ? candidate.status : null,
|
|
1794
|
-
disabled: candidate?.disabled === true,
|
|
1795
|
-
}))
|
|
1796
|
-
.filter((candidate) => candidate.userId
|
|
1797
|
-
&& String(candidate.status || 'active').trim().toLowerCase() === 'active'
|
|
1798
|
-
&& candidate.disabled !== true)
|
|
1799
|
-
.map((candidate) => ({
|
|
1800
|
-
userId: candidate.userId,
|
|
1801
|
-
email: candidate.email,
|
|
1802
|
-
displayName: candidate.displayName ?? null
|
|
1803
|
-
}));
|
|
1804
|
-
memberCandidates = Array.from(new Map([...fallbackCandidates, ...workspaceMemberCandidates].map((candidate) => [
|
|
1805
|
-
String(candidate.userId || '').trim(),
|
|
1806
|
-
candidate
|
|
1807
|
-
])).values()).filter((candidate) => String(candidate.userId || '').trim().length > 0);
|
|
1808
|
-
}
|
|
1809
|
-
else if (!workspaceSwitchingEnabled && !fallbackCandidates.length) {
|
|
1810
|
-
setHydratedWorkspaceRole(null);
|
|
1811
|
-
}
|
|
1812
|
-
const mergedOptions = mergeAssigneeOptions([
|
|
1813
|
-
...directOptions,
|
|
1814
|
-
...createWorkspaceAssigneeOptions(memberCandidates).filter((option) => option.kind === 'member')
|
|
1815
|
-
]);
|
|
1816
|
-
if (isWorkspaceRequestStale(requestState)) {
|
|
1817
|
-
return { success: false, error: 'Workspace changed while loading assignee options.' };
|
|
1818
|
-
}
|
|
1819
|
-
setAssigneeOptions(mergedOptions);
|
|
1820
|
-
setAssigneeOptionsLoaded(true);
|
|
1821
|
-
return { success: true };
|
|
1822
|
-
}
|
|
1823
|
-
catch (error) {
|
|
1824
|
-
if (isAbortError(error)) {
|
|
1825
|
-
return { success: false, error: 'Assignee options request aborted.' };
|
|
1826
|
-
}
|
|
1827
|
-
if (!isWorkspaceRequestStale(requestState)) {
|
|
1828
|
-
setAssigneeOptions(createDefaultAssigneeOptions());
|
|
1829
|
-
setAssigneeOptionsLoaded(true);
|
|
1830
|
-
}
|
|
1831
|
-
return { success: false, error: 'Failed to load assignee options.' };
|
|
1832
|
-
}
|
|
1833
|
-
finally {
|
|
1834
|
-
if (assigneeOptionsAbortRef.current === abortController) {
|
|
1835
|
-
assigneeOptionsAbortRef.current = null;
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
}, [authSessionResolved, authUserId, fetchWithTimeout, getWorkspaceRequestState, isAbortError, isAuthenticated, isWorkspaceRequestStale, workspaceSwitchingEnabled]);
|
|
1839
1397
|
useEffect(() => {
|
|
1840
1398
|
if (!configLoaded || workspaceBootstrapPending)
|
|
1841
1399
|
return;
|
|
@@ -1857,162 +1415,44 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
1857
1415
|
setHydratedWorkspaceRole(null);
|
|
1858
1416
|
}
|
|
1859
1417
|
}, [isAuthenticated]);
|
|
1860
|
-
const
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
return { success: false, error: '
|
|
1867
|
-
try {
|
|
1868
|
-
const res = await fetch('/api/taskforce/session/workspace', {
|
|
1869
|
-
method: 'POST',
|
|
1870
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1871
|
-
credentials: 'include',
|
|
1872
|
-
body: JSON.stringify({ workspaceId: normalizedWorkspaceId })
|
|
1873
|
-
});
|
|
1874
|
-
const data = await res.json().catch(() => ({}));
|
|
1875
|
-
if (!res.ok || data?.success === false) {
|
|
1876
|
-
return { success: false, error: data?.error || `Failed to switch workspace (${res.status})`, code: data?.code };
|
|
1877
|
-
}
|
|
1878
|
-
explicitWorkspaceSelectionRef.current = normalizedWorkspaceId;
|
|
1879
|
-
commitCurrentWorkspaceId(normalizedWorkspaceId, { clearExplicitSelection: false });
|
|
1880
|
-
abortWorkspaceScopedRequests();
|
|
1881
|
-
setWorkspaceBootstrapPending(true);
|
|
1882
|
-
markBootstrapPhase('workspace');
|
|
1883
|
-
const authenticated = await checkAuthSession({ force: true });
|
|
1884
|
-
if (options?.hydrate === false) {
|
|
1885
|
-
await fetchWorkspaces();
|
|
1886
|
-
}
|
|
1887
|
-
else {
|
|
1888
|
-
lastConfigRefreshKeyRef.current = `${runtimeMode}:${normalizedWorkspaceId}:${workspaceSelectionScope}`;
|
|
1889
|
-
lastAssigneeOptionsLoadKeyRef.current = `${runtimeMode}:${authenticated ? 'auth' : 'guest'}:${normalizedWorkspaceId}`;
|
|
1890
|
-
lastWorkspaceSyncStateLoadKeyRef.current = normalizedWorkspaceId;
|
|
1891
|
-
if (isOpen && activeTab === 'tasks') {
|
|
1892
|
-
lastTaskSurfaceLoadKeyRef.current = `${normalizedWorkspaceId}:${activeTab}:${showArchive ? 'archive' : 'active'}:${taskScope}`;
|
|
1893
|
-
}
|
|
1894
|
-
if (isOpen && activeTab === 'settings') {
|
|
1895
|
-
lastSettingsSurfaceLoadKeyRef.current = `${normalizedWorkspaceId}:${activeTab}:${settingsSection}`;
|
|
1896
|
-
}
|
|
1897
|
-
await Promise.all([
|
|
1898
|
-
fetchConfig(),
|
|
1899
|
-
fetchWorkspaces(),
|
|
1900
|
-
refreshTaskCollections({ isSilent: false }),
|
|
1901
|
-
fetchAssigneeOptions(),
|
|
1902
|
-
loadWorkspaceSyncState()
|
|
1903
|
-
]);
|
|
1904
|
-
}
|
|
1905
|
-
if (explicitWorkspaceSelectionRef.current === normalizedWorkspaceId) {
|
|
1906
|
-
explicitWorkspaceSelectionRef.current = null;
|
|
1907
|
-
}
|
|
1908
|
-
return { success: true };
|
|
1909
|
-
}
|
|
1910
|
-
catch {
|
|
1911
|
-
if (explicitWorkspaceSelectionRef.current === normalizedWorkspaceId) {
|
|
1912
|
-
explicitWorkspaceSelectionRef.current = null;
|
|
1913
|
-
}
|
|
1914
|
-
return { success: false, error: 'Failed to switch workspace.' };
|
|
1915
|
-
}
|
|
1916
|
-
finally {
|
|
1917
|
-
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' };
|
|
1918
1425
|
}
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
1924
|
-
try {
|
|
1925
|
-
if (!workspaceSwitchingEnabled) {
|
|
1926
|
-
const localWorkspaceId = String(currentWorkspaceId || '').trim() || 'default';
|
|
1927
|
-
commitCurrentWorkspaceId(localWorkspaceId);
|
|
1928
|
-
logBootstrapDebug('workspace_resolved', {
|
|
1929
|
-
workspaceId: localWorkspaceId,
|
|
1930
|
-
workspaceSetupRequired: false,
|
|
1931
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
1932
|
-
});
|
|
1933
|
-
return { success: true, workspaceSetupRequired: false, workspaceId: localWorkspaceId };
|
|
1934
|
-
}
|
|
1935
|
-
const preferredWorkspaceId = String(options?.preferredWorkspaceId || '').trim();
|
|
1936
|
-
const persistedWorkspaceId = readPersistedWorkspaceId(workspaceSelectionScope);
|
|
1937
|
-
const workspacesResult = await fetchWorkspaces();
|
|
1938
|
-
if (!workspacesResult.success) {
|
|
1426
|
+
if (workspaceSwitchingEnabled) {
|
|
1427
|
+
const workspaceResolution = await resolveWorkspaceAfterAuth();
|
|
1428
|
+
if (!workspaceResolution.success) {
|
|
1429
|
+
await fetchConfig({ ignoreAuthGuard: true });
|
|
1939
1430
|
return {
|
|
1940
1431
|
success: false,
|
|
1941
|
-
|
|
1942
|
-
error:
|
|
1432
|
+
destination: 'app',
|
|
1433
|
+
error: workspaceResolution.error || 'Unable to resolve workspace after plan selection.',
|
|
1434
|
+
code: workspaceResolution.code
|
|
1943
1435
|
};
|
|
1944
1436
|
}
|
|
1945
|
-
|
|
1946
|
-
if (workspaces.length === 0) {
|
|
1947
|
-
commitCurrentWorkspaceId('default');
|
|
1948
|
-
logBootstrapDebug('workspace_resolved', {
|
|
1949
|
-
workspaceId: 'default',
|
|
1950
|
-
workspaceSetupRequired: true,
|
|
1951
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
1952
|
-
});
|
|
1953
|
-
return { success: true, workspaceSetupRequired: true };
|
|
1954
|
-
}
|
|
1955
|
-
const ids = new Set(workspaces.map((workspace) => String(workspace.id || '').trim()).filter(Boolean));
|
|
1956
|
-
const selectedWorkspaceId = ((preferredWorkspaceId && ids.has(preferredWorkspaceId) ? preferredWorkspaceId : '')
|
|
1957
|
-
|| (persistedWorkspaceId && ids.has(persistedWorkspaceId) ? persistedWorkspaceId : '')
|
|
1958
|
-
|| String(workspaces[0]?.id || '').trim());
|
|
1959
|
-
if (!selectedWorkspaceId) {
|
|
1960
|
-
commitCurrentWorkspaceId('default');
|
|
1961
|
-
return { success: true, workspaceSetupRequired: true };
|
|
1962
|
-
}
|
|
1963
|
-
const current = String(currentWorkspaceIdRef.current || '').trim();
|
|
1964
|
-
if (current !== selectedWorkspaceId) {
|
|
1965
|
-
const switched = await switchWorkspace(selectedWorkspaceId, { hydrate: false });
|
|
1966
|
-
if (!switched.success) {
|
|
1967
|
-
return {
|
|
1968
|
-
success: false,
|
|
1969
|
-
workspaceSetupRequired: false,
|
|
1970
|
-
error: switched.error || 'Failed to set workspace after authentication.',
|
|
1971
|
-
code: switched.code
|
|
1972
|
-
};
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
else {
|
|
1976
|
-
commitCurrentWorkspaceId(selectedWorkspaceId);
|
|
1977
|
-
}
|
|
1978
|
-
logBootstrapDebug('workspace_resolved', {
|
|
1979
|
-
workspaceId: selectedWorkspaceId,
|
|
1980
|
-
workspaceSetupRequired: false,
|
|
1981
|
-
durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
|
|
1982
|
-
});
|
|
1983
|
-
return { success: true, workspaceSetupRequired: false, workspaceId: selectedWorkspaceId };
|
|
1984
|
-
}
|
|
1985
|
-
catch {
|
|
1986
|
-
markBootstrapStalled('Unable to resolve workspace access.');
|
|
1987
|
-
return { success: false, workspaceSetupRequired: false, error: 'Failed to resolve workspace access.' };
|
|
1988
|
-
}
|
|
1989
|
-
finally {
|
|
1990
|
-
setWorkspaceBootstrapPending(false);
|
|
1991
|
-
}
|
|
1992
|
-
}, [
|
|
1993
|
-
commitCurrentWorkspaceId,
|
|
1994
|
-
fetchWorkspaces,
|
|
1995
|
-
markBootstrapPhase,
|
|
1996
|
-
markBootstrapStalled,
|
|
1997
|
-
workspaceSelectionScope,
|
|
1998
|
-
switchWorkspace,
|
|
1999
|
-
workspaceSwitchingEnabled
|
|
2000
|
-
]);
|
|
2001
|
-
const retryBootstrapChecks = useCallback(async () => {
|
|
2002
|
-
setBootstrapError(null);
|
|
2003
|
-
markBootstrapPhase('auth');
|
|
2004
|
-
const authenticated = await checkAuthSession({ force: true });
|
|
2005
|
-
if (workspaceSwitchingEnabled && authenticated) {
|
|
2006
|
-
const workspaceResolution = await resolveWorkspaceAfterAuth();
|
|
2007
|
-
if (!workspaceResolution.success || workspaceResolution.workspaceSetupRequired) {
|
|
1437
|
+
if (workspaceResolution.workspaceSetupRequired) {
|
|
2008
1438
|
await fetchConfig({ ignoreAuthGuard: true });
|
|
2009
|
-
return;
|
|
1439
|
+
return { success: true, destination: 'setup' };
|
|
2010
1440
|
}
|
|
2011
1441
|
}
|
|
2012
|
-
await
|
|
1442
|
+
await fetchBootstrapConfig();
|
|
1443
|
+
await Promise.all([
|
|
1444
|
+
fetchTasks(true, { ignoreAuthGuard: true }),
|
|
1445
|
+
fetchReferenceData(),
|
|
1446
|
+
fetchPlanningEntities()
|
|
1447
|
+
]);
|
|
1448
|
+
return { success: true, destination: 'app' };
|
|
2013
1449
|
}, [
|
|
2014
1450
|
checkAuthSession,
|
|
1451
|
+
fetchBootstrapConfig,
|
|
2015
1452
|
fetchConfig,
|
|
1453
|
+
fetchPlanningEntities,
|
|
1454
|
+
fetchReferenceData,
|
|
1455
|
+
fetchTasks,
|
|
2016
1456
|
markBootstrapPhase,
|
|
2017
1457
|
resolveWorkspaceAfterAuth,
|
|
2018
1458
|
workspaceSwitchingEnabled
|
|
@@ -2106,6 +1546,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2106
1546
|
}, [refreshSetupContext]);
|
|
2107
1547
|
const saveWorkspaceProfile = useCallback(async (input) => {
|
|
2108
1548
|
const name = String(input?.name || '').trim();
|
|
1549
|
+
const requestedWorkspaceId = typeof input?.workspaceId === 'string'
|
|
1550
|
+
? input.workspaceId.trim()
|
|
1551
|
+
: '';
|
|
2109
1552
|
if (!name)
|
|
2110
1553
|
return { success: false, error: 'Workspace name is required.', code: 'WORKSPACE_NAME_REQUIRED' };
|
|
2111
1554
|
try {
|
|
@@ -2117,7 +1560,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2117
1560
|
},
|
|
2118
1561
|
credentials: 'include',
|
|
2119
1562
|
body: JSON.stringify({
|
|
2120
|
-
workspaceId:
|
|
1563
|
+
workspaceId: requestedWorkspaceId && requestedWorkspaceId !== 'default'
|
|
1564
|
+
? requestedWorkspaceId
|
|
1565
|
+
: undefined,
|
|
2121
1566
|
name,
|
|
2122
1567
|
description: typeof input.description === 'string' ? input.description : undefined,
|
|
2123
1568
|
allowCreate: true
|
|
@@ -2180,6 +1625,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2180
1625
|
};
|
|
2181
1626
|
}
|
|
2182
1627
|
if (workspaceResolution.workspaceSetupRequired) {
|
|
1628
|
+
await fetchConfig({ ignoreAuthGuard: true });
|
|
2183
1629
|
return { success: true, workspaceSetupRequired: true };
|
|
2184
1630
|
}
|
|
2185
1631
|
await Promise.all([
|
|
@@ -2226,15 +1672,64 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2226
1672
|
});
|
|
2227
1673
|
const data = await res.json().catch(() => ({}));
|
|
2228
1674
|
if (!res.ok || !data?.success) {
|
|
2229
|
-
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
|
+
};
|
|
2230
1725
|
}
|
|
2231
1726
|
return {
|
|
2232
1727
|
success: true,
|
|
2233
1728
|
workspaceSetupRequired: Boolean(data?.workspaceSetupRequired),
|
|
2234
|
-
planSelectionRequired
|
|
2235
|
-
checkoutPending
|
|
2236
|
-
commercialState
|
|
2237
|
-
verificationRequired
|
|
1729
|
+
planSelectionRequired,
|
|
1730
|
+
checkoutPending,
|
|
1731
|
+
commercialState,
|
|
1732
|
+
verificationRequired,
|
|
2238
1733
|
verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
|
|
2239
1734
|
emailSent: data?.emailSent !== false,
|
|
2240
1735
|
emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
|
|
@@ -2243,7 +1738,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2243
1738
|
catch {
|
|
2244
1739
|
return { success: false, error: 'Create account failed.' };
|
|
2245
1740
|
}
|
|
2246
|
-
}, [resolveCloudAuthUrl, cloudAuthConfigured]);
|
|
1741
|
+
}, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
|
|
2247
1742
|
const updateCurrentUserProfile = useCallback(async (input) => {
|
|
2248
1743
|
if (!cloudAuthConfigured) {
|
|
2249
1744
|
return { success: false, error: 'Cloud auth endpoint is not configured.' };
|
|
@@ -2516,6 +2011,34 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2516
2011
|
return { success: false, error: 'Invite join failed.' };
|
|
2517
2012
|
}
|
|
2518
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]);
|
|
2519
2042
|
const logout = useCallback(async () => {
|
|
2520
2043
|
try {
|
|
2521
2044
|
if (cloudAuthConfigured) {
|
|
@@ -2529,6 +2052,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2529
2052
|
// no-op
|
|
2530
2053
|
}
|
|
2531
2054
|
finally {
|
|
2055
|
+
abortWorkspaceScopedRequests();
|
|
2056
|
+
authSessionEpochRef.current += 1;
|
|
2532
2057
|
authSessionRequestRef.current = null;
|
|
2533
2058
|
authSessionLastResultRef.current = false;
|
|
2534
2059
|
authSessionLastCheckedAtRef.current = Date.now();
|
|
@@ -2543,19 +2068,18 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2543
2068
|
setUserGlobalSyncStatus('disconnected');
|
|
2544
2069
|
setUserGlobalSyncError(null);
|
|
2545
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
|
+
}
|
|
2546
2081
|
setAuthBlocked(runtimeMode === 'cloud' && authRequiredForApi);
|
|
2547
|
-
|
|
2548
|
-
const existing = String(prev || '').trim();
|
|
2549
|
-
let resolvedWorkspaceId = 'default';
|
|
2550
|
-
if (existing && existing.toLowerCase() !== 'default') {
|
|
2551
|
-
resolvedWorkspaceId = existing;
|
|
2552
|
-
}
|
|
2553
|
-
else if (runtimeMode !== 'local') {
|
|
2554
|
-
resolvedWorkspaceId = readPersistedWorkspaceId(workspaceSelectionScope) || 'default';
|
|
2555
|
-
}
|
|
2556
|
-
currentWorkspaceIdRef.current = resolvedWorkspaceId;
|
|
2557
|
-
return resolvedWorkspaceId;
|
|
2558
|
-
});
|
|
2082
|
+
commitCurrentWorkspaceId('default');
|
|
2559
2083
|
setAvailableWorkspaces([]);
|
|
2560
2084
|
workspaceListHydratedRef.current = false;
|
|
2561
2085
|
setHydratedWorkspaceRole(null);
|
|
@@ -2568,12 +2092,14 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2568
2092
|
]);
|
|
2569
2093
|
return;
|
|
2570
2094
|
}
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
setWorkstreams([]);
|
|
2095
|
+
hasBootstrappedDataRef.current = false;
|
|
2096
|
+
resetCloudLogoutClientState();
|
|
2574
2097
|
}
|
|
2575
2098
|
}, [
|
|
2099
|
+
abortWorkspaceScopedRequests,
|
|
2576
2100
|
authRequiredForApi,
|
|
2101
|
+
clearPersistedWorkspaceId,
|
|
2102
|
+
commitCurrentWorkspaceId,
|
|
2577
2103
|
resolveCloudAuthUrl,
|
|
2578
2104
|
cloudAuthConfigured,
|
|
2579
2105
|
workspaceSelectionScope,
|
|
@@ -2581,7 +2107,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2581
2107
|
fetchConfig,
|
|
2582
2108
|
fetchPlanningEntities,
|
|
2583
2109
|
fetchReferenceData,
|
|
2584
|
-
fetchTasks
|
|
2110
|
+
fetchTasks,
|
|
2111
|
+
resetCloudLogoutClientState
|
|
2585
2112
|
]);
|
|
2586
2113
|
const createInitiativeFromTemplate = useCallback(async (payload) => {
|
|
2587
2114
|
try {
|
|
@@ -2699,7 +2226,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2699
2226
|
}, [fetchPlanningEntities]);
|
|
2700
2227
|
// Initialize auth once, then bootstrap protected data once auth gates are settled.
|
|
2701
2228
|
const hasInitializedAuthRef = useRef(false);
|
|
2702
|
-
const hasBootstrappedDataRef = useRef(false);
|
|
2703
2229
|
useEffect(() => {
|
|
2704
2230
|
setIsOpen(true); // Always open in standalone-first
|
|
2705
2231
|
if (hasInitializedAuthRef.current)
|
|
@@ -2958,73 +2484,19 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2958
2484
|
userId: normalizedRealtimeUserId || undefined
|
|
2959
2485
|
});
|
|
2960
2486
|
// Fallback sync for cross-process writes (e.g., MCP tools) that may miss watcher/websocket events.
|
|
2961
|
-
|
|
2962
|
-
if (typeof window === 'undefined' || typeof document === 'undefined')
|
|
2963
|
-
return;
|
|
2964
|
-
if (shouldSkipDataVersionPolling({
|
|
2965
|
-
isOpen,
|
|
2966
|
-
authBlocked,
|
|
2967
|
-
authRequiredForApi,
|
|
2968
|
-
isAuthenticated,
|
|
2969
|
-
canCallProtectedApi: authGuardPolicy.canCallProtectedApi
|
|
2970
|
-
}))
|
|
2971
|
-
return;
|
|
2972
|
-
let cancelled = false;
|
|
2973
|
-
const checkDataVersion = async () => {
|
|
2974
|
-
if (cancelled || document.visibilityState !== 'visible')
|
|
2975
|
-
return;
|
|
2976
|
-
if (shouldSkipDataVersionTick({
|
|
2977
|
-
shouldGateProtectedApiCalls,
|
|
2978
|
-
authSessionResolved: authSessionResolvedRef.current,
|
|
2979
|
-
authRequiredForApi: authRequiredForApiRef.current,
|
|
2980
|
-
isAuthenticated: isAuthenticatedRef.current
|
|
2981
|
-
}))
|
|
2982
|
-
return;
|
|
2983
|
-
try {
|
|
2984
|
-
const res = await fetch('/api/taskforce/data-version');
|
|
2985
|
-
if (!res.ok)
|
|
2986
|
-
return;
|
|
2987
|
-
const data = await res.json();
|
|
2988
|
-
const version = Number(data?.dataVersion);
|
|
2989
|
-
if (!Number.isFinite(version))
|
|
2990
|
-
return;
|
|
2991
|
-
if (dataVersionRef.current === null) {
|
|
2992
|
-
dataVersionRef.current = version;
|
|
2993
|
-
return;
|
|
2994
|
-
}
|
|
2995
|
-
if (version !== dataVersionRef.current) {
|
|
2996
|
-
dataVersionRef.current = version;
|
|
2997
|
-
await refreshTaskCollectionsForDataVersion();
|
|
2998
|
-
}
|
|
2999
|
-
}
|
|
3000
|
-
catch {
|
|
3001
|
-
// Keep polling best-effort and silent.
|
|
3002
|
-
}
|
|
3003
|
-
};
|
|
3004
|
-
const onVisibilityChange = () => {
|
|
3005
|
-
if (document.visibilityState === 'visible') {
|
|
3006
|
-
checkDataVersion();
|
|
3007
|
-
}
|
|
3008
|
-
};
|
|
3009
|
-
const intervalId = window.setInterval(checkDataVersion, 3000);
|
|
3010
|
-
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
3011
|
-
checkDataVersion();
|
|
3012
|
-
return () => {
|
|
3013
|
-
cancelled = true;
|
|
3014
|
-
window.clearInterval(intervalId);
|
|
3015
|
-
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
3016
|
-
};
|
|
3017
|
-
}, [
|
|
2487
|
+
useDataVersionRefresh({
|
|
3018
2488
|
isOpen,
|
|
3019
|
-
fetchTasks,
|
|
3020
|
-
fetchArchive,
|
|
3021
2489
|
authBlocked,
|
|
3022
2490
|
authRequiredForApi,
|
|
3023
2491
|
isAuthenticated,
|
|
3024
|
-
authGuardPolicy,
|
|
2492
|
+
canCallProtectedApi: authGuardPolicy.canCallProtectedApi,
|
|
3025
2493
|
shouldGateProtectedApiCalls,
|
|
2494
|
+
authSessionResolvedRef,
|
|
2495
|
+
authRequiredForApiRef,
|
|
2496
|
+
isAuthenticatedRef,
|
|
2497
|
+
dataVersionRef,
|
|
3026
2498
|
refreshTaskCollectionsForDataVersion
|
|
3027
|
-
|
|
2499
|
+
});
|
|
3028
2500
|
const relationshipTasks = useMemo(() => {
|
|
3029
2501
|
const byId = new Map();
|
|
3030
2502
|
for (const task of archivedTasks)
|
|
@@ -3548,6 +3020,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
3548
3020
|
runtimeCapabilities,
|
|
3549
3021
|
refreshSetupContext,
|
|
3550
3022
|
retryBootstrapChecks,
|
|
3023
|
+
continueAfterCommercialOnboarding,
|
|
3551
3024
|
saveWorkspaceProfile,
|
|
3552
3025
|
fetchWorkspaces,
|
|
3553
3026
|
createWorkspace,
|