@taskforcehq/taskforce 0.3.168

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.
Files changed (492) hide show
  1. package/README.md +241 -0
  2. package/dist/Taskforce.d.ts +3 -0
  3. package/dist/Taskforce.js +3 -0
  4. package/dist/Taskforce.module.css +7587 -0
  5. package/dist/TaskforceCore.CategoryFallback.test.d.ts +1 -0
  6. package/dist/TaskforceCore.CategoryFallback.test.js +98 -0
  7. package/dist/TaskforceCore.d.ts +12 -0
  8. package/dist/TaskforceCore.js +729 -0
  9. package/dist/TaskforceCore.test.d.ts +1 -0
  10. package/dist/TaskforceCore.test.js +2132 -0
  11. package/dist/TaskforceScreenshot.d.ts +11 -0
  12. package/dist/TaskforceScreenshot.js +113 -0
  13. package/dist/TaskforceWidget.d.ts +6 -0
  14. package/dist/TaskforceWidget.js +101 -0
  15. package/dist/TaskforceWidget.test.d.ts +1 -0
  16. package/dist/TaskforceWidget.test.js +92 -0
  17. package/dist/UnsavedChangesRelaxed.test.d.ts +1 -0
  18. package/dist/UnsavedChangesRelaxed.test.js +16 -0
  19. package/dist/__tests__/localizationCatalogs.test.d.ts +1 -0
  20. package/dist/__tests__/localizationCatalogs.test.js +37 -0
  21. package/dist/__tests__/planParser.test.d.ts +1 -0
  22. package/dist/__tests__/planParser.test.js +183 -0
  23. package/dist/__tests__/releaseVersion.test.d.ts +1 -0
  24. package/dist/__tests__/releaseVersion.test.js +32 -0
  25. package/dist/__tests__/structuredCommentValidation.test.d.ts +1 -0
  26. package/dist/__tests__/structuredCommentValidation.test.js +31 -0
  27. package/dist/__tests__/structuredDocValidation.test.d.ts +1 -0
  28. package/dist/__tests__/structuredDocValidation.test.js +30 -0
  29. package/dist/__tests__/templateEngine.test.d.ts +1 -0
  30. package/dist/__tests__/templateEngine.test.js +150 -0
  31. package/dist/__tests__/templates.test.d.ts +1 -0
  32. package/dist/__tests__/templates.test.js +45 -0
  33. package/dist/__tests__/workflowDocumentFixtures.test.d.ts +1 -0
  34. package/dist/__tests__/workflowDocumentFixtures.test.js +78 -0
  35. package/dist/__tests__/workflowExportMatrix.test.d.ts +1 -0
  36. package/dist/__tests__/workflowExportMatrix.test.js +113 -0
  37. package/dist/__tests__/workflowExportSnapshots.test.d.ts +1 -0
  38. package/dist/__tests__/workflowExportSnapshots.test.js +28 -0
  39. package/dist/__tests__/workflowGeneration.test.d.ts +1 -0
  40. package/dist/__tests__/workflowGeneration.test.js +12 -0
  41. package/dist/__tests__/workflowSimulation.test.d.ts +1 -0
  42. package/dist/__tests__/workflowSimulation.test.js +113 -0
  43. package/dist/__tests__/workflowV2Policy.test.d.ts +1 -0
  44. package/dist/__tests__/workflowV2Policy.test.js +80 -0
  45. package/dist/api.d.ts +30 -0
  46. package/dist/api.js +85 -0
  47. package/dist/cli.d.ts +2 -0
  48. package/dist/cli.js +217 -0
  49. package/dist/compat/priorityCompat.d.ts +2 -0
  50. package/dist/compat/priorityCompat.js +24 -0
  51. package/dist/components/features/DocumentGenerateModal.d.ts +9 -0
  52. package/dist/components/features/DocumentGenerateModal.js +114 -0
  53. package/dist/components/features/DocumentIndex.d.ts +19 -0
  54. package/dist/components/features/DocumentIndex.js +51 -0
  55. package/dist/components/features/DocumentViewer.d.ts +9 -0
  56. package/dist/components/features/DocumentViewer.js +253 -0
  57. package/dist/components/features/DocumentWorkspace.d.ts +21 -0
  58. package/dist/components/features/DocumentWorkspace.js +88 -0
  59. package/dist/components/features/TaskSettings.d.ts +209 -0
  60. package/dist/components/features/TaskSettings.js +500 -0
  61. package/dist/components/features/TaskSettings.test.d.ts +1 -0
  62. package/dist/components/features/TaskSettings.test.js +308 -0
  63. package/dist/components/features/settings/CategoryManager.d.ts +19 -0
  64. package/dist/components/features/settings/CategoryManager.js +113 -0
  65. package/dist/components/features/settings/TaxonomyEditor.d.ts +8 -0
  66. package/dist/components/features/settings/TaxonomyEditor.js +98 -0
  67. package/dist/components/features/settings/TaxonomyManager 2.d.ts +52 -0
  68. package/dist/components/features/settings/TaxonomyManager 2.js +75 -0
  69. package/dist/components/features/settings/TaxonomyManager.d.ts +54 -0
  70. package/dist/components/features/settings/TaxonomyManager.js +77 -0
  71. package/dist/components/features/settings/TypeManager.d.ts +10 -0
  72. package/dist/components/features/settings/TypeManager.js +50 -0
  73. package/dist/components/features/settings/WorkflowEditorPrototype.d.ts +15 -0
  74. package/dist/components/features/settings/WorkflowEditorPrototype.js +245 -0
  75. package/dist/components/task/TaskActionHeader.d.ts +20 -0
  76. package/dist/components/task/TaskActionHeader.js +13 -0
  77. package/dist/components/task/TaskCard.d.ts +47 -0
  78. package/dist/components/task/TaskCard.js +326 -0
  79. package/dist/components/task/TaskCard.test.d.ts +1 -0
  80. package/dist/components/task/TaskCard.test.js +95 -0
  81. package/dist/components/task/TaskContextUpload.d.ts +12 -0
  82. package/dist/components/task/TaskContextUpload.js +393 -0
  83. package/dist/components/task/TaskContextUpload.test.d.ts +1 -0
  84. package/dist/components/task/TaskContextUpload.test.js +189 -0
  85. package/dist/components/task/TaskForm.d.ts +105 -0
  86. package/dist/components/task/TaskForm.js +241 -0
  87. package/dist/components/task/TaskForm.test.d.ts +1 -0
  88. package/dist/components/task/TaskForm.test.js +288 -0
  89. package/dist/components/task/TaskKanban 2.d.ts +76 -0
  90. package/dist/components/task/TaskKanban 2.js +1004 -0
  91. package/dist/components/task/TaskKanban.d.ts +76 -0
  92. package/dist/components/task/TaskKanban.empty-column-drop.test.d.ts +1 -0
  93. package/dist/components/task/TaskKanban.empty-column-drop.test.js +66 -0
  94. package/dist/components/task/TaskKanban.js +1026 -0
  95. package/dist/components/task/TaskKanban.test.d.ts +1 -0
  96. package/dist/components/task/TaskKanban.test.js +46 -0
  97. package/dist/components/task/TaskList.d.ts +74 -0
  98. package/dist/components/task/TaskList.js +38 -0
  99. package/dist/components/task/TaskList.test.d.ts +1 -0
  100. package/dist/components/task/TaskList.test.js +270 -0
  101. package/dist/components/task/TaskScreenshotSection.d.ts +15 -0
  102. package/dist/components/task/TaskScreenshotSection.js +22 -0
  103. package/dist/components/ui/LevelSelector.d.ts +13 -0
  104. package/dist/components/ui/LevelSelector.js +25 -0
  105. package/dist/components/ui/LevelSelector.test.d.ts +1 -0
  106. package/dist/components/ui/LevelSelector.test.js +55 -0
  107. package/dist/components/ui/Markdown.d.ts +6 -0
  108. package/dist/components/ui/Markdown.js +94 -0
  109. package/dist/components/ui/Markdown.test.d.ts +1 -0
  110. package/dist/components/ui/Markdown.test.js +38 -0
  111. package/dist/components/ui/Modal.d.ts +16 -0
  112. package/dist/components/ui/Modal.js +49 -0
  113. package/dist/components/ui/MultiSelectFilter.d.ts +10 -0
  114. package/dist/components/ui/MultiSelectFilter.js +71 -0
  115. package/dist/components/ui/MultiSelectFilter.test.d.ts +1 -0
  116. package/dist/components/ui/MultiSelectFilter.test.js +74 -0
  117. package/dist/components/ui/TaxonomyDropdown.d.ts +17 -0
  118. package/dist/components/ui/TaxonomyDropdown.js +145 -0
  119. package/dist/components/ui/TaxonomyDropdown.test.d.ts +1 -0
  120. package/dist/components/ui/TaxonomyDropdown.test.js +130 -0
  121. package/dist/components/views/StandaloneLayout 2.d.ts +8 -0
  122. package/dist/components/views/StandaloneLayout 2.js +1660 -0
  123. package/dist/components/views/StandaloneLayout.d.ts +8 -0
  124. package/dist/components/views/StandaloneLayout.js +2117 -0
  125. package/dist/components/views/WidgetView.d.ts +8 -0
  126. package/dist/components/views/WidgetView.js +221 -0
  127. package/dist/config/envSchema.d.ts +28 -0
  128. package/dist/config/envSchema.js +374 -0
  129. package/dist/config/envSchema.test.d.ts +1 -0
  130. package/dist/config/envSchema.test.js +97 -0
  131. package/dist/config/surfaceBase.d.ts +2 -0
  132. package/dist/config/surfaceBase.js +8 -0
  133. package/dist/core/BulkUpdateFieldsPreview.test.d.ts +1 -0
  134. package/dist/core/BulkUpdateFieldsPreview.test.js +62 -0
  135. package/dist/core/CompletedAt.test.d.ts +1 -0
  136. package/dist/core/CompletedAt.test.js +44 -0
  137. package/dist/core/CreatedBy.test.d.ts +1 -0
  138. package/dist/core/CreatedBy.test.js +60 -0
  139. package/dist/core/EntitlementsPolicy.test.d.ts +1 -0
  140. package/dist/core/EntitlementsPolicy.test.js +145 -0
  141. package/dist/core/InitiativeTemplates.test.d.ts +1 -0
  142. package/dist/core/InitiativeTemplates.test.js +50 -0
  143. package/dist/core/ParentLifecycle.test.d.ts +1 -0
  144. package/dist/core/ParentLifecycle.test.js +98 -0
  145. package/dist/core/ScheduleFields.test.d.ts +1 -0
  146. package/dist/core/ScheduleFields.test.js +197 -0
  147. package/dist/core/SetupState.test.d.ts +1 -0
  148. package/dist/core/SetupState.test.js +113 -0
  149. package/dist/core/SystemAdmin.test.d.ts +1 -0
  150. package/dist/core/SystemAdmin.test.js +295 -0
  151. package/dist/core/TaskTaxonomyValidation.test.d.ts +1 -0
  152. package/dist/core/TaskTaxonomyValidation.test.js +64 -0
  153. package/dist/core/Taskforce.d.ts +1063 -0
  154. package/dist/core/Taskforce.ids.test.d.ts +1 -0
  155. package/dist/core/Taskforce.ids.test.js +168 -0
  156. package/dist/core/Taskforce.js +5981 -0
  157. package/dist/core/Taskforce.realtimeMutation.test.d.ts +1 -0
  158. package/dist/core/Taskforce.realtimeMutation.test.js +76 -0
  159. package/dist/core/WorkspaceDelete.test.d.ts +1 -0
  160. package/dist/core/WorkspaceDelete.test.js +140 -0
  161. package/dist/core/WorkspacePermissions.test.d.ts +1 -0
  162. package/dist/core/WorkspacePermissions.test.js +151 -0
  163. package/dist/core/WorkspacePlanMode.test.d.ts +1 -0
  164. package/dist/core/WorkspacePlanMode.test.js +104 -0
  165. package/dist/hooks/authGuardPolicy.d.ts +26 -0
  166. package/dist/hooks/authGuardPolicy.js +32 -0
  167. package/dist/hooks/authGuardPolicy.test.d.ts +1 -0
  168. package/dist/hooks/authGuardPolicy.test.js +76 -0
  169. package/dist/hooks/useDraggable.d.ts +12 -0
  170. package/dist/hooks/useDraggable.js +65 -0
  171. package/dist/hooks/useRealtimeSync.d.ts +24 -0
  172. package/dist/hooks/useRealtimeSync.js +275 -0
  173. package/dist/hooks/useRealtimeSync.test.d.ts +1 -0
  174. package/dist/hooks/useRealtimeSync.test.js +284 -0
  175. package/dist/hooks/useSyncOrchestrator.d.ts +164 -0
  176. package/dist/hooks/useSyncOrchestrator.js +1427 -0
  177. package/dist/hooks/useSyncOrchestrator.retry-closure.test.d.ts +1 -0
  178. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +367 -0
  179. package/dist/hooks/useTaskforce.d.ts +727 -0
  180. package/dist/hooks/useTaskforce.js +4396 -0
  181. package/dist/hooks/useTaskforce.priority-filters.test.d.ts +1 -0
  182. package/dist/hooks/useTaskforce.priority-filters.test.js +33 -0
  183. package/dist/hooks/useWorkspaceSyncController.d.ts +80 -0
  184. package/dist/hooks/useWorkspaceSyncController.js +598 -0
  185. package/dist/hooks/useWorkspaceSyncController.test.d.ts +1 -0
  186. package/dist/hooks/useWorkspaceSyncController.test.js +236 -0
  187. package/dist/index.d.ts +12 -0
  188. package/dist/index.js +11 -0
  189. package/dist/localization/index.d.ts +8 -0
  190. package/dist/localization/index.js +81 -0
  191. package/dist/localization/locales/en-US.d.ts +151 -0
  192. package/dist/localization/locales/en-US.js +150 -0
  193. package/dist/localization/locales/es-419.d.ts +2 -0
  194. package/dist/localization/locales/es-419.js +150 -0
  195. package/dist/localization/locales/pt-BR.d.ts +2 -0
  196. package/dist/localization/locales/pt-BR.js +150 -0
  197. package/dist/main.d.ts +1 -0
  198. package/dist/main.js +33 -0
  199. package/dist/mcp/optionResolution.d.ts +14 -0
  200. package/dist/mcp/optionResolution.js +126 -0
  201. package/dist/mcp/optionResolution.test.d.ts +1 -0
  202. package/dist/mcp/optionResolution.test.js +39 -0
  203. package/dist/mcp/server.d.ts +1 -0
  204. package/dist/mcp/server.js +1793 -0
  205. package/dist/mcp/structuredCommentValidation.d.ts +6 -0
  206. package/dist/mcp/structuredCommentValidation.js +56 -0
  207. package/dist/mcp/structuredDocValidation.d.ts +6 -0
  208. package/dist/mcp/structuredDocValidation.js +75 -0
  209. package/dist/migrations/billingSchemaParity.test.d.ts +1 -0
  210. package/dist/migrations/billingSchemaParity.test.js +46 -0
  211. package/dist/migrations/clientMigrations.d.ts +9 -0
  212. package/dist/migrations/clientMigrations.js +74 -0
  213. package/dist/migrations/flags.d.ts +6 -0
  214. package/dist/migrations/flags.js +8 -0
  215. package/dist/migrations/taskMigrations.d.ts +71 -0
  216. package/dist/migrations/taskMigrations.js +236 -0
  217. package/dist/migrations/taskMigrations.test.d.ts +1 -0
  218. package/dist/migrations/taskMigrations.test.js +34 -0
  219. package/dist/migrations/taskSchemaMigrations.d.ts +11 -0
  220. package/dist/migrations/taskSchemaMigrations.js +453 -0
  221. package/dist/plugins/index.d.ts +10 -0
  222. package/dist/plugins/index.js +10 -0
  223. package/dist/plugins/vite.d.ts +19 -0
  224. package/dist/plugins/vite.js +271 -0
  225. package/dist/public/android-chrome-192x192.png +0 -0
  226. package/dist/public/android-chrome-512x512.png +0 -0
  227. package/dist/public/apple-touch-icon.png +0 -0
  228. package/dist/public/favicon-16x16.png +0 -0
  229. package/dist/public/favicon-32x32.png +0 -0
  230. package/dist/public/favicon.ico +0 -0
  231. package/dist/public/favicon.png +0 -0
  232. package/dist/public/site.webmanifest +1 -0
  233. package/dist/public/taskforce.png +0 -0
  234. package/dist/resources/templates/WORKFLOW_V2_POLICY.md +55 -0
  235. package/dist/resources/templates/environments/aider.yaml +14 -0
  236. package/dist/resources/templates/environments/antigravity.yaml +10 -0
  237. package/dist/resources/templates/environments/claude.yaml +10 -0
  238. package/dist/resources/templates/environments/cline.yaml +10 -0
  239. package/dist/resources/templates/environments/codex.yaml +13 -0
  240. package/dist/resources/templates/environments/copilot.yaml +10 -0
  241. package/dist/resources/templates/environments/cursor.yaml +9 -0
  242. package/dist/resources/templates/environments/gemini.yaml +9 -0
  243. package/dist/resources/templates/environments/windsurf.yaml +10 -0
  244. package/dist/resources/templates/workflow-sources/README.md +9 -0
  245. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +520 -0
  246. package/dist/resources/templates/workflow-sources/workflowEditorOverrides.json +1 -0
  247. package/dist/resources/templates/workflows/collaborate.yaml +104 -0
  248. package/dist/resources/templates/workflows/do.yaml +82 -0
  249. package/dist/resources/templates/workflows/evaluate.yaml +111 -0
  250. package/dist/resources/templates/workflows/execute.yaml +142 -0
  251. package/dist/resources/templates/workflows/plan.yaml +112 -0
  252. package/dist/resources/templates/workflows/review.yaml +109 -0
  253. package/dist/runtime/capabilities.d.ts +8 -0
  254. package/dist/runtime/capabilities.js +16 -0
  255. package/dist/security/virusScan.d.ts +16 -0
  256. package/dist/security/virusScan.js +83 -0
  257. package/dist/server/auth.d.ts +51 -0
  258. package/dist/server/auth.js +266 -0
  259. package/dist/server/auth.test.d.ts +1 -0
  260. package/dist/server/auth.test.js +111 -0
  261. package/dist/server/buildInfo.d.ts +7 -0
  262. package/dist/server/buildInfo.js +100 -0
  263. package/dist/server/buildInfo.test.d.ts +1 -0
  264. package/dist/server/buildInfo.test.js +29 -0
  265. package/dist/server/cors.d.ts +7 -0
  266. package/dist/server/cors.js +76 -0
  267. package/dist/server/cors.test.d.ts +1 -0
  268. package/dist/server/cors.test.js +71 -0
  269. package/dist/server/email.d.ts +28 -0
  270. package/dist/server/email.js +138 -0
  271. package/dist/server/email.test.d.ts +1 -0
  272. package/dist/server/email.test.js +113 -0
  273. package/dist/server/index.cookieProxy.test.d.ts +1 -0
  274. package/dist/server/index.cookieProxy.test.js +19 -0
  275. package/dist/server/index.d.ts +17 -0
  276. package/dist/server/index.js +757 -0
  277. package/dist/server/index.rateLimit.test.d.ts +1 -0
  278. package/dist/server/index.rateLimit.test.js +13 -0
  279. package/dist/server/index.test.d.ts +1 -0
  280. package/dist/server/index.test.js +19 -0
  281. package/dist/server/rateLimit.d.ts +29 -0
  282. package/dist/server/rateLimit.js +99 -0
  283. package/dist/server/rateLimit.test.d.ts +1 -0
  284. package/dist/server/rateLimit.test.js +107 -0
  285. package/dist/server/realtimeSync.auth.test.d.ts +1 -0
  286. package/dist/server/realtimeSync.auth.test.js +62 -0
  287. package/dist/server/realtimeSync.replay.test.d.ts +1 -0
  288. package/dist/server/realtimeSync.replay.test.js +192 -0
  289. package/dist/server/realtimeSync.workspaceIsolation.test.d.ts +1 -0
  290. package/dist/server/realtimeSync.workspaceIsolation.test.js +101 -0
  291. package/dist/server/realtimeSyncWs.d.ts +30 -0
  292. package/dist/server/realtimeSyncWs.js +257 -0
  293. package/dist/server/routes.billing.d.ts +15 -0
  294. package/dist/server/routes.billing.js +991 -0
  295. package/dist/server/routes.billing.test.d.ts +1 -0
  296. package/dist/server/routes.billing.test.js +387 -0
  297. package/dist/server/routes.d.ts +72 -0
  298. package/dist/server/routes.js +4820 -0
  299. package/dist/server/routes.sync.d.ts +17 -0
  300. package/dist/server/routes.sync.integration.test.d.ts +1 -0
  301. package/dist/server/routes.sync.integration.test.js +470 -0
  302. package/dist/server/routes.sync.js +1141 -0
  303. package/dist/server/routes.test.d.ts +1 -0
  304. package/dist/server/routes.test.js +4945 -0
  305. package/dist/server/runtimeMode.d.ts +4 -0
  306. package/dist/server/runtimeMode.js +25 -0
  307. package/dist/server/runtimeMode.test.d.ts +1 -0
  308. package/dist/server/runtimeMode.test.js +26 -0
  309. package/dist/server/securityHeaders.d.ts +3 -0
  310. package/dist/server/securityHeaders.js +25 -0
  311. package/dist/server/securityHeaders.test.d.ts +1 -0
  312. package/dist/server/securityHeaders.test.js +32 -0
  313. package/dist/server/smtpTransport.d.ts +21 -0
  314. package/dist/server/smtpTransport.js +11 -0
  315. package/dist/server/start.d.ts +1 -0
  316. package/dist/server/start.js +17 -0
  317. package/dist/services/formatTransformers.d.ts +29 -0
  318. package/dist/services/formatTransformers.js +56 -0
  319. package/dist/services/templateEngine.d.ts +46 -0
  320. package/dist/services/templateEngine.js +148 -0
  321. package/dist/shared/runtimeContract.d.ts +12 -0
  322. package/dist/shared/runtimeContract.js +23 -0
  323. package/dist/shared/runtimeContract.test.d.ts +1 -0
  324. package/dist/shared/runtimeContract.test.js +23 -0
  325. package/dist/soak/preflight.cli.d.ts +1 -0
  326. package/dist/soak/preflight.cli.js +28 -0
  327. package/dist/soak/preflight.d.ts +26 -0
  328. package/dist/soak/preflight.js +113 -0
  329. package/dist/soak/preflight.test.d.ts +1 -0
  330. package/dist/soak/preflight.test.js +39 -0
  331. package/dist/soak/syncSoakHarness.d.ts +6 -0
  332. package/dist/soak/syncSoakHarness.js +10 -0
  333. package/dist/storage/createDatabaseAdapter.d.ts +2 -0
  334. package/dist/storage/createDatabaseAdapter.js +9 -0
  335. package/dist/storage/databaseAdapter.d.ts +22 -0
  336. package/dist/storage/databaseAdapter.js +1 -0
  337. package/dist/storage/documentIntegrity.d.ts +28 -0
  338. package/dist/storage/documentIntegrity.js +103 -0
  339. package/dist/storage/documentPurge.d.ts +14 -0
  340. package/dist/storage/documentPurge.js +46 -0
  341. package/dist/storage/documentRegistry.d.ts +39 -0
  342. package/dist/storage/documentRegistry.js +115 -0
  343. package/dist/storage/integritySchedule.d.ts +17 -0
  344. package/dist/storage/integritySchedule.js +62 -0
  345. package/dist/storage/objectStorage.d.ts +20 -0
  346. package/dist/storage/objectStorage.js +15 -0
  347. package/dist/storage/objectStorageClient.d.ts +18 -0
  348. package/dist/storage/objectStorageClient.js +301 -0
  349. package/dist/storage/objectStorageConfig.d.ts +2 -0
  350. package/dist/storage/objectStorageConfig.js +45 -0
  351. package/dist/storage/postgresAdapter.d.ts +20 -0
  352. package/dist/storage/postgresAdapter.js +204 -0
  353. package/dist/storage/postgresWorker.d.ts +1 -0
  354. package/dist/storage/postgresWorker.js +110 -0
  355. package/dist/storage/providerConfig.d.ts +6 -0
  356. package/dist/storage/providerConfig.js +11 -0
  357. package/dist/storage/sqliteAdapter.d.ts +14 -0
  358. package/dist/storage/sqliteAdapter.js +28 -0
  359. package/dist/storage/taskAssetStore.d.ts +56 -0
  360. package/dist/storage/taskAssetStore.js +297 -0
  361. package/dist/sync/cloudSyncApi.d.ts +47 -0
  362. package/dist/sync/cloudSyncApi.js +138 -0
  363. package/dist/sync/cloudSyncApi.test.d.ts +1 -0
  364. package/dist/sync/cloudSyncApi.test.js +54 -0
  365. package/dist/sync/encryptionService.d.ts +11 -0
  366. package/dist/sync/encryptionService.js +61 -0
  367. package/dist/sync/encryptionService.test.d.ts +1 -0
  368. package/dist/sync/encryptionService.test.js +52 -0
  369. package/dist/sync/realtimeFeature.d.ts +6 -0
  370. package/dist/sync/realtimeFeature.js +20 -0
  371. package/dist/sync/realtimeFeature.test.d.ts +1 -0
  372. package/dist/sync/realtimeFeature.test.js +30 -0
  373. package/dist/sync/realtimeReconcile.d.ts +30 -0
  374. package/dist/sync/realtimeReconcile.js +90 -0
  375. package/dist/sync/realtimeReconcile.test.d.ts +1 -0
  376. package/dist/sync/realtimeReconcile.test.js +43 -0
  377. package/dist/sync/syncService.d.ts +96 -0
  378. package/dist/sync/syncService.js +423 -0
  379. package/dist/sync/syncService.test.d.ts +1 -0
  380. package/dist/sync/syncService.test.js +484 -0
  381. package/dist/sync/workspaceSyncModel.d.ts +61 -0
  382. package/dist/sync/workspaceSyncModel.js +136 -0
  383. package/dist/sync/workspaceSyncModel.test.d.ts +1 -0
  384. package/dist/sync/workspaceSyncModel.test.js +73 -0
  385. package/dist/test/setup.d.ts +1 -0
  386. package/dist/test/setup.js +15 -0
  387. package/dist/types.d.ts +157 -0
  388. package/dist/types.js +41 -0
  389. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  390. package/dist/ui/assets/index-0W5vyCpn.css +1 -0
  391. package/dist/ui/assets/index-MCZuNNfn.js +23 -0
  392. package/dist/ui/assets/logo_white_01-0Vx-iIIv.png +0 -0
  393. package/dist/ui/assets/taskforce-BxLPokNB.png +0 -0
  394. package/dist/ui/assets/vendor-dnd-BI1K77C1.js +5 -0
  395. package/dist/ui/assets/vendor-icons-ZFG3SGwo.js +1 -0
  396. package/dist/ui/assets/vendor-markdown-CKYkQHfj.js +29 -0
  397. package/dist/ui/assets/vendor-react-DdIDsh7f.js +9 -0
  398. package/dist/ui/assets/vendor-router-BtiNU7Dl.js +3 -0
  399. package/dist/ui/branding/logos/logo_black_01.png +0 -0
  400. package/dist/ui/branding/logos/logo_black_02.png +0 -0
  401. package/dist/ui/branding/logos/logo_white_01.png +0 -0
  402. package/dist/ui/branding/logos/logo_white_02.png +0 -0
  403. package/dist/ui/favicon/android-chrome-192x192.png +0 -0
  404. package/dist/ui/favicon/android-chrome-512x512.png +0 -0
  405. package/dist/ui/favicon/apple-touch-icon.png +0 -0
  406. package/dist/ui/favicon/favicon-16x16.png +0 -0
  407. package/dist/ui/favicon/favicon-32x32.png +0 -0
  408. package/dist/ui/favicon/favicon.ico +0 -0
  409. package/dist/ui/favicon/favicon.png +0 -0
  410. package/dist/ui/favicon/site.webmanifest +1 -0
  411. package/dist/ui/fonts/RussoOne-Regular.ttf +0 -0
  412. package/dist/ui/images/taskforce.png +0 -0
  413. package/dist/ui/index.html +19 -0
  414. package/dist/utils/checklistMarkdown.d.ts +9 -0
  415. package/dist/utils/checklistMarkdown.js +46 -0
  416. package/dist/utils/constants.d.ts +21 -0
  417. package/dist/utils/constants.icons.test.d.ts +1 -0
  418. package/dist/utils/constants.icons.test.js +40 -0
  419. package/dist/utils/constants.js +100 -0
  420. package/dist/utils/contextFiles.d.ts +8 -0
  421. package/dist/utils/contextFiles.js +54 -0
  422. package/dist/utils/formatting.d.ts +5 -0
  423. package/dist/utils/formatting.js +30 -0
  424. package/dist/utils/planParser.d.ts +22 -0
  425. package/dist/utils/planParser.js +151 -0
  426. package/dist/utils/taskDragZones.d.ts +8 -0
  427. package/dist/utils/taskDragZones.js +14 -0
  428. package/dist/utils/taskDragZones.test.d.ts +1 -0
  429. package/dist/utils/taskDragZones.test.js +14 -0
  430. package/dist/utils/taskHierarchy.d.ts +4 -0
  431. package/dist/utils/taskHierarchy.js +69 -0
  432. package/dist/utils/taskHierarchy.test.d.ts +1 -0
  433. package/dist/utils/taskHierarchy.test.js +38 -0
  434. package/dist/utils/taskParenting.d.ts +3 -0
  435. package/dist/utils/taskParenting.js +24 -0
  436. package/dist/utils/taskParenting.test.d.ts +1 -0
  437. package/dist/utils/taskParenting.test.js +21 -0
  438. package/dist/utils/taskRelations.d.ts +6 -0
  439. package/dist/utils/taskRelations.js +24 -0
  440. package/dist/utils/taskRelations.test.d.ts +1 -0
  441. package/dist/utils/taskRelations.test.js +34 -0
  442. package/dist/utils/taskUtils.d.ts +2 -0
  443. package/dist/utils/taskUtils.js +20 -0
  444. package/dist/utils/taskforceApiClient.d.ts +7 -0
  445. package/dist/utils/taskforceApiClient.js +42 -0
  446. package/dist/utils/workspaceIdentity.d.ts +8 -0
  447. package/dist/utils/workspaceIdentity.js +47 -0
  448. package/dist/utils/workspaceIdentity.test.d.ts +1 -0
  449. package/dist/utils/workspaceIdentity.test.js +32 -0
  450. package/dist/utils/workspaceLifecycle.d.ts +26 -0
  451. package/dist/utils/workspaceLifecycle.js +69 -0
  452. package/dist/utils/workspaceLifecycle.test.d.ts +1 -0
  453. package/dist/utils/workspaceLifecycle.test.js +64 -0
  454. package/package.json +133 -0
  455. package/scripts/check-compat-boundaries.js +48 -0
  456. package/scripts/check-env.ts +24 -0
  457. package/scripts/check-localization-strings.mjs +49 -0
  458. package/scripts/check-runtime-boundaries.mjs +44 -0
  459. package/scripts/debug-global.mjs +34 -0
  460. package/scripts/debug-server.mjs +36 -0
  461. package/scripts/export-sqlite-to-postgres.mjs +125 -0
  462. package/scripts/generate-env-example.ts +8 -0
  463. package/scripts/generate-workflow-templates.mjs +134 -0
  464. package/scripts/mcp-stdio-adapter.mjs +160 -0
  465. package/scripts/postinstall.js +60 -0
  466. package/scripts/release-version-lib.mjs +88 -0
  467. package/scripts/release-version.mjs +72 -0
  468. package/scripts/run-dev-proxy.sh +49 -0
  469. package/scripts/run-e2e-realtime.sh +16 -0
  470. package/scripts/run-smoke.sh +23 -0
  471. package/scripts/run-soak.sh +29 -0
  472. package/scripts/test-global.js +143 -0
  473. package/scripts/update-homebrew-formula.sh +38 -0
  474. package/src/resources/templates/WORKFLOW_V2_POLICY.md +55 -0
  475. package/src/resources/templates/environments/aider.yaml +14 -0
  476. package/src/resources/templates/environments/antigravity.yaml +10 -0
  477. package/src/resources/templates/environments/claude.yaml +10 -0
  478. package/src/resources/templates/environments/cline.yaml +10 -0
  479. package/src/resources/templates/environments/codex.yaml +13 -0
  480. package/src/resources/templates/environments/copilot.yaml +10 -0
  481. package/src/resources/templates/environments/cursor.yaml +9 -0
  482. package/src/resources/templates/environments/gemini.yaml +9 -0
  483. package/src/resources/templates/environments/windsurf.yaml +10 -0
  484. package/src/resources/templates/workflow-sources/README.md +9 -0
  485. package/src/resources/templates/workflow-sources/workflowDocs.mjs +520 -0
  486. package/src/resources/templates/workflow-sources/workflowEditorOverrides.json +1 -0
  487. package/src/resources/templates/workflows/collaborate.yaml +104 -0
  488. package/src/resources/templates/workflows/do.yaml +82 -0
  489. package/src/resources/templates/workflows/evaluate.yaml +111 -0
  490. package/src/resources/templates/workflows/execute.yaml +142 -0
  491. package/src/resources/templates/workflows/plan.yaml +112 -0
  492. package/src/resources/templates/workflows/review.yaml +109 -0
@@ -0,0 +1,2117 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useMemo, useCallback, useState, useEffect, useRef } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { useLocation, useNavigate } from 'react-router-dom';
5
+ import { Settings, Plus, Search, Filter, Layers, User, Maximize2, Minimize2, Check, X, Ban, ChevronLeft, ChevronRight, ArrowUp, ArrowDown, ArrowUpDown, AlertTriangle, Save, Loader2, Folder, FileText, Trash2, LogOut, Cloud, CloudOff, RefreshCw, Users, ChevronsUp, ChevronsDown, EyeOff, Zap, HelpCircle, CalendarDays, PanelRightClose, PanelRightOpen } from 'lucide-react';
6
+ import { STATUS_OPTIONS } from '../../utils/constants';
7
+ import { MultiSelectFilter } from '../ui/MultiSelectFilter';
8
+ import styles from '../../Taskforce.module.css';
9
+ import modalStyles from '../ui/Modal.module.css';
10
+ import { Modal } from '../ui/Modal';
11
+ import { TaskForm } from '../task/TaskForm';
12
+ import { TaskSettings } from '../features/TaskSettings';
13
+ import { DocumentWorkspace } from '../features/DocumentWorkspace';
14
+ import { TaskKanban } from '../task/TaskKanban';
15
+ import { TaskActionHeader } from '../task/TaskActionHeader';
16
+ import { TypeMeta } from '../../utils/constants';
17
+ import { buildHierarchyDepthMap, collectSubtreeTaskIds } from '../../utils/taskHierarchy';
18
+ import { formatDate } from '../../utils/formatting';
19
+ import { getLocale, t } from '../../localization';
20
+ import logo from '../../assets/branding/logos/logo_white_01.png';
21
+ import favicon from '../../assets/favicon/favicon.png';
22
+ import taskforceBg from '../../assets/images/taskforce.png';
23
+ const SCHEDULE_DAY_KEYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
24
+ const SCHEDULE_DAY_LABELS = {
25
+ mon: 'Mon',
26
+ tue: 'Tue',
27
+ wed: 'Wed',
28
+ thu: 'Thu',
29
+ fri: 'Fri',
30
+ sat: 'Sat',
31
+ sun: 'Sun'
32
+ };
33
+ const SYNCING_DISPLAY_DELAY_MS = 220;
34
+ const SYNCING_MIN_VISIBLE_MS = 1200;
35
+ const ERROR_MIN_VISIBLE_MS = 1800;
36
+ function toDateOnlyLocal(date) {
37
+ const y = date.getFullYear();
38
+ const m = String(date.getMonth() + 1).padStart(2, '0');
39
+ const d = String(date.getDate()).padStart(2, '0');
40
+ return `${y}-${m}-${d}`;
41
+ }
42
+ function parseDateOnlyLocal(dateOnly) {
43
+ const match = dateOnly.match(/^(\d{4})-(\d{2})-(\d{2})$/);
44
+ if (!match)
45
+ return null;
46
+ const parsed = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
47
+ if (Number.isNaN(parsed.getTime()))
48
+ return null;
49
+ return parsed;
50
+ }
51
+ function startOfWeek(date, weekStartsOn) {
52
+ const day = date.getDay();
53
+ const delta = weekStartsOn === 'sunday'
54
+ ? -day
55
+ : (day === 0 ? -6 : 1 - day);
56
+ const result = new Date(date);
57
+ result.setHours(0, 0, 0, 0);
58
+ result.setDate(result.getDate() + delta);
59
+ return result;
60
+ }
61
+ function getDayOffsetFromWeekStart(dayKey, weekStartsOn) {
62
+ if (weekStartsOn === 'sunday') {
63
+ const sundayFirst = {
64
+ sun: 0,
65
+ mon: 1,
66
+ tue: 2,
67
+ wed: 3,
68
+ thu: 4,
69
+ fri: 5,
70
+ sat: 6
71
+ };
72
+ return sundayFirst[dayKey];
73
+ }
74
+ const mondayFirst = {
75
+ mon: 0,
76
+ tue: 1,
77
+ wed: 2,
78
+ thu: 3,
79
+ fri: 4,
80
+ sat: 5,
81
+ sun: 6
82
+ };
83
+ return mondayFirst[dayKey];
84
+ }
85
+ function addDays(base, days) {
86
+ const next = new Date(base);
87
+ next.setDate(base.getDate() + days);
88
+ return next;
89
+ }
90
+ function dateOnlyToIsoWeekKey(dateOnly) {
91
+ const match = dateOnly.match(/^(\d{4})-(\d{2})-(\d{2})$/);
92
+ if (!match)
93
+ return null;
94
+ const dt = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
95
+ const day = dt.getUTCDay() || 7;
96
+ dt.setUTCDate(dt.getUTCDate() + 4 - day);
97
+ const yearStart = new Date(Date.UTC(dt.getUTCFullYear(), 0, 1));
98
+ const week = Math.ceil((((dt.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
99
+ return `${dt.getUTCFullYear()}-W${String(week).padStart(2, '0')}`;
100
+ }
101
+ export function StandaloneLayout(props) {
102
+ const navigate = useNavigate();
103
+ const location = useLocation();
104
+ const {
105
+ // State
106
+ activeTab, setActiveTab, activeCategories, groupBy, setGroupBy, emptyColumnMode, setEmptyColumnMode, zenMode, setZenMode, filterStatus, setFilterStatus, parentTaskFilterMode, setParentTaskFilterMode,
107
+ // Data & Actions
108
+ tasks, handleEdit, handleSubmit, resetForm, handleUpdateTask,
109
+ // Form State (destructure all needed for TaskForm)
110
+ editingTaskId, loading, error, title, setTitle, description, setDescription, category, setCategory, type, setType, priority, setPriority, complexity, setComplexity, approach, setApproach, assignee, setAssignee, scheduledDate, setScheduledDate, dueDate, setDueDate, parentTaskIdInput, setParentTaskIdInput, addChildTaskIdInput, setAddChildTaskIdInput, manualComplexityEnabled, checklistDropdownEnabled, formTaxonomies, setFormTaxonomies, selectedSpecialists, setSelectedSpecialists, comments, newCommentText, setNewCommentText, screenshots, setScreenshots, descriptionFocused, setDescriptionFocused, showMarkdownHelp, setShowMarkdownHelp, showSpecialists, setShowSpecialists, showChecklist, setShowChecklist, showTaskRelationships, setShowTaskRelationships, showChildTasks, setShowChildTasks, showComments, setShowComments,
111
+ // Form Actions
112
+ handleAddComment, handleSetParentForCurrentTask, handleAddChildToCurrentTask, handleCreateChildForCurrentTask, handleUnlinkChildTask, handleReorderChildTasks, handleClearParentTask, handleOpenTaskById,
113
+ // Other Props for Form
114
+ taxonomies, activeTypes, priorities, approaches, availableSpecialists, copiedId, handleCopyId, handleToggleInProgress, handleToggleComplete, handleToggleReview, handleToggleCancel, handleArchiveTask, handleDelete, handleUnarchive, fetchTasks, searchQuery, setSearchQuery, filterCategories, setFilterCategories, filterTypes, setFilterTypes, filterPriorities, setFilterPriorities, filterAssignees, setFilterAssignees,
115
+ // filterStatus, setFilterStatus, // This was duplicated, removed one instance
116
+ sortBy, setSortBy, clearFilters, // setSearchQuery, // This was duplicated, removed one instance
117
+ // filterCategories, setFilterCategories, // This was duplicated, removed one instance
118
+ // filterTypes, setFilterTypes, // This was duplicated, removed one instance
119
+ // filterPriorities, setFilterPriorities, // This was duplicated, removed one instance
120
+ // filterStatus, setFilterStatus, // This was duplicated, removed one instance
121
+ // sortBy, setSortBy, // This was duplicated, removed one instance
122
+ // clearFilters, // This was duplicated, removed one instance
123
+ // activeTypes, // This was duplicated, removed one instance
124
+ // Settings State & Props
125
+ settingsModel, currentTheme, setCurrentTheme, pathSaved, saveSettings, keyShortcut, setKeyShortcut, globalWeekStartsOn, locale, jsonBackupEnabled, setJsonBackupEnabled, assigneeFilterEnabled, mcpHostRoot, setMcpHostRoot, settingsSection, setSettingsSection, runtimeMode, workspaceSwitchingEnabled, cloudAuthConfigured, authRequiredForApi, authBlocked, isAuthenticated, authUserEmail, authSessionResolved, realtimeSyncEnabled, realtimeSyncFlagSource, workspaceCloudSyncEnabled, realtimeConnectionState, realtimeTelemetry, workspaceSyncSourceOfTruth, workspaceSyncOnboardingCompleted, workspaceSyncReady, workspaceCloudHydrated, saveWorkspaceCloudSyncSettings, userGlobalSyncStatus, userGlobalLastSyncedAt, workspaceLastPullAt, workspaceLastPushAt, workspaceLastErrorAt, workspaceRetryAt, workspaceBootstrapSyncInProgress, workspaceBootstrapSyncPages, workspaceBootstrapSyncAppliedChanges, userGlobalSyncPendingChanges, userGlobalSyncError, workspaceLastErrorMessage, workspaceLastWarningMessage, workspaceSyncFailureCount, workspaceSyncRetryScheduledCount, workspaceSyncAuthFailureCount, workspaceSyncLastFailureSource, workspaceSyncLastFailureStatusCode, retryWorkspaceCloudSync, resetWorkspaceSyncCursorAndPull, currentWorkspaceId, availableWorkspaces, createWorkspace, switchWorkspace, logout, projectRoot, projectName, mcpScriptPath, serverHostRoot,
126
+ // Browser Props
127
+ showFolderBrowser, setShowFolderBrowser, folders, files, currentBrowsePath, fetchFolders, browserTarget, setBrowserTarget, contextLinkBrowsePath, handleSelectPath, handleAddPath, handleRemovePath,
128
+ // Settings Managers
129
+ handleUpdateCategory, handleRemoveCategory, handleSaveCategory, handleUpdateCategoryIcon, handleUpdateCategoryColor, handleSaveType, handleRemoveType, handleUpdateTaxonomies, handleUpdatePriorities, handleUpdateApproaches, pathValidation,
130
+ // Exports
131
+ exportEnvironment, setExportEnvironment, exportWorkflowsPath, setExportWorkflowsPath, exportSpecialistsPath, setExportSpecialistsPath, exportResult, handleExportResources, exportingResource, availableWorkflows, onExportWorkflows, fetchWorkflows, availableEnvironments,
132
+ // Misc
133
+ successBanner, returnToParentTaskId, returnFromChildTaskId, clearReturnToParentTask, tasksScrollRef, setTasksScrollPos,
134
+ // Refs
135
+ commentsEndRef,
136
+ // Modals - Unsaved Changes
137
+ unsavedModalOpen, setUnsavedModalOpen, pendingNavigation, handleNavigation, currentTask, currentTaskParent, currentTaskChildren,
138
+ // Modals - Cancellation
139
+ cancellationPromptTask, cancellationOpenDescendantIds, cancellationMode, setCancellationMode, confirmCancellation, cancelCancellationPrompt, scheduleWarningPrompt, confirmScheduleWarning, cancelScheduleWarning } = props;
140
+ const resolvedLocale = locale || getLocale();
141
+ useEffect(() => {
142
+ const normalizedProject = String(projectName || '').trim();
143
+ document.title = normalizedProject ? `Taskforce - ${normalizedProject}` : 'Taskforce';
144
+ }, [projectName]);
145
+ // Handlers
146
+ const handleDiscard = () => {
147
+ setUnsavedModalOpen(false);
148
+ if (pendingNavigation) {
149
+ if (activeTab === 'add')
150
+ resetForm();
151
+ pendingNavigation();
152
+ }
153
+ else {
154
+ if (activeTab === 'add')
155
+ resetForm();
156
+ setActiveTab('tasks');
157
+ }
158
+ };
159
+ const handleSaveAndNavigate = async () => {
160
+ // Fake event for handleSubmit
161
+ const fakeEvent = { preventDefault: () => { } };
162
+ await handleSubmit(fakeEvent);
163
+ setUnsavedModalOpen(false);
164
+ if (pendingNavigation) {
165
+ pendingNavigation();
166
+ }
167
+ };
168
+ // State for Card Compression
169
+ const [compressedCards, setCompressedCards] = useState(false);
170
+ const [scheduleShowWeekends, setScheduleShowWeekends] = useState(false);
171
+ const [scheduleShowBacklog, setScheduleShowBacklog] = useState(true);
172
+ const [scheduleOnlyExpired, setScheduleOnlyExpired] = useState(false);
173
+ const [scheduleSidebarOpen, setScheduleSidebarOpen] = useState(true);
174
+ const [hierarchySidebarOpen, setHierarchySidebarOpen] = useState(true);
175
+ const [hierarchyRootTaskId, setHierarchyRootTaskId] = useState('');
176
+ const [hierarchyShowUnlinkedColumn, setHierarchyShowUnlinkedColumn] = useState(true);
177
+ const [hierarchyChildOrderMode, setHierarchyChildOrderMode] = useState('manual');
178
+ const [hierarchyRootSearch, setHierarchyRootSearch] = useState('');
179
+ const [scheduleSelectedDate, setScheduleSelectedDate] = useState(toDateOnlyLocal(new Date()));
180
+ const [scheduleCalendarMonth, setScheduleCalendarMonth] = useState(() => {
181
+ const now = new Date();
182
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
183
+ });
184
+ const [scheduleScrollLeft, setScheduleScrollLeft] = useState(0);
185
+ const [showFilters, setShowFilters] = useState(false);
186
+ const [requestedDocPath, setRequestedDocPath] = useState(null);
187
+ const [layoutStateReady, setLayoutStateReady] = useState(false);
188
+ const lastNonDocsGroupByRef = useRef('category');
189
+ const loadedLayoutStateWorkspaceRef = useRef('');
190
+ const workspaceUiStateHeaders = useMemo(() => {
191
+ const headers = {};
192
+ const workspaceIdHeader = String(currentWorkspaceId || '').trim();
193
+ if (runtimeMode === 'cloud' && workspaceIdHeader && workspaceIdHeader !== 'default') {
194
+ headers['x-taskforce-workspace-id'] = workspaceIdHeader;
195
+ }
196
+ return headers;
197
+ }, [runtimeMode, currentWorkspaceId]);
198
+ const layoutStateWorkspaceKey = useMemo(() => `${runtimeMode}:${String(currentWorkspaceId || 'default').trim() || 'default'}`, [runtimeMode, currentWorkspaceId]);
199
+ useEffect(() => {
200
+ const handleOpenMarkdownDocument = (event) => {
201
+ const customEvent = event;
202
+ const fsPath = customEvent.detail?.fsPath;
203
+ if (!fsPath)
204
+ return;
205
+ customEvent.preventDefault();
206
+ setRequestedDocPath(fsPath);
207
+ if (groupBy !== 'docs') {
208
+ setGroupBy('docs');
209
+ }
210
+ };
211
+ window.addEventListener('taskforce:open-markdown-document', handleOpenMarkdownDocument);
212
+ return () => {
213
+ window.removeEventListener('taskforce:open-markdown-document', handleOpenMarkdownDocument);
214
+ };
215
+ }, [groupBy, setGroupBy]);
216
+ useEffect(() => {
217
+ if (loadedLayoutStateWorkspaceRef.current === layoutStateWorkspaceKey)
218
+ return;
219
+ loadedLayoutStateWorkspaceRef.current = layoutStateWorkspaceKey;
220
+ setLayoutStateReady(false);
221
+ let cancelled = false;
222
+ const loadLayoutUiState = async () => {
223
+ try {
224
+ const res = await fetch('/api/taskforce/ui-state?key=layout', {
225
+ method: 'GET',
226
+ credentials: 'include',
227
+ headers: workspaceUiStateHeaders
228
+ });
229
+ if (!res.ok)
230
+ return;
231
+ const data = await res.json().catch(() => ({}));
232
+ const state = (data?.state && typeof data.state === 'object')
233
+ ? data.state
234
+ : null;
235
+ if (!state || cancelled)
236
+ return;
237
+ if (typeof state.scheduleShowWeekends === 'boolean')
238
+ setScheduleShowWeekends(state.scheduleShowWeekends);
239
+ if (typeof state.scheduleShowBacklog === 'boolean')
240
+ setScheduleShowBacklog(state.scheduleShowBacklog);
241
+ if (typeof state.scheduleOnlyExpired === 'boolean')
242
+ setScheduleOnlyExpired(state.scheduleOnlyExpired);
243
+ if (typeof state.scheduleSidebarOpen === 'boolean')
244
+ setScheduleSidebarOpen(state.scheduleSidebarOpen);
245
+ if (typeof state.hierarchySidebarOpen === 'boolean')
246
+ setHierarchySidebarOpen(state.hierarchySidebarOpen);
247
+ if (typeof state.hierarchyRootTaskId === 'string')
248
+ setHierarchyRootTaskId(state.hierarchyRootTaskId);
249
+ if (typeof state.hierarchyShowUnlinkedColumn === 'boolean')
250
+ setHierarchyShowUnlinkedColumn(state.hierarchyShowUnlinkedColumn);
251
+ if (state.hierarchyChildOrderMode === 'manual' || state.hierarchyChildOrderMode === 'auto') {
252
+ setHierarchyChildOrderMode(state.hierarchyChildOrderMode);
253
+ }
254
+ if (typeof state.scheduleSelectedDate === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(state.scheduleSelectedDate)) {
255
+ setScheduleSelectedDate(state.scheduleSelectedDate);
256
+ }
257
+ if (typeof state.scheduleCalendarMonth === 'string' && /^\d{4}-\d{2}$/.test(state.scheduleCalendarMonth)) {
258
+ setScheduleCalendarMonth(state.scheduleCalendarMonth);
259
+ }
260
+ if (typeof state.scheduleScrollLeft === 'number' && Number.isFinite(state.scheduleScrollLeft) && state.scheduleScrollLeft >= 0) {
261
+ setScheduleScrollLeft(state.scheduleScrollLeft);
262
+ }
263
+ if (typeof state.showFilters === 'boolean')
264
+ setShowFilters(state.showFilters);
265
+ }
266
+ catch {
267
+ // ignore read failure
268
+ }
269
+ finally {
270
+ if (!cancelled)
271
+ setLayoutStateReady(true);
272
+ }
273
+ };
274
+ loadLayoutUiState();
275
+ return () => {
276
+ cancelled = true;
277
+ };
278
+ }, [layoutStateWorkspaceKey, workspaceUiStateHeaders]);
279
+ useEffect(() => {
280
+ if (!layoutStateReady)
281
+ return;
282
+ const timeoutId = window.setTimeout(async () => {
283
+ try {
284
+ await fetch('/api/taskforce/ui-state', {
285
+ method: 'POST',
286
+ headers: { 'Content-Type': 'application/json', ...workspaceUiStateHeaders },
287
+ credentials: 'include',
288
+ body: JSON.stringify({
289
+ stateKey: 'layout',
290
+ patch: {
291
+ scheduleShowWeekends,
292
+ scheduleShowBacklog,
293
+ scheduleOnlyExpired,
294
+ scheduleSidebarOpen,
295
+ hierarchySidebarOpen,
296
+ hierarchyRootTaskId,
297
+ hierarchyShowUnlinkedColumn,
298
+ hierarchyChildOrderMode,
299
+ scheduleSelectedDate,
300
+ scheduleCalendarMonth,
301
+ scheduleScrollLeft,
302
+ showFilters
303
+ }
304
+ })
305
+ });
306
+ }
307
+ catch {
308
+ // ignore write failure
309
+ }
310
+ }, 250);
311
+ return () => window.clearTimeout(timeoutId);
312
+ }, [
313
+ layoutStateReady,
314
+ scheduleShowWeekends,
315
+ scheduleShowBacklog,
316
+ scheduleOnlyExpired,
317
+ scheduleSidebarOpen,
318
+ hierarchySidebarOpen,
319
+ hierarchyRootTaskId,
320
+ hierarchyShowUnlinkedColumn,
321
+ hierarchyChildOrderMode,
322
+ scheduleSelectedDate,
323
+ scheduleCalendarMonth,
324
+ scheduleScrollLeft,
325
+ showFilters,
326
+ workspaceUiStateHeaders
327
+ ]);
328
+ useEffect(() => {
329
+ if (groupBy !== 'docs') {
330
+ lastNonDocsGroupByRef.current = groupBy;
331
+ }
332
+ }, [groupBy]);
333
+ useEffect(() => {
334
+ if (groupBy === 'schedule') {
335
+ setScheduleSidebarOpen(true);
336
+ }
337
+ if (groupBy === 'hierarchy') {
338
+ setHierarchySidebarOpen(true);
339
+ }
340
+ }, [groupBy]);
341
+ const { showArchive, setShowArchive, fetchArchive, filteredArchive } = props;
342
+ const kanbanTasks = useMemo(() => {
343
+ if (!showArchive)
344
+ return props.filteredTasks;
345
+ const archivedWithFlag = filteredArchive.map(t => ({ ...t, isArchived: true }));
346
+ return [...props.filteredTasks, ...archivedWithFlag];
347
+ }, [props.filteredTasks, filteredArchive, showArchive]);
348
+ const scheduleBaseTasks = useMemo(() => props.tasks.filter(task => !task.isArchived), [props.tasks]);
349
+ const scheduleFilteredTaskIds = useMemo(() => new Set(props.filteredTasks.map(task => task.id)), [props.filteredTasks]);
350
+ const kanbanAllTasks = useMemo(() => {
351
+ if (!showArchive)
352
+ return props.searchAgnosticTasks;
353
+ return [...props.searchAgnosticTasks, ...filteredArchive];
354
+ }, [props.searchAgnosticTasks, filteredArchive, showArchive]);
355
+ const hierarchyTasks = useMemo(() => {
356
+ const byId = new Map();
357
+ for (const task of props.archivedTasks)
358
+ byId.set(task.id, task);
359
+ for (const task of props.tasks)
360
+ byId.set(task.id, task);
361
+ return Array.from(byId.values());
362
+ }, [props.tasks, props.archivedTasks]);
363
+ const hierarchyParentIdSet = useMemo(() => {
364
+ const set = new Set();
365
+ for (const task of hierarchyTasks) {
366
+ if (task.parentTaskId)
367
+ set.add(task.parentTaskId);
368
+ }
369
+ return set;
370
+ }, [hierarchyTasks]);
371
+ const hierarchyTaskById = useMemo(() => {
372
+ const map = new Map();
373
+ hierarchyTasks.forEach((task) => map.set(task.id, task));
374
+ return map;
375
+ }, [hierarchyTasks]);
376
+ const hierarchyChildrenByParent = useMemo(() => {
377
+ const map = new Map();
378
+ hierarchyTasks.forEach((task) => {
379
+ if (!task.parentTaskId)
380
+ return;
381
+ const list = map.get(task.parentTaskId) || [];
382
+ list.push(task);
383
+ map.set(task.parentTaskId, list);
384
+ });
385
+ return map;
386
+ }, [hierarchyTasks]);
387
+ const hierarchySearchMatches = useMemo(() => {
388
+ const needle = hierarchyRootSearch.trim().toLowerCase();
389
+ if (!needle)
390
+ return [];
391
+ const rankForTask = (task) => {
392
+ const id = task.id.toLowerCase();
393
+ const shortId = (task.id.split('-').slice(-1)[0] || task.id).toLowerCase();
394
+ const title = task.title.toLowerCase();
395
+ if (id === needle)
396
+ return 0;
397
+ if (shortId === needle)
398
+ return 1;
399
+ if (id.startsWith(needle) || shortId.startsWith(needle))
400
+ return 2;
401
+ if (title.startsWith(needle))
402
+ return 3;
403
+ if (title.includes(needle))
404
+ return 4;
405
+ if (id.includes(needle) || shortId.includes(needle))
406
+ return 5;
407
+ return null;
408
+ };
409
+ return hierarchyTasks
410
+ .map((task) => ({ task, rank: rankForTask(task) }))
411
+ .filter((entry) => entry.rank !== null)
412
+ .sort((a, b) => {
413
+ if (a.rank !== b.rank)
414
+ return a.rank - b.rank;
415
+ return a.task.title.localeCompare(b.task.title);
416
+ })
417
+ .map((entry) => entry.task);
418
+ }, [hierarchyTasks, hierarchyRootSearch]);
419
+ const hierarchyDescendantIdSet = useMemo(() => {
420
+ if (!hierarchyRootTaskId)
421
+ return null;
422
+ return collectSubtreeTaskIds(hierarchyTasks, hierarchyRootTaskId);
423
+ }, [hierarchyRootTaskId, hierarchyTasks]);
424
+ const hierarchyVisibleTasks = useMemo(() => {
425
+ if (groupBy === 'schedule')
426
+ return scheduleBaseTasks;
427
+ if (groupBy !== 'hierarchy')
428
+ return kanbanTasks;
429
+ const isUnlinkedTask = (task) => !task.parentTaskId && !hierarchyParentIdSet.has(task.id);
430
+ let base = kanbanTasks;
431
+ if (parentTaskFilterMode === 'parents') {
432
+ base = kanbanTasks.filter(task => hierarchyParentIdSet.has(task.id));
433
+ }
434
+ else if (parentTaskFilterMode === 'linked') {
435
+ base = kanbanTasks.filter(task => !!task.parentTaskId || hierarchyParentIdSet.has(task.id));
436
+ }
437
+ else if (parentTaskFilterMode === 'unlinked') {
438
+ base = kanbanTasks.filter(task => isUnlinkedTask(task));
439
+ }
440
+ if (!hierarchyShowUnlinkedColumn && parentTaskFilterMode !== 'unlinked') {
441
+ base = base.filter((task) => !isUnlinkedTask(task));
442
+ }
443
+ if (hierarchyDescendantIdSet) {
444
+ base = base.filter(task => hierarchyDescendantIdSet.has(task.id)
445
+ || (hierarchyShowUnlinkedColumn && (parentTaskFilterMode === 'all' || parentTaskFilterMode === 'unlinked') && isUnlinkedTask(task)));
446
+ }
447
+ return base;
448
+ }, [groupBy, parentTaskFilterMode, kanbanTasks, hierarchyParentIdSet, scheduleBaseTasks, hierarchyDescendantIdSet, hierarchyShowUnlinkedColumn]);
449
+ const todayDateOnly = useMemo(() => {
450
+ const now = new Date();
451
+ const y = now.getFullYear();
452
+ const m = String(now.getMonth() + 1).padStart(2, '0');
453
+ const d = String(now.getDate()).padStart(2, '0');
454
+ return `${y}-${m}-${d}`;
455
+ }, []);
456
+ const boardVisibleTasks = useMemo(() => {
457
+ if (groupBy !== 'schedule' || !scheduleOnlyExpired)
458
+ return hierarchyVisibleTasks;
459
+ return hierarchyVisibleTasks.filter((task) => {
460
+ const isTerminal = task.status === 'done' || task.status === 'cancelled';
461
+ if (isTerminal)
462
+ return false;
463
+ const expiredScheduled = !!task.scheduledDate && task.scheduledDate < todayDateOnly;
464
+ const overdueDue = !!task.dueDate && task.dueDate < todayDateOnly;
465
+ return expiredScheduled || overdueDue;
466
+ });
467
+ }, [groupBy, scheduleOnlyExpired, hierarchyVisibleTasks, todayDateOnly]);
468
+ const expiredRecoveryCandidates = useMemo(() => {
469
+ if (groupBy !== 'schedule')
470
+ return [];
471
+ return hierarchyVisibleTasks.filter((task) => {
472
+ const isTerminal = task.status === 'done' || task.status === 'cancelled';
473
+ if (isTerminal)
474
+ return false;
475
+ const expiredScheduled = !!task.scheduledDate && task.scheduledDate < todayDateOnly;
476
+ const overdueDue = !!task.dueDate && task.dueDate < todayDateOnly;
477
+ return expiredScheduled || overdueDue;
478
+ });
479
+ }, [groupBy, hierarchyVisibleTasks, todayDateOnly]);
480
+ const expiredScheduledCount = useMemo(() => {
481
+ if (groupBy !== 'schedule')
482
+ return 0;
483
+ return hierarchyVisibleTasks.filter((task) => {
484
+ const isTerminal = task.status === 'done' || task.status === 'cancelled';
485
+ if (isTerminal)
486
+ return false;
487
+ return !!task.scheduledDate && task.scheduledDate < todayDateOnly;
488
+ }).length;
489
+ }, [groupBy, hierarchyVisibleTasks, todayDateOnly]);
490
+ const overdueDueCount = useMemo(() => {
491
+ if (groupBy !== 'schedule')
492
+ return 0;
493
+ return hierarchyVisibleTasks.filter((task) => {
494
+ const isTerminal = task.status === 'done' || task.status === 'cancelled';
495
+ if (isTerminal)
496
+ return false;
497
+ return !!task.dueDate && task.dueDate < todayDateOnly;
498
+ }).length;
499
+ }, [groupBy, hierarchyVisibleTasks, todayDateOnly]);
500
+ const expiredColumnCandidates = useMemo(() => {
501
+ if (groupBy !== 'schedule')
502
+ return [];
503
+ return hierarchyVisibleTasks.filter((task) => {
504
+ const isTerminal = task.status === 'done' || task.status === 'cancelled';
505
+ if (isTerminal)
506
+ return false;
507
+ return !!task.scheduledDate && task.scheduledDate < todayDateOnly;
508
+ });
509
+ }, [groupBy, hierarchyVisibleTasks, todayDateOnly]);
510
+ const expiredLeafCandidates = useMemo(() => expiredRecoveryCandidates.filter(task => !hierarchyParentIdSet.has(task.id)), [expiredRecoveryCandidates, hierarchyParentIdSet]);
511
+ const [scheduleBulkBusy, setScheduleBulkBusy] = useState(false);
512
+ const [scheduleParentDropBusy, setScheduleParentDropBusy] = useState(false);
513
+ const [scheduleParentDropPrompt, setScheduleParentDropPrompt] = useState(null);
514
+ const hierarchyDepthTasks = useMemo(() => hierarchyVisibleTasks.filter(task => !!task.parentTaskId || hierarchyParentIdSet.has(task.id)), [hierarchyVisibleTasks, hierarchyParentIdSet]);
515
+ const hierarchyGlobalDepthMap = useMemo(() => buildHierarchyDepthMap(hierarchyTasks), [hierarchyTasks]);
516
+ const maxHierarchyDepth = useMemo(() => hierarchyDepthTasks.reduce((maxDepth, task) => {
517
+ const depth = hierarchyGlobalDepthMap.get(task.id) ?? 0;
518
+ return depth > maxDepth ? depth : maxDepth;
519
+ }, 0), [hierarchyDepthTasks, hierarchyGlobalDepthMap]);
520
+ const tryReturnToParent = () => {
521
+ if (activeTab === 'add'
522
+ && editingTaskId
523
+ && returnToParentTaskId
524
+ && returnFromChildTaskId === editingTaskId) {
525
+ const parentTask = tasks.find(t => t.id === returnToParentTaskId);
526
+ clearReturnToParentTask();
527
+ if (parentTask) {
528
+ handleEdit(parentTask);
529
+ return true;
530
+ }
531
+ }
532
+ return false;
533
+ };
534
+ const handleCloseModal = () => {
535
+ handleNavigation(() => {
536
+ if (tryReturnToParent())
537
+ return;
538
+ if (activeTab === 'add')
539
+ resetForm();
540
+ setActiveTab('tasks');
541
+ });
542
+ };
543
+ const [localCancellationReason, setLocalCancellationReason] = useState('');
544
+ const handleConfirmCancellation = async () => {
545
+ const success = await confirmCancellation(localCancellationReason, cancellationMode);
546
+ if (success) {
547
+ setLocalCancellationReason('');
548
+ }
549
+ };
550
+ const handleCancelCancellation = () => {
551
+ cancelCancellationPrompt();
552
+ setLocalCancellationReason('');
553
+ };
554
+ const handleDeleteFromCancellation = async () => {
555
+ if (!cancellationPromptTask)
556
+ return;
557
+ const confirmed = window.confirm('Delete this task permanently? This cannot be undone.');
558
+ if (!confirmed)
559
+ return;
560
+ const deleted = await handleDelete(cancellationPromptTask.id, false, { skipConfirm: true });
561
+ if (deleted) {
562
+ handleCancelCancellation();
563
+ }
564
+ };
565
+ const handleAddTaskToColumn = useCallback((groupField, value) => {
566
+ resetForm();
567
+ // Handle different grouping fields
568
+ if (groupField === 'category') {
569
+ const cat = activeCategories.find(c => String(c.value) === String(value) || c.label === String(value));
570
+ if (cat)
571
+ setCategory(cat.label);
572
+ }
573
+ else if (groupField === 'type') {
574
+ setType(String(value));
575
+ }
576
+ else if (groupField === 'priority') {
577
+ setPriority(Number(value));
578
+ }
579
+ else if (groupField === 'approach') {
580
+ const app = approaches.find(a => String(a.value) === String(value) || a.label === String(value));
581
+ if (app) {
582
+ setApproach(app.value);
583
+ setFormTaxonomies(prev => ({ ...prev, approach: app.value }));
584
+ }
585
+ }
586
+ else if (groupField === 'status') {
587
+ // When adding to a status column, we just open the form.
588
+ // But we can preset the status flags if we want, or just let users set them.
589
+ // Currently, the TaskForm doesn't expose a simple 'status' dropdown to preset.
590
+ // So for now, we'll just open the form.
591
+ // Ideally, we would preset the flags (inProgress, readyForReview, etc.)
592
+ // implementing basic presetting:
593
+ if (value === 'inProgress') {
594
+ handleToggleInProgress({}); // Hacky: assumes handleToggle works on new task? No.
595
+ // Better: we can't easily preset status flags in the current form setup without modifying resetForm/TaskForm props deeply.
596
+ // For now, simple open.
597
+ // Actually, let's try to pass initial state if possible, but useTaskforce API is limited.
598
+ // We'll leave it as default (Task) for now, or users can toggle after adding.
599
+ }
600
+ }
601
+ else if (groupField === 'complexity') {
602
+ const direct = Number(value);
603
+ if (Number.isFinite(direct)) {
604
+ setComplexity(direct);
605
+ }
606
+ }
607
+ else if (groupField === 'hierarchy') {
608
+ // Hierarchy columns are depth-based and do not map to form defaults.
609
+ }
610
+ setActiveTab('add');
611
+ }, [resetForm, activeCategories, approaches, setCategory, setType, setPriority, setComplexity, setApproach, setFormTaxonomies, setActiveTab]);
612
+ // Ensure 'done' status is shown by default as requested
613
+ React.useEffect(() => {
614
+ if (filterStatus && !filterStatus.includes('done')) {
615
+ setFilterStatus(prev => [...prev, 'done']);
616
+ }
617
+ }, []);
618
+ // Local state for filter toolbar
619
+ const [showHelpModal, setShowHelpModal] = useState(false);
620
+ const [showAccountMenu, setShowAccountMenu] = useState(false);
621
+ const [showAccountHub, setShowAccountHub] = useState(false);
622
+ const [showSyncStatusModal, setShowSyncStatusModal] = useState(false);
623
+ const [workspaceSyncBusy, setWorkspaceSyncBusy] = useState(false);
624
+ const [workspaceSyncError, setWorkspaceSyncError] = useState(null);
625
+ const [workspacePanelOpen, setWorkspacePanelOpen] = useState(false);
626
+ const [workspaceFormName, setWorkspaceFormName] = useState('');
627
+ const [workspaceFormDescription, setWorkspaceFormDescription] = useState('');
628
+ const [workspaceActionBusy, setWorkspaceActionBusy] = useState(false);
629
+ const [workspaceActionError, setWorkspaceActionError] = useState('');
630
+ const [showTeamManagementModal, setShowTeamManagementModal] = useState(false);
631
+ const [teamManagementTab, setTeamManagementTab] = useState('members');
632
+ const [teamPlanMode, setTeamPlanMode] = useState('unknown');
633
+ const [teamMgmtError, setTeamMgmtError] = useState(null);
634
+ const [teamUsersLoading, setTeamUsersLoading] = useState(false);
635
+ const [teamUsers, setTeamUsers] = useState([]);
636
+ const [teamActionBusyUserId, setTeamActionBusyUserId] = useState(null);
637
+ const [teamInviteEmail, setTeamInviteEmail] = useState('');
638
+ const [teamInviteRole, setTeamInviteRole] = useState('member');
639
+ const [teamInvitePermissionMode, setTeamInvitePermissionMode] = useState('read-write');
640
+ const [teamInviteBusy, setTeamInviteBusy] = useState(false);
641
+ const [teamInviteFeedback, setTeamInviteFeedback] = useState(null);
642
+ const [teamAuditPage, setTeamAuditPage] = useState(0);
643
+ const [teamAuditLoading, setTeamAuditLoading] = useState(false);
644
+ const [teamAuditEvents, setTeamAuditEvents] = useState([]);
645
+ const [teamAuditHasMore, setTeamAuditHasMore] = useState(false);
646
+ const [teamAuditPages, setTeamAuditPages] = useState(1);
647
+ const [billingStatus, setBillingStatus] = useState(null);
648
+ const [billingLoading, setBillingLoading] = useState(false);
649
+ const [billingError, setBillingError] = useState(null);
650
+ const [billingActionBusy, setBillingActionBusy] = useState(false);
651
+ const [billingActionError, setBillingActionError] = useState(null);
652
+ const [billingNotice, setBillingNotice] = useState(null);
653
+ const [billingIntervalChoice, setBillingIntervalChoice] = useState('month');
654
+ const TEAM_AUDIT_PAGE_SIZE = 25;
655
+ const accountMenuRef = useRef(null);
656
+ const billingSuccessPollRef = useRef(null);
657
+ // showSearch state removed - search always visible in actions area
658
+ useEffect(() => {
659
+ if (!showAccountMenu)
660
+ return;
661
+ const handlePointerDown = (event) => {
662
+ if (!accountMenuRef.current?.contains(event.target)) {
663
+ setShowAccountMenu(false);
664
+ }
665
+ };
666
+ document.addEventListener('mousedown', handlePointerDown);
667
+ return () => document.removeEventListener('mousedown', handlePointerDown);
668
+ }, [showAccountMenu]);
669
+ useEffect(() => () => {
670
+ if (billingSuccessPollRef.current) {
671
+ window.clearInterval(billingSuccessPollRef.current);
672
+ billingSuccessPollRef.current = null;
673
+ }
674
+ }, []);
675
+ const canManageCloudWorkspaces = runtimeMode === 'cloud' && workspaceSwitchingEnabled && isAuthenticated;
676
+ const canManageWorkspaceSync = runtimeMode === 'local' && isAuthenticated;
677
+ const currentWorkspaceRole = useMemo(() => {
678
+ const current = availableWorkspaces.find((workspace) => workspace.id === currentWorkspaceId);
679
+ if (!current)
680
+ return null;
681
+ const role = String(current.role || '').trim().toLowerCase();
682
+ if (role === 'owner' || role === 'admin' || role === 'member' || role === 'read-only')
683
+ return role;
684
+ return null;
685
+ }, [availableWorkspaces, currentWorkspaceId]);
686
+ const canOpenTeamManagement = canManageCloudWorkspaces
687
+ && (currentWorkspaceRole === 'owner' || currentWorkspaceRole === 'admin')
688
+ && teamPlanMode === 'team';
689
+ const authIdentityLabel = String(authUserEmail || '').trim();
690
+ const hasAuthIdentity = authIdentityLabel.length > 0;
691
+ const isAuthBootstrapPending = cloudAuthConfigured && !authSessionResolved;
692
+ const teamAdminHeaders = useCallback(() => {
693
+ const headers = {};
694
+ const workspaceId = String(currentWorkspaceId || '').trim();
695
+ if (workspaceId)
696
+ headers['x-taskforce-workspace-id'] = workspaceId;
697
+ return headers;
698
+ }, [currentWorkspaceId]);
699
+ const loadBillingStatus = useCallback(async () => {
700
+ if (runtimeMode !== 'cloud' || !isAuthenticated) {
701
+ setBillingStatus(null);
702
+ return null;
703
+ }
704
+ setBillingLoading(true);
705
+ setBillingError(null);
706
+ try {
707
+ const res = await fetch('/api/taskforce/billing/status', {
708
+ method: 'GET',
709
+ credentials: 'include'
710
+ });
711
+ const payload = await res.json().catch(() => ({}));
712
+ if (!res.ok) {
713
+ setBillingStatus(null);
714
+ setBillingError(String(payload?.error || 'Failed to load billing status.'));
715
+ return null;
716
+ }
717
+ setBillingStatus(payload);
718
+ return payload;
719
+ }
720
+ catch {
721
+ setBillingStatus(null);
722
+ setBillingError('Failed to load billing status.');
723
+ return null;
724
+ }
725
+ finally {
726
+ setBillingLoading(false);
727
+ }
728
+ }, [isAuthenticated, runtimeMode]);
729
+ const handleStartCheckout = useCallback(async () => {
730
+ setBillingActionError(null);
731
+ setBillingNotice(null);
732
+ setBillingActionBusy(true);
733
+ try {
734
+ const res = await fetch('/api/taskforce/billing/checkout-session', {
735
+ method: 'POST',
736
+ credentials: 'include',
737
+ headers: { 'Content-Type': 'application/json' },
738
+ body: JSON.stringify({
739
+ tier: 'team',
740
+ interval: billingIntervalChoice
741
+ })
742
+ });
743
+ const payload = await res.json().catch(() => ({}));
744
+ if (!res.ok) {
745
+ setBillingActionError(String(payload?.error || 'Failed to create checkout session.'));
746
+ return;
747
+ }
748
+ const nextUrl = String(payload?.url || '').trim();
749
+ if (!nextUrl) {
750
+ setBillingActionError('Checkout session did not return a redirect URL.');
751
+ return;
752
+ }
753
+ window.location.assign(nextUrl);
754
+ }
755
+ catch {
756
+ setBillingActionError('Failed to start checkout.');
757
+ }
758
+ finally {
759
+ setBillingActionBusy(false);
760
+ }
761
+ }, [billingIntervalChoice]);
762
+ const handleOpenBillingPortal = useCallback(async () => {
763
+ setBillingActionError(null);
764
+ setBillingNotice(null);
765
+ setBillingActionBusy(true);
766
+ try {
767
+ const res = await fetch('/api/taskforce/billing/portal-session', {
768
+ method: 'POST',
769
+ credentials: 'include'
770
+ });
771
+ const payload = await res.json().catch(() => ({}));
772
+ if (!res.ok) {
773
+ setBillingActionError(String(payload?.error || 'Failed to create portal session.'));
774
+ return;
775
+ }
776
+ const nextUrl = String(payload?.url || '').trim();
777
+ if (!nextUrl) {
778
+ setBillingActionError('Portal session did not return a redirect URL.');
779
+ return;
780
+ }
781
+ window.location.assign(nextUrl);
782
+ }
783
+ catch {
784
+ setBillingActionError('Failed to open billing portal.');
785
+ }
786
+ finally {
787
+ setBillingActionBusy(false);
788
+ }
789
+ }, []);
790
+ const handleSwitchSubscriptionInterval = useCallback(async () => {
791
+ setBillingActionError(null);
792
+ setBillingNotice(null);
793
+ setBillingActionBusy(true);
794
+ try {
795
+ const res = await fetch('/api/taskforce/billing/subscription', {
796
+ method: 'POST',
797
+ credentials: 'include',
798
+ headers: { 'Content-Type': 'application/json' },
799
+ body: JSON.stringify({
800
+ tier: 'team',
801
+ interval: billingIntervalChoice
802
+ })
803
+ });
804
+ const payload = await res.json().catch(() => ({}));
805
+ if (!res.ok) {
806
+ setBillingActionError(String(payload?.error || 'Failed to update subscription interval.'));
807
+ return;
808
+ }
809
+ setBillingNotice('Subscription updated.');
810
+ await loadBillingStatus();
811
+ }
812
+ catch {
813
+ setBillingActionError('Failed to update subscription interval.');
814
+ }
815
+ finally {
816
+ setBillingActionBusy(false);
817
+ }
818
+ }, [billingIntervalChoice, loadBillingStatus]);
819
+ const loadTeamPlanMode = useCallback(async () => {
820
+ if (!canManageCloudWorkspaces || !(currentWorkspaceRole === 'owner' || currentWorkspaceRole === 'admin')) {
821
+ setTeamPlanMode('unknown');
822
+ return;
823
+ }
824
+ try {
825
+ const res = await fetch('/api/taskforce/account/entitlements', {
826
+ method: 'GET',
827
+ credentials: 'include',
828
+ headers: teamAdminHeaders()
829
+ });
830
+ const payload = await res.json().catch(() => ({}));
831
+ if (!res.ok) {
832
+ setTeamPlanMode('unknown');
833
+ return;
834
+ }
835
+ const planId = String(payload?.planId || '').trim().toLowerCase();
836
+ const state = String(payload?.state || '').trim().toLowerCase();
837
+ const teamAllowed = payload?.features?.['collaboration.team_management']?.allowed === true;
838
+ const stateAllows = state === 'active' || state === 'trialing' || state === 'grace';
839
+ setTeamPlanMode(planId === 'team' && teamAllowed && stateAllows ? 'team' : 'personal');
840
+ }
841
+ catch {
842
+ setTeamPlanMode('unknown');
843
+ }
844
+ }, [canManageCloudWorkspaces, currentWorkspaceRole, teamAdminHeaders]);
845
+ useEffect(() => {
846
+ if (!showAccountMenu && !showTeamManagementModal && !showAccountHub)
847
+ return;
848
+ void loadTeamPlanMode();
849
+ }, [showAccountHub, showAccountMenu, showTeamManagementModal, loadTeamPlanMode]);
850
+ useEffect(() => {
851
+ if (!showAccountHub)
852
+ return;
853
+ void loadBillingStatus();
854
+ }, [loadBillingStatus, showAccountHub]);
855
+ useEffect(() => {
856
+ if (!isAuthenticated)
857
+ return;
858
+ if (location.pathname !== '/billing/success' && location.pathname !== '/billing/cancel')
859
+ return;
860
+ setShowAccountHub(true);
861
+ setShowAccountMenu(false);
862
+ setBillingActionError(null);
863
+ setBillingError(null);
864
+ if (location.pathname === '/billing/cancel') {
865
+ setBillingNotice('Checkout canceled. No billing changes were applied.');
866
+ navigate('/', { replace: true });
867
+ return;
868
+ }
869
+ setBillingNotice('Checkout complete. Waiting for billing confirmation...');
870
+ void loadBillingStatus();
871
+ let attempts = 0;
872
+ const maxAttempts = 30;
873
+ if (billingSuccessPollRef.current) {
874
+ window.clearInterval(billingSuccessPollRef.current);
875
+ billingSuccessPollRef.current = null;
876
+ }
877
+ billingSuccessPollRef.current = window.setInterval(() => {
878
+ attempts += 1;
879
+ void loadBillingStatus().then((payload) => {
880
+ const entitlementState = String(payload?.entitlementState || '').trim().toLowerCase();
881
+ if (entitlementState && entitlementState !== 'free') {
882
+ setBillingNotice('Subscription is active.');
883
+ if (billingSuccessPollRef.current) {
884
+ window.clearInterval(billingSuccessPollRef.current);
885
+ billingSuccessPollRef.current = null;
886
+ }
887
+ navigate('/', { replace: true });
888
+ return;
889
+ }
890
+ if (attempts >= maxAttempts && billingSuccessPollRef.current) {
891
+ window.clearInterval(billingSuccessPollRef.current);
892
+ billingSuccessPollRef.current = null;
893
+ setBillingNotice('Checkout finished, but entitlement update is still pending. Refresh Billing Status in a few seconds.');
894
+ navigate('/', { replace: true });
895
+ }
896
+ });
897
+ }, 1500);
898
+ return () => {
899
+ if (billingSuccessPollRef.current) {
900
+ window.clearInterval(billingSuccessPollRef.current);
901
+ billingSuccessPollRef.current = null;
902
+ }
903
+ };
904
+ }, [isAuthenticated, loadBillingStatus, location.pathname, navigate]);
905
+ const loadTeamUsers = useCallback(async () => {
906
+ if (!canOpenTeamManagement)
907
+ return;
908
+ setTeamUsersLoading(true);
909
+ setTeamMgmtError(null);
910
+ try {
911
+ const res = await fetch('/api/taskforce/admin/users', {
912
+ method: 'GET',
913
+ credentials: 'include',
914
+ headers: teamAdminHeaders()
915
+ });
916
+ const payload = await res.json().catch(() => ({}));
917
+ if (!res.ok) {
918
+ setTeamMgmtError(payload?.error || 'Failed to load workspace users.');
919
+ return;
920
+ }
921
+ const users = Array.isArray(payload?.users) ? payload.users : [];
922
+ const roleSortOrder = { owner: 0, admin: 1, member: 2, 'read-only': 3 };
923
+ const mapped = users.map((user) => ({
924
+ userId: String(user?.userId || ''),
925
+ email: String(user?.email || ''),
926
+ displayName: typeof user?.displayName === 'string' ? user.displayName : null,
927
+ role: user?.role === 'owner' || user?.role === 'admin' || user?.role === 'member' || user?.role === 'read-only'
928
+ ? user.role
929
+ : 'member',
930
+ permissionMode: user?.permissionMode === 'read-only' ? 'read-only' : 'read-write',
931
+ status: String(user?.status || 'active'),
932
+ disabled: user?.disabled === true
933
+ }));
934
+ mapped.sort((a, b) => {
935
+ const roleDelta = (roleSortOrder[a.role] ?? 99) - (roleSortOrder[b.role] ?? 99);
936
+ if (roleDelta !== 0)
937
+ return roleDelta;
938
+ return (a.displayName || a.email).localeCompare(b.displayName || b.email, undefined, { sensitivity: 'base' });
939
+ });
940
+ setTeamUsers(mapped);
941
+ }
942
+ catch {
943
+ setTeamMgmtError('Failed to load workspace users.');
944
+ }
945
+ finally {
946
+ setTeamUsersLoading(false);
947
+ }
948
+ }, [canOpenTeamManagement, teamAdminHeaders]);
949
+ const loadTeamAuditPage = useCallback(async (page) => {
950
+ if (!canOpenTeamManagement)
951
+ return;
952
+ const safePage = Math.max(0, Math.floor(page));
953
+ setTeamAuditLoading(true);
954
+ setTeamMgmtError(null);
955
+ try {
956
+ const offset = safePage * TEAM_AUDIT_PAGE_SIZE;
957
+ const res = await fetch(`/api/taskforce/admin/audit-logs?limit=${TEAM_AUDIT_PAGE_SIZE}&offset=${offset}`, {
958
+ method: 'GET',
959
+ credentials: 'include',
960
+ headers: teamAdminHeaders()
961
+ });
962
+ const payload = await res.json().catch(() => ({}));
963
+ if (!res.ok) {
964
+ setTeamMgmtError(payload?.error || 'Failed to load workspace audit log.');
965
+ return;
966
+ }
967
+ const events = Array.isArray(payload?.events) ? payload.events : [];
968
+ setTeamAuditEvents(events.map((event) => ({
969
+ id: String(event?.id || ''),
970
+ action: String(event?.action || ''),
971
+ actorUserId: String(event?.actorUserId || ''),
972
+ actorRole: String(event?.actorRole || ''),
973
+ createdAt: String(event?.createdAt || event?.ts || '')
974
+ })));
975
+ setTeamAuditPage(safePage);
976
+ const pages = Math.max(1, Number(payload?.pages || 1));
977
+ setTeamAuditPages(pages);
978
+ setTeamAuditHasMore(safePage + 1 < pages);
979
+ }
980
+ catch {
981
+ setTeamMgmtError('Failed to load workspace audit log.');
982
+ }
983
+ finally {
984
+ setTeamAuditLoading(false);
985
+ }
986
+ }, [canOpenTeamManagement, teamAdminHeaders]);
987
+ const reloadTeamManagementData = useCallback(async () => {
988
+ await Promise.all([loadTeamUsers(), loadTeamAuditPage(teamAuditPage)]);
989
+ }, [loadTeamUsers, loadTeamAuditPage, teamAuditPage]);
990
+ const openSharedLoginRoute = useCallback((mode = 'login') => {
991
+ const targetRaw = `${location.pathname}${location.search}${location.hash}`;
992
+ const target = (!targetRaw || targetRaw === '/login' || !targetRaw.startsWith('/')) ? '/' : targetRaw;
993
+ setShowAccountMenu(false);
994
+ setShowAccountHub(false);
995
+ setShowSyncStatusModal(false);
996
+ navigate(`/login?mode=${mode}&next=${encodeURIComponent(target)}`);
997
+ }, [location.hash, location.pathname, location.search, navigate]);
998
+ const openWorkspaceSetup = useCallback(() => {
999
+ setShowAccountMenu(false);
1000
+ setWorkspacePanelOpen(false);
1001
+ navigate('/setup?step=workspace', { replace: true });
1002
+ }, [navigate]);
1003
+ const handleSwitchWorkspace = useCallback(async (workspaceId) => {
1004
+ if (!workspaceId || workspaceActionBusy)
1005
+ return;
1006
+ setWorkspaceActionError('');
1007
+ setWorkspaceActionBusy(true);
1008
+ try {
1009
+ const result = await switchWorkspace(workspaceId);
1010
+ if (!result.success) {
1011
+ if (result.code === 'WORKSPACE_NOT_FOUND' || result.code === 'WORKSPACE_ID_REQUIRED') {
1012
+ openWorkspaceSetup();
1013
+ return;
1014
+ }
1015
+ setWorkspaceActionError(result.error || 'Failed to switch workspace.');
1016
+ return;
1017
+ }
1018
+ await fetchTasks(true);
1019
+ setShowAccountMenu(false);
1020
+ setWorkspacePanelOpen(false);
1021
+ }
1022
+ finally {
1023
+ setWorkspaceActionBusy(false);
1024
+ }
1025
+ }, [switchWorkspace, workspaceActionBusy, fetchTasks, openWorkspaceSetup]);
1026
+ const handleCreateWorkspace = useCallback(async () => {
1027
+ if (workspaceActionBusy)
1028
+ return;
1029
+ const name = workspaceFormName.trim();
1030
+ if (!name) {
1031
+ setWorkspaceActionError('Workspace name is required.');
1032
+ return;
1033
+ }
1034
+ setWorkspaceActionError('');
1035
+ setWorkspaceActionBusy(true);
1036
+ try {
1037
+ const result = await createWorkspace(name, workspaceFormDescription.trim() || undefined);
1038
+ if (!result.success) {
1039
+ if (result.code === 'WORKSPACE_NOT_FOUND' || result.code === 'WORKSPACE_ID_REQUIRED') {
1040
+ openWorkspaceSetup();
1041
+ return;
1042
+ }
1043
+ setWorkspaceActionError(result.error || 'Failed to create workspace.');
1044
+ return;
1045
+ }
1046
+ await fetchTasks(true);
1047
+ setWorkspaceFormName('');
1048
+ setWorkspaceFormDescription('');
1049
+ setWorkspacePanelOpen(false);
1050
+ setShowAccountMenu(false);
1051
+ }
1052
+ finally {
1053
+ setWorkspaceActionBusy(false);
1054
+ }
1055
+ }, [workspaceActionBusy, workspaceFormName, workspaceFormDescription, createWorkspace, fetchTasks, openWorkspaceSetup]);
1056
+ const openTeamManagementModal = useCallback(async () => {
1057
+ setShowAccountMenu(false);
1058
+ setShowTeamManagementModal(true);
1059
+ setTeamManagementTab('members');
1060
+ await loadTeamPlanMode();
1061
+ await loadTeamUsers();
1062
+ await loadTeamAuditPage(0);
1063
+ }, [loadTeamAuditPage, loadTeamPlanMode, loadTeamUsers]);
1064
+ const executeTeamUserAction = useCallback(async (userId, fn) => {
1065
+ setTeamMgmtError(null);
1066
+ setTeamActionBusyUserId(userId);
1067
+ try {
1068
+ const res = await fn();
1069
+ const payload = await res.json().catch(() => ({}));
1070
+ if (!res.ok) {
1071
+ setTeamMgmtError(payload?.error || 'Team management action failed.');
1072
+ return;
1073
+ }
1074
+ await reloadTeamManagementData();
1075
+ }
1076
+ catch {
1077
+ setTeamMgmtError('Team management action failed.');
1078
+ }
1079
+ finally {
1080
+ setTeamActionBusyUserId(null);
1081
+ }
1082
+ }, [reloadTeamManagementData]);
1083
+ const submitTeamInvite = useCallback(async () => {
1084
+ const email = teamInviteEmail.trim();
1085
+ if (!email)
1086
+ return;
1087
+ setTeamInviteBusy(true);
1088
+ setTeamMgmtError(null);
1089
+ setTeamInviteFeedback(null);
1090
+ try {
1091
+ const res = await fetch('/api/taskforce/admin/users/invite', {
1092
+ method: 'POST',
1093
+ headers: {
1094
+ 'Content-Type': 'application/json',
1095
+ ...teamAdminHeaders()
1096
+ },
1097
+ credentials: 'include',
1098
+ body: JSON.stringify({
1099
+ workspaceId: currentWorkspaceId,
1100
+ email,
1101
+ role: teamInviteRole,
1102
+ permissionMode: teamInviteRole === 'member' ? teamInvitePermissionMode : 'read-write'
1103
+ })
1104
+ });
1105
+ const payload = await res.json().catch(() => ({}));
1106
+ if (!res.ok) {
1107
+ setTeamMgmtError(payload?.error || 'Failed to send invite.');
1108
+ return;
1109
+ }
1110
+ setTeamInviteEmail('');
1111
+ if (payload?.inviteEmailSent === false) {
1112
+ setTeamInviteFeedback(`Invite created, but email delivery failed${payload?.inviteEmailError ? `: ${String(payload.inviteEmailError)}` : '.'}`);
1113
+ }
1114
+ else {
1115
+ setTeamInviteFeedback('Invite sent.');
1116
+ }
1117
+ await reloadTeamManagementData();
1118
+ }
1119
+ catch {
1120
+ setTeamMgmtError('Failed to send invite.');
1121
+ }
1122
+ finally {
1123
+ setTeamInviteBusy(false);
1124
+ }
1125
+ }, [
1126
+ currentWorkspaceId,
1127
+ reloadTeamManagementData,
1128
+ teamAdminHeaders,
1129
+ teamInviteEmail,
1130
+ teamInvitePermissionMode,
1131
+ teamInviteRole
1132
+ ]);
1133
+ const hasSearchFilter = useMemo(() => searchQuery.trim().length > 0, [searchQuery]);
1134
+ const pendingInvites = useMemo(() => teamUsers.filter((user) => user.status === 'invited'), [teamUsers]);
1135
+ // Check if any non-search filters are set from the filters dropdown/panel
1136
+ const hasDropdownFilters = useMemo(() => {
1137
+ const hasCategoryFilter = filterCategories.length !== activeCategories.length;
1138
+ const hasTypeFilter = filterTypes.length !== activeTypes.length;
1139
+ const hasPriorityFilter = filterPriorities.length !== priorities.length;
1140
+ const hasStatusFilter = filterStatus.length !== STATUS_OPTIONS.length;
1141
+ const hasAssigneeFilter = assigneeFilterEnabled && filterAssignees.length !== 3;
1142
+ const hasParentsOnlyFilter = parentTaskFilterMode !== 'all';
1143
+ return hasCategoryFilter || hasTypeFilter || hasPriorityFilter || hasStatusFilter || hasAssigneeFilter || hasParentsOnlyFilter;
1144
+ }, [filterCategories, activeCategories, filterTypes, activeTypes, filterPriorities, priorities, filterStatus, assigneeFilterEnabled, filterAssignees, parentTaskFilterMode]);
1145
+ // Check if any filters are set (search + dropdown filters)
1146
+ const isFiltered = hasSearchFilter || hasDropdownFilters;
1147
+ const totalActiveCount = tasks.length;
1148
+ const visibleActiveCount = props.filteredTasks.length;
1149
+ const activeCountLabel = isFiltered ? `${visibleActiveCount}/${totalActiveCount}` : `${totalActiveCount}`;
1150
+ const activeCountTitle = isFiltered
1151
+ ? t('standalone.visibleActiveTasksTitle')
1152
+ : t('standalone.totalActiveTasksTitle');
1153
+ const shouldShowSyncStatus = runtimeMode === 'local';
1154
+ const formattedLastSyncTime = useMemo(() => {
1155
+ if (!userGlobalLastSyncedAt)
1156
+ return 'Never';
1157
+ const parsed = new Date(userGlobalLastSyncedAt);
1158
+ if (Number.isNaN(parsed.getTime()))
1159
+ return 'Never';
1160
+ return parsed.toLocaleString();
1161
+ }, [userGlobalLastSyncedAt]);
1162
+ const formatSyncTimestamp = useCallback((value) => {
1163
+ if (!value)
1164
+ return 'Never';
1165
+ const parsed = new Date(value);
1166
+ if (Number.isNaN(parsed.getTime()))
1167
+ return 'Never';
1168
+ return parsed.toLocaleString();
1169
+ }, []);
1170
+ const formattedLastPullTime = useMemo(() => formatSyncTimestamp(workspaceLastPullAt), [workspaceLastPullAt, formatSyncTimestamp]);
1171
+ const formattedLastPushTime = useMemo(() => formatSyncTimestamp(workspaceLastPushAt), [workspaceLastPushAt, formatSyncTimestamp]);
1172
+ const formattedLastErrorTime = useMemo(() => formatSyncTimestamp(workspaceLastErrorAt), [workspaceLastErrorAt, formatSyncTimestamp]);
1173
+ const formattedRetryTime = useMemo(() => formatSyncTimestamp(workspaceRetryAt), [workspaceRetryAt, formatSyncTimestamp]);
1174
+ const [displaySyncState, setDisplaySyncState] = useState(() => (workspaceCloudSyncEnabled ? userGlobalSyncStatus : 'disconnected'));
1175
+ const syncStateSinceRef = useRef(Date.now());
1176
+ const syncStateTimerRef = useRef(null);
1177
+ const rawSyncState = useMemo(() => {
1178
+ if (!workspaceCloudSyncEnabled)
1179
+ return 'disconnected';
1180
+ if (userGlobalSyncStatus === 'error')
1181
+ return 'error';
1182
+ if (realtimeSyncEnabled && realtimeConnectionState === 'degraded-fallback')
1183
+ return 'degraded-fallback';
1184
+ if (realtimeSyncEnabled && realtimeConnectionState === 'reconnecting')
1185
+ return 'reconnecting';
1186
+ return userGlobalSyncStatus;
1187
+ }, [workspaceCloudSyncEnabled, userGlobalSyncStatus, realtimeSyncEnabled, realtimeConnectionState]);
1188
+ useEffect(() => {
1189
+ if (syncStateTimerRef.current !== null) {
1190
+ window.clearTimeout(syncStateTimerRef.current);
1191
+ syncStateTimerRef.current = null;
1192
+ }
1193
+ const applyState = (next) => {
1194
+ setDisplaySyncState(next);
1195
+ syncStateSinceRef.current = Date.now();
1196
+ };
1197
+ if (rawSyncState === displaySyncState)
1198
+ return;
1199
+ // Delay "syncing" to avoid flicker on very fast sync cycles.
1200
+ if (rawSyncState === 'syncing' || rawSyncState === 'reconnecting') {
1201
+ syncStateTimerRef.current = window.setTimeout(() => {
1202
+ applyState(rawSyncState);
1203
+ }, SYNCING_DISPLAY_DELAY_MS);
1204
+ return;
1205
+ }
1206
+ // Hold syncing/error briefly so status doesn't flash rapidly.
1207
+ if (displaySyncState === 'syncing' || displaySyncState === 'reconnecting') {
1208
+ const elapsed = Date.now() - syncStateSinceRef.current;
1209
+ const remaining = Math.max(0, SYNCING_MIN_VISIBLE_MS - elapsed);
1210
+ syncStateTimerRef.current = window.setTimeout(() => {
1211
+ applyState(rawSyncState);
1212
+ }, remaining);
1213
+ return;
1214
+ }
1215
+ if (displaySyncState === 'error' && rawSyncState !== 'error') {
1216
+ const elapsed = Date.now() - syncStateSinceRef.current;
1217
+ const remaining = Math.max(0, ERROR_MIN_VISIBLE_MS - elapsed);
1218
+ syncStateTimerRef.current = window.setTimeout(() => {
1219
+ applyState(rawSyncState);
1220
+ }, remaining);
1221
+ return;
1222
+ }
1223
+ applyState(rawSyncState);
1224
+ }, [rawSyncState, displaySyncState]);
1225
+ useEffect(() => {
1226
+ return () => {
1227
+ if (syncStateTimerRef.current !== null) {
1228
+ window.clearTimeout(syncStateTimerRef.current);
1229
+ syncStateTimerRef.current = null;
1230
+ }
1231
+ };
1232
+ }, []);
1233
+ const modalSyncStatusMeta = useMemo(() => {
1234
+ if (!workspaceCloudSyncEnabled) {
1235
+ return {
1236
+ label: 'Sync Off',
1237
+ color: '#94a3b8',
1238
+ border: 'rgba(148, 163, 184, 0.45)',
1239
+ background: 'rgba(148, 163, 184, 0.12)'
1240
+ };
1241
+ }
1242
+ switch (displaySyncState) {
1243
+ case 'syncing':
1244
+ return {
1245
+ label: 'Syncing',
1246
+ color: '#f59e0b',
1247
+ border: 'rgba(245, 158, 11, 0.45)',
1248
+ background: 'rgba(245, 158, 11, 0.16)'
1249
+ };
1250
+ case 'reconnecting':
1251
+ return {
1252
+ label: 'Reconnecting',
1253
+ color: '#f59e0b',
1254
+ border: 'rgba(245, 158, 11, 0.45)',
1255
+ background: 'rgba(245, 158, 11, 0.16)'
1256
+ };
1257
+ case 'degraded-fallback':
1258
+ return {
1259
+ label: 'Fallback Pull',
1260
+ color: '#fb923c',
1261
+ border: 'rgba(251, 146, 60, 0.45)',
1262
+ background: 'rgba(251, 146, 60, 0.16)'
1263
+ };
1264
+ case 'error':
1265
+ return {
1266
+ label: 'Sync Error',
1267
+ color: '#f87171',
1268
+ border: 'rgba(248, 113, 113, 0.45)',
1269
+ background: 'rgba(248, 113, 113, 0.14)'
1270
+ };
1271
+ case 'disconnected':
1272
+ return {
1273
+ label: 'Disconnected',
1274
+ color: '#94a3b8',
1275
+ border: 'rgba(148, 163, 184, 0.45)',
1276
+ background: 'rgba(148, 163, 184, 0.12)'
1277
+ };
1278
+ case 'idle':
1279
+ default:
1280
+ return {
1281
+ label: 'Synced',
1282
+ color: '#34d399',
1283
+ border: 'rgba(52, 211, 153, 0.45)',
1284
+ background: 'rgba(52, 211, 153, 0.14)'
1285
+ };
1286
+ }
1287
+ }, [displaySyncState, workspaceCloudSyncEnabled]);
1288
+ const headerSyncStatusMeta = useMemo(() => {
1289
+ if (!workspaceCloudSyncEnabled) {
1290
+ return {
1291
+ label: 'Sync Off',
1292
+ color: '#94a3b8',
1293
+ border: 'rgba(148, 163, 184, 0.45)',
1294
+ background: 'rgba(148, 163, 184, 0.12)',
1295
+ icon: 'off'
1296
+ };
1297
+ }
1298
+ if (displaySyncState === 'error') {
1299
+ return {
1300
+ label: 'Sync Error',
1301
+ color: '#f87171',
1302
+ border: 'rgba(248, 113, 113, 0.45)',
1303
+ background: 'rgba(248, 113, 113, 0.14)',
1304
+ icon: 'off'
1305
+ };
1306
+ }
1307
+ if (displaySyncState === 'degraded-fallback') {
1308
+ return {
1309
+ label: 'Fallback Pull',
1310
+ color: '#fb923c',
1311
+ border: 'rgba(251, 146, 60, 0.45)',
1312
+ background: 'rgba(251, 146, 60, 0.16)',
1313
+ icon: 'off'
1314
+ };
1315
+ }
1316
+ if (displaySyncState === 'reconnecting') {
1317
+ return {
1318
+ label: 'Reconnecting',
1319
+ color: '#f59e0b',
1320
+ border: 'rgba(245, 158, 11, 0.45)',
1321
+ background: 'rgba(245, 158, 11, 0.16)',
1322
+ icon: 'off'
1323
+ };
1324
+ }
1325
+ if (displaySyncState === 'disconnected') {
1326
+ return {
1327
+ label: 'Disconnected',
1328
+ color: '#94a3b8',
1329
+ border: 'rgba(148, 163, 184, 0.45)',
1330
+ background: 'rgba(148, 163, 184, 0.12)',
1331
+ icon: 'off'
1332
+ };
1333
+ }
1334
+ return {
1335
+ label: 'Synced',
1336
+ color: '#34d399',
1337
+ border: 'rgba(52, 211, 153, 0.45)',
1338
+ background: 'rgba(52, 211, 153, 0.14)',
1339
+ icon: 'cloud'
1340
+ };
1341
+ }, [displaySyncState, workspaceCloudSyncEnabled]);
1342
+ const parentFilterLabel = hierarchyRootTaskId
1343
+ ? 'Subtree'
1344
+ : parentTaskFilterMode === 'parents'
1345
+ ? t('standalone.linksParents')
1346
+ : parentTaskFilterMode === 'linked'
1347
+ ? t('standalone.linksLinked')
1348
+ : parentTaskFilterMode === 'unlinked'
1349
+ ? t('standalone.linksUnlinked')
1350
+ : t('standalone.linksAll');
1351
+ const selectedHierarchyRootTask = hierarchyRootTaskId ? hierarchyTaskById.get(hierarchyRootTaskId) : undefined;
1352
+ const hierarchySubtreeStats = useMemo(() => {
1353
+ if (!hierarchyDescendantIdSet || !selectedHierarchyRootTask)
1354
+ return null;
1355
+ const selectedTasks = hierarchyTasks.filter((task) => hierarchyDescendantIdSet.has(task.id));
1356
+ let active = 0;
1357
+ let terminal = 0;
1358
+ let overdue = 0;
1359
+ selectedTasks.forEach((task) => {
1360
+ const isTerminal = task.status === 'done' || task.status === 'cancelled' || Boolean(task.isArchived);
1361
+ if (isTerminal) {
1362
+ terminal += 1;
1363
+ }
1364
+ else {
1365
+ active += 1;
1366
+ if (task.dueDate && task.dueDate < todayDateOnly)
1367
+ overdue += 1;
1368
+ }
1369
+ });
1370
+ let depth = 0;
1371
+ const visited = new Set();
1372
+ const stack = [{ id: selectedHierarchyRootTask.id, depth: 0 }];
1373
+ while (stack.length > 0) {
1374
+ const current = stack.pop();
1375
+ if (visited.has(current.id))
1376
+ continue;
1377
+ visited.add(current.id);
1378
+ if (current.depth > depth)
1379
+ depth = current.depth;
1380
+ const children = hierarchyChildrenByParent.get(current.id) || [];
1381
+ children.forEach((child) => {
1382
+ if (!visited.has(child.id) && hierarchyDescendantIdSet.has(child.id)) {
1383
+ stack.push({ id: child.id, depth: current.depth + 1 });
1384
+ }
1385
+ });
1386
+ }
1387
+ return { total: selectedTasks.length, active, terminal, overdue, depth };
1388
+ }, [hierarchyDescendantIdSet, selectedHierarchyRootTask, hierarchyTasks, todayDateOnly, hierarchyChildrenByParent]);
1389
+ const hierarchyVisibleLevelCounts = useMemo(() => {
1390
+ const byLevel = new Map();
1391
+ hierarchyDepthTasks.forEach((task) => {
1392
+ const level = (hierarchyGlobalDepthMap.get(task.id) ?? 0) + 1;
1393
+ byLevel.set(level, (byLevel.get(level) || 0) + 1);
1394
+ });
1395
+ return Array.from(byLevel.entries())
1396
+ .sort((a, b) => a[0] - b[0])
1397
+ .map(([level, count]) => ({ level, count }));
1398
+ }, [hierarchyDepthTasks, hierarchyGlobalDepthMap]);
1399
+ const hierarchyVisibleUnlinkedCount = useMemo(() => hierarchyVisibleTasks.filter((task) => !task.parentTaskId && !hierarchyParentIdSet.has(task.id)).length, [hierarchyVisibleTasks, hierarchyParentIdSet]);
1400
+ const hierarchyVisibleRootCount = useMemo(() => hierarchyVisibleTasks.filter((task) => !task.parentTaskId && hierarchyParentIdSet.has(task.id)).length, [hierarchyVisibleTasks, hierarchyParentIdSet]);
1401
+ const hierarchySubtreeTaskList = useMemo(() => {
1402
+ if (!hierarchyRootTaskId || !hierarchyDescendantIdSet)
1403
+ return [];
1404
+ return hierarchyTasks
1405
+ .filter((task) => hierarchyDescendantIdSet.has(task.id))
1406
+ .sort((a, b) => {
1407
+ const levelA = hierarchyGlobalDepthMap.get(a.id) ?? Number.MAX_SAFE_INTEGER;
1408
+ const levelB = hierarchyGlobalDepthMap.get(b.id) ?? Number.MAX_SAFE_INTEGER;
1409
+ if (levelA !== levelB)
1410
+ return levelA - levelB;
1411
+ return a.title.localeCompare(b.title);
1412
+ });
1413
+ }, [hierarchyRootTaskId, hierarchyDescendantIdSet, hierarchyTasks, hierarchyGlobalDepthMap]);
1414
+ const applyHierarchySearchSelection = useCallback(() => {
1415
+ const bestMatch = hierarchySearchMatches[0];
1416
+ if (!bestMatch)
1417
+ return;
1418
+ setHierarchyRootTaskId(bestMatch.id);
1419
+ setHierarchyRootSearch('');
1420
+ if (parentTaskFilterMode === 'unlinked')
1421
+ setParentTaskFilterMode('all');
1422
+ }, [hierarchySearchMatches, parentTaskFilterMode, setParentTaskFilterMode]);
1423
+ const selectedDateObj = useMemo(() => parseDateOnlyLocal(scheduleSelectedDate) || new Date(), [scheduleSelectedDate]);
1424
+ const scheduleWeekStart = useMemo(() => startOfWeek(selectedDateObj, globalWeekStartsOn), [selectedDateObj, globalWeekStartsOn]);
1425
+ const scheduleDates = useMemo(() => {
1426
+ const map = {
1427
+ mon: '',
1428
+ tue: '',
1429
+ wed: '',
1430
+ thu: '',
1431
+ fri: '',
1432
+ sat: '',
1433
+ sun: ''
1434
+ };
1435
+ SCHEDULE_DAY_KEYS.forEach((key) => {
1436
+ const offset = getDayOffsetFromWeekStart(key, globalWeekStartsOn);
1437
+ map[key] = toDateOnlyLocal(addDays(scheduleWeekStart, offset));
1438
+ });
1439
+ return map;
1440
+ }, [scheduleWeekStart, globalWeekStartsOn]);
1441
+ const mapToSelectedWeekDate = useCallback((sourceDateOnly) => {
1442
+ const parsed = parseDateOnlyLocal(sourceDateOnly);
1443
+ if (!parsed)
1444
+ return scheduleDates.mon;
1445
+ const day = parsed.getDay();
1446
+ const key = day === 1 ? 'mon' :
1447
+ day === 2 ? 'tue' :
1448
+ day === 3 ? 'wed' :
1449
+ day === 4 ? 'thu' :
1450
+ day === 5 ? 'fri' :
1451
+ day === 6 ? (scheduleShowWeekends ? 'sat' : 'fri') :
1452
+ (scheduleShowWeekends ? 'sun' : 'fri');
1453
+ return scheduleDates[key] || scheduleDates.mon;
1454
+ }, [scheduleDates, scheduleShowWeekends]);
1455
+ const handleMoveExpiredToSelectedWeek = useCallback(async () => {
1456
+ if (scheduleBulkBusy || expiredLeafCandidates.length === 0)
1457
+ return;
1458
+ setScheduleBulkBusy(true);
1459
+ try {
1460
+ for (const task of expiredLeafCandidates) {
1461
+ const sourceDate = task.scheduledDate || task.dueDate || todayDateOnly;
1462
+ const nextDate = mapToSelectedWeekDate(sourceDate);
1463
+ await handleUpdateTask(task.id, {
1464
+ scheduledDate: nextDate,
1465
+ scheduledWeekKey: dateOnlyToIsoWeekKey(nextDate),
1466
+ orderInDay: null
1467
+ });
1468
+ }
1469
+ await fetchTasks(true);
1470
+ }
1471
+ finally {
1472
+ setScheduleBulkBusy(false);
1473
+ }
1474
+ }, [scheduleBulkBusy, expiredLeafCandidates, todayDateOnly, mapToSelectedWeekDate, handleUpdateTask, fetchTasks]);
1475
+ const handleMoveExpiredToBacklog = useCallback(async () => {
1476
+ if (scheduleBulkBusy || expiredRecoveryCandidates.length === 0)
1477
+ return;
1478
+ setScheduleBulkBusy(true);
1479
+ try {
1480
+ for (const task of expiredRecoveryCandidates) {
1481
+ await handleUpdateTask(task.id, {
1482
+ scheduledDate: null,
1483
+ scheduledWeekKey: null,
1484
+ orderInDay: null
1485
+ });
1486
+ }
1487
+ await fetchTasks(true);
1488
+ }
1489
+ finally {
1490
+ setScheduleBulkBusy(false);
1491
+ }
1492
+ }, [scheduleBulkBusy, expiredRecoveryCandidates, handleUpdateTask, fetchTasks]);
1493
+ const handleScheduleParentDrop = useCallback((payload) => {
1494
+ if (payload.targetDate < todayDateOnly) {
1495
+ const byId = new Map(tasks.map((task) => [task.id, task]));
1496
+ const hasNonTerminalLeaf = payload.toScheduleLeafTaskIds.some((taskId) => {
1497
+ const task = byId.get(taskId);
1498
+ if (!task)
1499
+ return true;
1500
+ return task.status !== 'done' && task.status !== 'cancelled';
1501
+ });
1502
+ if (hasNonTerminalLeaf) {
1503
+ window.alert(t('schedule.completedOnlyPastSchedule'));
1504
+ return;
1505
+ }
1506
+ }
1507
+ if (payload.toScheduleLeafTaskIds.length === 0) {
1508
+ window.alert(t('schedule.noUnscheduledLeafTasks', { parentTitle: payload.parentTitle }));
1509
+ return;
1510
+ }
1511
+ setScheduleParentDropPrompt(payload);
1512
+ }, [tasks, todayDateOnly]);
1513
+ const confirmScheduleParentDrop = useCallback(async () => {
1514
+ if (!scheduleParentDropPrompt || scheduleParentDropBusy)
1515
+ return;
1516
+ setScheduleParentDropBusy(true);
1517
+ try {
1518
+ const { targetDate, toScheduleLeafTaskIds } = scheduleParentDropPrompt;
1519
+ const dayTasks = tasks
1520
+ .filter((task) => task.scheduledDate === targetDate && !toScheduleLeafTaskIds.includes(task.id))
1521
+ .sort((a, b) => {
1522
+ const orderA = typeof a.orderInDay === 'number' ? a.orderInDay : Number.MAX_SAFE_INTEGER;
1523
+ const orderB = typeof b.orderInDay === 'number' ? b.orderInDay : Number.MAX_SAFE_INTEGER;
1524
+ if (orderA !== orderB)
1525
+ return orderA - orderB;
1526
+ return String(a.createdAt || '').localeCompare(String(b.createdAt || ''));
1527
+ });
1528
+ let nextOrder = dayTasks.length;
1529
+ for (const taskId of toScheduleLeafTaskIds) {
1530
+ await handleUpdateTask(taskId, {
1531
+ scheduledDate: targetDate,
1532
+ scheduledWeekKey: dateOnlyToIsoWeekKey(targetDate),
1533
+ orderInDay: nextOrder++
1534
+ });
1535
+ }
1536
+ setScheduleParentDropPrompt(null);
1537
+ await fetchTasks(true);
1538
+ }
1539
+ finally {
1540
+ setScheduleParentDropBusy(false);
1541
+ }
1542
+ }, [scheduleParentDropPrompt, scheduleParentDropBusy, tasks, handleUpdateTask, fetchTasks]);
1543
+ const cancelScheduleParentDrop = useCallback(() => {
1544
+ if (scheduleParentDropBusy)
1545
+ return;
1546
+ setScheduleParentDropPrompt(null);
1547
+ }, [scheduleParentDropBusy]);
1548
+ const scheduleMonthDate = useMemo(() => {
1549
+ const [yearRaw, monthRaw] = scheduleCalendarMonth.split('-');
1550
+ const year = Number(yearRaw);
1551
+ const month = Number(monthRaw);
1552
+ if (!Number.isInteger(year) || !Number.isInteger(month) || month < 1 || month > 12) {
1553
+ return new Date(selectedDateObj.getFullYear(), selectedDateObj.getMonth(), 1);
1554
+ }
1555
+ return new Date(year, month - 1, 1);
1556
+ }, [scheduleCalendarMonth, selectedDateObj]);
1557
+ const scheduleCalendarMonthLabel = useMemo(() => formatDate(scheduleMonthDate, { month: 'long', year: 'numeric' }, resolvedLocale), [scheduleMonthDate, resolvedLocale]);
1558
+ const scheduleWeekLabel = useMemo(() => formatDate(scheduleWeekStart, { month: 'short', day: 'numeric', year: 'numeric' }, resolvedLocale), [scheduleWeekStart, resolvedLocale]);
1559
+ const calendarCells = useMemo(() => {
1560
+ const start = startOfWeek(scheduleMonthDate, globalWeekStartsOn);
1561
+ return Array.from({ length: 42 }, (_, i) => addDays(start, i));
1562
+ }, [scheduleMonthDate, globalWeekStartsOn]);
1563
+ const calendarTaskSignals = useMemo(() => {
1564
+ const byDate = new Map();
1565
+ for (const task of scheduleBaseTasks) {
1566
+ const dateOnly = task.scheduledDate || '';
1567
+ if (!dateOnly)
1568
+ continue;
1569
+ const isTerminal = task.status === 'done' || task.status === 'cancelled';
1570
+ const isExpired = !isTerminal && dateOnly < todayDateOnly;
1571
+ const existing = byDate.get(dateOnly);
1572
+ if (existing) {
1573
+ existing.count += 1;
1574
+ if (isExpired)
1575
+ existing.hasExpired = true;
1576
+ }
1577
+ else {
1578
+ byDate.set(dateOnly, { count: 1, hasExpired: isExpired });
1579
+ }
1580
+ }
1581
+ return byDate;
1582
+ }, [scheduleBaseTasks, todayDateOnly]);
1583
+ const calendarWeekdayLabels = useMemo(() => (globalWeekStartsOn === 'sunday'
1584
+ ? ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
1585
+ : ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']), [globalWeekStartsOn]);
1586
+ // Calculate columns based on grouping
1587
+ const scheduleColumns = useMemo(() => {
1588
+ const orderedKeys = globalWeekStartsOn === 'sunday'
1589
+ ? ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
1590
+ : SCHEDULE_DAY_KEYS;
1591
+ const visibleKeys = scheduleShowWeekends
1592
+ ? orderedKeys
1593
+ : ['mon', 'tue', 'wed', 'thu', 'fri'];
1594
+ const days = visibleKeys.map((key) => {
1595
+ const date = parseDateOnlyLocal(scheduleDates[key]) || scheduleWeekStart;
1596
+ const dateOnly = scheduleDates[key];
1597
+ const isWeekend = key === 'sat' || key === 'sun';
1598
+ return {
1599
+ value: key,
1600
+ label: `${SCHEDULE_DAY_LABELS[key]} ${formatDate(date, { month: 'short', day: 'numeric' }, resolvedLocale)}`,
1601
+ color: isWeekend ? '#1e3a8a' : '#3b82f6',
1602
+ icon: 'Calendar',
1603
+ date: dateOnly,
1604
+ isPast: dateOnly < todayDateOnly,
1605
+ isSelected: dateOnly === scheduleSelectedDate,
1606
+ isWeekend
1607
+ };
1608
+ });
1609
+ const hasExpiredTasks = expiredColumnCandidates.length > 0;
1610
+ return [
1611
+ ...(scheduleShowBacklog ? [{ value: 'backlog', label: 'Backlog', color: '#64748b', icon: 'Inbox' }] : []),
1612
+ ...(hasExpiredTasks ? [{ value: 'expired', label: 'Expired', color: '#ef4444', icon: 'AlertTriangle' }] : []),
1613
+ ...days
1614
+ ];
1615
+ }, [scheduleShowWeekends, scheduleShowBacklog, scheduleDates, scheduleWeekStart, expiredColumnCandidates.length, todayDateOnly, scheduleSelectedDate, globalWeekStartsOn, resolvedLocale]);
1616
+ const kanbanColumns = useMemo(() => {
1617
+ switch (groupBy) {
1618
+ case 'category':
1619
+ return activeCategories;
1620
+ case 'type':
1621
+ return (activeTypes || []).map(t => ({
1622
+ ...t,
1623
+ icon: TypeMeta[t.value]?.icon,
1624
+ color: TypeMeta[t.value]?.color
1625
+ }));
1626
+ case 'priority':
1627
+ return priorities;
1628
+ case 'complexity':
1629
+ return [
1630
+ { value: 1, label: 'Tiny', icon: 'Gauge', color: '#10b981' },
1631
+ { value: 2, label: 'Low', icon: 'Gauge', color: '#14b8a6' },
1632
+ { value: 3, label: 'Medium', icon: 'Gauge', color: '#3b82f6' },
1633
+ { value: 4, label: 'High', icon: 'Gauge', color: '#8b5cf6' },
1634
+ { value: 5, label: 'Epic', icon: 'Gauge', color: '#d946ef' }
1635
+ ];
1636
+ case 'approach':
1637
+ return approaches;
1638
+ case 'assignee':
1639
+ return [
1640
+ { value: 'agent', label: 'AI', icon: 'Bot', color: '#8b5cf6' },
1641
+ { value: 'user', label: 'User', icon: 'User', color: '#22c55e' },
1642
+ { value: 'unassigned', label: 'Unassigned', icon: 'Circle', color: '#64748b' }
1643
+ ];
1644
+ case 'status':
1645
+ return [
1646
+ { value: 'task', label: 'To Do', icon: 'Circle', color: 'blue-200' },
1647
+ { value: 'on-hold', label: 'Blocked', icon: 'Pause', color: 'amber-500' },
1648
+ { value: 'in-progress', label: 'In Progress', icon: 'Play', color: 'blue-500' },
1649
+ { value: 'review', label: 'Ready for Review', icon: 'Sparkles', color: 'violet-500' },
1650
+ { value: 'completed', label: 'Completed', icon: 'Check', color: 'green-500' }
1651
+ ];
1652
+ case 'hierarchy': {
1653
+ if (parentTaskFilterMode === 'unlinked') {
1654
+ return [{
1655
+ value: 'standalone',
1656
+ label: 'Unlinked',
1657
+ icon: 'Circle',
1658
+ color: '#64748b'
1659
+ }];
1660
+ }
1661
+ const levels = Array.from({ length: hierarchyDepthTasks.length > 0 ? maxHierarchyDepth + 1 : 0 }, (_, index) => index + 1);
1662
+ const hierarchyColumns = levels.map((level) => ({
1663
+ value: level,
1664
+ label: `Level ${level}`,
1665
+ icon: 'Layers',
1666
+ color: level === 1 ? '#3b82f6' : '#64748b'
1667
+ }));
1668
+ if (parentTaskFilterMode === 'all' && hierarchyShowUnlinkedColumn) {
1669
+ hierarchyColumns.push({
1670
+ value: 'standalone',
1671
+ label: 'Unlinked',
1672
+ icon: 'Circle',
1673
+ color: '#64748b'
1674
+ });
1675
+ }
1676
+ return hierarchyColumns;
1677
+ }
1678
+ case 'schedule':
1679
+ return scheduleColumns;
1680
+ default:
1681
+ return activeCategories;
1682
+ }
1683
+ }, [groupBy, activeCategories, activeTypes, priorities, approaches, maxHierarchyDepth, parentTaskFilterMode, scheduleColumns, hierarchyDepthTasks.length, hierarchyShowUnlinkedColumn]);
1684
+ return (_jsxs("div", { className: `${styles.standaloneWrapper} ${zenMode ? styles.zenModeEnabled : ''}`, "data-theme": currentTheme, children: [_jsxs("div", { className: `${styles.header} ${styles.standaloneHeader} `, children: [_jsxs("div", { className: `${styles.headerTitle} ${styles.standaloneTitle}`, children: [_jsx("img", { src: logo, alt: "Taskforce Logo", className: styles.brandIcon, onError: (e) => {
1685
+ const target = e.currentTarget;
1686
+ if (target.src !== favicon) {
1687
+ target.src = favicon;
1688
+ }
1689
+ else {
1690
+ target.style.display = 'none';
1691
+ }
1692
+ } }), _jsx("span", { children: "TaskForce" }), projectName && (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.projectSlash, children: "/" }), _jsx("span", { className: styles.projectName, title: `Workspace ID: ${currentWorkspaceId}`, children: projectName })] })), _jsx("span", { className: styles.taskCountBadge, title: activeCountTitle, children: activeCountLabel }), shouldShowSyncStatus && (_jsx("button", { className: styles.headerActionBtn, onClick: () => setShowSyncStatusModal(true), title: `Cloud sync status: ${headerSyncStatusMeta.label} | Pull: ${formattedLastPullTime} | Push: ${formattedLastPushTime}${realtimeSyncEnabled ? ` | Realtime: ${realtimeConnectionState} (${realtimeSyncFlagSource}) dup=${realtimeTelemetry.duplicateDiscarded} ooo=${realtimeTelemetry.outOfOrderDiscarded} invalid=${realtimeTelemetry.invalidDiscarded}` : ''}`, style: {
1693
+ marginLeft: '6px',
1694
+ height: '24px',
1695
+ width: '24px',
1696
+ padding: 0,
1697
+ borderRadius: '999px',
1698
+ border: `1px solid ${headerSyncStatusMeta.border}`,
1699
+ background: headerSyncStatusMeta.background,
1700
+ color: headerSyncStatusMeta.color,
1701
+ display: 'inline-flex',
1702
+ alignItems: 'center',
1703
+ justifyContent: 'center'
1704
+ }, children: headerSyncStatusMeta.icon === 'off' ? (_jsx(CloudOff, { size: 12 })) : (_jsx(Cloud, { size: 12 })) })), props.loadingTasks && (_jsx("div", { style: { marginLeft: '8px', display: 'flex', alignItems: 'center' }, children: _jsx(Loader2, { size: 14, className: styles.spinner }) })), successBanner && (_jsxs("div", { className: styles.headerSuccess, children: [_jsx(Check, { size: 14 }), _jsx("span", { children: successBanner.message })] }))] }), _jsxs("div", { className: styles.headerActions, style: { gap: '16px' }, children: [_jsxs("div", { className: styles.searchContainer, style: { width: '240px' }, children: [_jsx(Search, { size: 16, className: styles.searchIcon }), _jsx("input", { type: "text", className: `${styles.searchInput} ${hasSearchFilter ? styles.searchActive : ''}`, placeholder: t('standalone.searchPlaceholder'), value: searchQuery, onChange: (e) => setSearchQuery(e.target.value) }), searchQuery && (_jsx("button", { className: styles.clearSearchBtn, onClick: () => setSearchQuery(''), title: t('standalone.clearSearchTitle'), children: _jsx(X, { size: 14 }) }))] }), _jsxs("div", { className: styles.groupByContainer, children: [_jsxs("span", { className: styles.groupByLabel, children: [_jsx(ArrowUpDown, { size: 12 }), " ", t('standalone.sortLabel')] }), _jsxs("select", { className: `${styles.select} ${styles.groupBySelect} `, value: sortBy, onChange: (e) => setSortBy(e.target.value), children: [_jsx("option", { value: "created", children: t('standalone.sortCreated') }), _jsx("option", { value: "updated", children: t('standalone.sortUpdated') }), _jsx("option", { value: "priority", children: t('standalone.sortPriority') }), _jsx("option", { value: "complexity", children: t('standalone.sortComplexity') })] }), _jsx("button", { className: `${styles.headerActionBtn} ${styles.sortDirectionBtn}`, onClick: () => props.toggleSortOrder(), title: props.sortOrder === 'desc' ? t('standalone.sortDirectionDescTitle') : t('standalone.sortDirectionAscTitle'), children: props.sortOrder === 'desc' ? _jsx(ArrowDown, { size: 14 }) : _jsx(ArrowUp, { size: 14 }) })] }), _jsx("div", { className: styles.headerDivider }), groupBy !== 'docs' && (_jsxs("div", { className: styles.groupByContainer, children: [_jsxs("span", { className: styles.groupByLabel, children: [_jsx(Layers, { size: 12 }), " ", t('standalone.groupLabel')] }), _jsxs("select", { className: `${styles.select} ${styles.groupBySelect} `, value: groupBy, onChange: (e) => setGroupBy(e.target.value), children: [_jsx("option", { value: "category", children: t('standalone.groupCategory') }), _jsx("option", { value: "type", children: t('standalone.groupType') }), _jsx("option", { value: "priority", children: t('standalone.groupPriority') }), _jsx("option", { value: "complexity", children: t('standalone.groupComplexity') }), _jsx("option", { value: "approach", children: t('standalone.groupApproach') }), _jsx("option", { value: "assignee", children: t('standalone.groupAssignee') }), _jsx("option", { value: "status", children: t('standalone.groupStatus') }), _jsx("option", { value: "hierarchy", children: t('standalone.groupHierarchy') }), _jsx("option", { value: "schedule", children: t('standalone.groupSchedule') })] })] })), _jsx("button", { className: `${styles.headerActionBtn} ${groupBy === 'docs' ? styles.headerActionBtnActive : ''}`, onClick: () => setGroupBy(groupBy === 'docs' ? lastNonDocsGroupByRef.current : 'docs'), title: groupBy === 'docs' ? 'Exit Docs view' : 'Open Docs view', children: _jsx(FileText, { size: 15 }) }), _jsxs("button", { className: `${styles.headerActionBtn} ${emptyColumnMode !== 'show' ? styles.headerActionBtnActive : ''} `, onClick: () => {
1705
+ setEmptyColumnMode(prev => {
1706
+ if (prev === 'show')
1707
+ return 'collapse';
1708
+ if (prev === 'collapse')
1709
+ return 'hide';
1710
+ return 'show';
1711
+ });
1712
+ }, title: emptyColumnMode === 'show' ? t('standalone.showEmptyColumns') :
1713
+ emptyColumnMode === 'collapse' ? t('standalone.compressEmptyColumns') :
1714
+ t('standalone.hideEmptyColumns'), children: [emptyColumnMode === 'show' && _jsx(Maximize2, { size: 16 }), emptyColumnMode === 'collapse' && _jsx(Minimize2, { size: 16 }), emptyColumnMode === 'hide' && _jsx(EyeOff, { size: 16 })] }), _jsx("button", { className: `${styles.headerActionBtn} ${compressedCards ? styles.headerActionBtnActive : ''}`, onClick: () => setCompressedCards(!compressedCards), title: compressedCards ? t('standalone.expandCards') : t('standalone.compressCards'), children: compressedCards ? _jsx(ChevronsDown, { size: 16 }) : _jsx(ChevronsUp, { size: 16 }) }), _jsx("button", { className: `${styles.headerActionBtn} ${zenMode ? styles.headerActionBtnActive : ''}`, onClick: () => setZenMode(), title: zenMode ? t('standalone.exitZenMode') : t('standalone.enterZenMode'), children: _jsx(Zap, { size: 16, fill: zenMode ? "currentColor" : "none" }) }), _jsx("div", { className: styles.headerDivider }), _jsx("button", { className: `${styles.headerActionBtn} ${showFilters ? styles.headerActionBtnActive : ''} ${hasDropdownFilters ? styles.filterGlow : ''}`, onClick: () => setShowFilters(!showFilters), title: t('standalone.toggleFilters'), children: _jsx(Filter, { size: 16 }) }), _jsxs("button", { className: styles.primaryUpdateBtn, onClick: () => {
1715
+ resetForm();
1716
+ setActiveTab('add');
1717
+ }, children: [_jsx(Plus, { size: 16 }), " ", t('standalone.addTask')] }), _jsxs("div", { className: styles.accountMenuWrap, ref: accountMenuRef, children: [_jsx("button", { className: `${styles.headerActionBtn} ${styles.avatarBtn}`, onClick: () => setShowAccountMenu(prev => !prev), title: isAuthenticated ? 'Account' : 'Account (Not signed in)', "aria-label": isAuthenticated ? 'Account' : 'Account (Not signed in)', "aria-haspopup": "menu", "aria-expanded": showAccountMenu, children: _jsx("span", { className: styles.avatarBadge, "aria-hidden": "true", children: isAuthenticated ? 'U' : '?' }) }), showAccountMenu && (_jsxs("div", { className: styles.accountMenu, role: "menu", "aria-label": "Account menu", children: [_jsxs("div", { className: styles.accountMenuSection, children: [_jsx("div", { className: styles.accountMenuSectionLabel, children: "Account" }), _jsx("div", { className: styles.accountMenuHint, children: isAuthBootstrapPending
1718
+ ? 'Checking sign-in status...'
1719
+ : isAuthenticated
1720
+ ? (_jsx(_Fragment, { children: hasAuthIdentity
1721
+ ? _jsxs(_Fragment, { children: ["Signed in as ", _jsx("span", { className: styles.accountIdentityEmail, children: authIdentityLabel })] })
1722
+ : 'Signed in' }))
1723
+ : 'Not signed in' })] }), canManageCloudWorkspaces && (_jsxs("div", { className: styles.accountMenuSection, children: [_jsx("div", { className: styles.accountMenuSectionLabel, children: "Workspaces (Owned + Invited)" }), availableWorkspaces.length === 0 && (_jsx("div", { className: styles.accountMenuHint, children: "No workspaces found for this account yet." })), availableWorkspaces.map((workspace) => (_jsxs("button", { className: `${styles.accountMenuItem} ${workspace.id === currentWorkspaceId ? styles.accountMenuItemActive : ''}`, onClick: () => handleSwitchWorkspace(workspace.id), role: "menuitem", disabled: workspaceActionBusy || workspace.id === currentWorkspaceId, title: workspace.description || workspace.name, children: [_jsx(Folder, { size: 15 }), _jsxs("span", { style: { display: 'flex', flexDirection: 'column', gap: '2px' }, children: [_jsx("span", { children: workspace.name }), _jsx("span", { className: styles.accountMenuMeta, children: workspace.role })] })] }, workspace.id))), _jsxs("button", { className: styles.accountMenuItem, onClick: () => {
1724
+ setWorkspaceActionError('');
1725
+ setWorkspacePanelOpen((prev) => !prev);
1726
+ }, role: "menuitem", disabled: workspaceActionBusy, children: [_jsx(Plus, { size: 15 }), workspacePanelOpen ? 'Close Workspace Form' : 'Create Workspace'] }), workspacePanelOpen && (_jsxs("div", { className: styles.accountMenuWorkspaceForm, children: [_jsx("input", { className: styles.input, value: workspaceFormName, onChange: (event) => setWorkspaceFormName(event.target.value), placeholder: "Workspace name", disabled: workspaceActionBusy }), _jsx("input", { className: styles.input, value: workspaceFormDescription, onChange: (event) => setWorkspaceFormDescription(event.target.value), placeholder: "Workspace description (optional)", disabled: workspaceActionBusy }), _jsx("button", { className: styles.primaryUpdateBtn, onClick: handleCreateWorkspace, disabled: workspaceActionBusy, children: workspaceActionBusy ? 'Saving...' : 'Save Workspace' })] })), workspaceActionError && (_jsx("div", { className: styles.accountMenuError, children: workspaceActionError }))] })), canOpenTeamManagement && (_jsxs("button", { className: styles.accountMenuItem, onClick: () => { void openTeamManagementModal(); }, role: "menuitem", children: [_jsx(Users, { size: 15 }), "Team Management"] })), _jsxs("button", { className: styles.accountMenuItem, onClick: () => {
1727
+ setShowAccountMenu(false);
1728
+ setActiveTab('settings');
1729
+ setSettingsSection('general');
1730
+ }, role: "menuitem", children: [_jsx(Settings, { size: 15 }), "Workspace Settings"] }), _jsxs("button", { className: styles.accountMenuItem, onClick: () => {
1731
+ setShowAccountMenu(false);
1732
+ setShowAccountHub(true);
1733
+ }, role: "menuitem", children: [_jsx(User, { size: 15 }), "Account Hub"] }), _jsxs("button", { className: styles.accountMenuItem, onClick: () => {
1734
+ setShowAccountMenu(false);
1735
+ setShowHelpModal(true);
1736
+ }, role: "menuitem", children: [_jsx(HelpCircle, { size: 15 }), "Help & Tutorial"] }), runtimeMode === 'local' && !isAuthenticated && !isAuthBootstrapPending && (_jsxs("button", { className: styles.accountMenuItem, onClick: () => {
1737
+ openSharedLoginRoute('login');
1738
+ }, role: "menuitem", children: [_jsx(User, { size: 15 }), "Sign In / Register"] })), isAuthenticated && (_jsxs("button", { className: styles.accountMenuItem, onClick: async () => {
1739
+ setShowAccountMenu(false);
1740
+ await logout();
1741
+ }, role: "menuitem", children: [_jsx(LogOut, { size: 15 }), "Sign Out"] }))] }))] })] })] }), authBlocked && (_jsx("div", { className: styles.authBlockedBanner, children: "Authentication required for this environment. Use the Account menu to sign in." })), showFilters && (_jsxs("div", { className: styles.filterToolbar, style: {
1742
+ padding: '8px 24px',
1743
+ borderBottom: '1px solid var(--border-color)',
1744
+ background: 'var(--bg-secondary)',
1745
+ display: 'flex',
1746
+ alignItems: 'center',
1747
+ gap: '12px',
1748
+ flexWrap: 'wrap',
1749
+ position: 'relative',
1750
+ zIndex: 50
1751
+ }, children: [_jsx("span", { style: { fontSize: '12px', fontWeight: 600, color: 'var(--text-secondary)', marginRight: '4px' }, children: t('standalone.filtersCaps') }), _jsx(MultiSelectFilter, { label: t('standalone.categoryLabel'), options: activeCategories.filter(c => !c.disabled), selected: filterCategories, onChange: (vals) => setFilterCategories(vals), variant: "value" }), _jsx(MultiSelectFilter, { label: t('standalone.typeLabel'), options: activeTypes.map(t => ({ value: t.value, label: t.label })), selected: filterTypes, onChange: (vals) => setFilterTypes(vals), variant: "value" }), _jsx(MultiSelectFilter, { label: t('standalone.priorityLabel'), options: priorities, selected: filterPriorities, onChange: (vals) => setFilterPriorities(vals), variant: "value" }), _jsx(MultiSelectFilter, { label: t('standalone.statusLabel'), options: STATUS_OPTIONS, selected: filterStatus, onChange: (vals) => setFilterStatus(vals), variant: "value" }), assigneeFilterEnabled && (_jsx(MultiSelectFilter, { label: t('standalone.assigneeLabel'), options: [
1752
+ { value: 'agent', label: 'AI' },
1753
+ { value: 'user', label: 'User' },
1754
+ { value: 'unassigned', label: 'Unassigned' }
1755
+ ], selected: filterAssignees, onChange: (vals) => setFilterAssignees(vals), variant: "value" })), _jsx("div", { className: styles.headerDivider, style: { height: '16px', margin: '0 8px' } }), _jsxs("div", { className: styles.groupByContainer, children: [_jsxs("span", { className: styles.groupByLabel, children: [_jsx(Layers, { size: 12 }), " ", t('standalone.linksLabel')] }), _jsxs("select", { className: `${styles.select} ${styles.groupBySelect}`, value: parentTaskFilterMode, onChange: (e) => setParentTaskFilterMode(e.target.value), title: t('standalone.chooseLinksTitle'), children: [_jsx("option", { value: "all", children: t('standalone.linksAll') }), _jsx("option", { value: "parents", children: t('standalone.linksParents') }), _jsx("option", { value: "linked", children: t('standalone.linksLinked') }), _jsx("option", { value: "unlinked", children: t('standalone.linksUnlinked') })] })] }), _jsxs("label", { className: styles.archiveToggle, title: t('standalone.includeArchiveTitle'), children: [_jsx("input", { type: "checkbox", checked: showArchive, onChange: (e) => {
1756
+ setShowArchive(e.target.checked);
1757
+ if (e.target.checked)
1758
+ fetchArchive();
1759
+ } }), _jsx("span", { children: t('standalone.includeArchive') })] }), _jsx("div", { style: { flex: 1 } }), _jsxs("button", { onClick: clearFilters, style: {
1760
+ background: 'none',
1761
+ border: 'none',
1762
+ color: 'var(--text-secondary)',
1763
+ cursor: 'pointer',
1764
+ fontSize: '12px',
1765
+ display: 'flex',
1766
+ alignItems: 'center',
1767
+ gap: '4px'
1768
+ }, children: [_jsx(X, { size: 12 }), " ", t('standalone.clearFilters')] })] })), groupBy === 'docs' ? (_jsx("div", { className: `${styles.standalonePage} ${styles.standaloneContent}`, style: { position: 'relative', display: 'flex', flex: 1, minHeight: 0, overflow: 'hidden' }, children: _jsx(DocumentWorkspace, { tasks: tasks, apiBaseUrl: props.config?.apiBaseUrl || '', requestedDocPath: requestedDocPath, onRequestedDocHandled: () => setRequestedDocPath(null) }) })) : (_jsxs("div", { className: `${styles.standalonePage} ${styles.standaloneContent} `, style: {
1769
+ position: 'relative',
1770
+ opacity: props.loadingTasks ? 0.7 : 1,
1771
+ transition: 'opacity 0.2s ease, padding-right 220ms cubic-bezier(0.4, 0, 0.2, 1)',
1772
+ display: 'flex',
1773
+ flex: 1,
1774
+ minHeight: 0,
1775
+ paddingRight: (groupBy === 'schedule' && scheduleSidebarOpen) || (groupBy === 'hierarchy' && hierarchySidebarOpen) ? '332px' : 0
1776
+ }, children: [_jsxs("div", { style: { position: 'relative', width: '100%', minWidth: 0, flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: [_jsx("div", { style: {
1777
+ position: 'absolute',
1778
+ top: 0,
1779
+ right: 0,
1780
+ width: '100%',
1781
+ height: '100%',
1782
+ backgroundImage: `url(${taskforceBg})`,
1783
+ backgroundRepeat: 'no-repeat',
1784
+ backgroundPosition: 'top right',
1785
+ backgroundSize: '60%',
1786
+ opacity: 0.2,
1787
+ pointerEvents: 'none',
1788
+ zIndex: 0
1789
+ } }), boardVisibleTasks.length === 0 && groupBy !== 'schedule' ? (_jsxs("div", { className: styles.emptyState, children: [_jsx(Layers, { size: 48 }), _jsx("p", { children: groupBy === 'hierarchy' && hierarchyRootTaskId ? 'No tasks match this subtree with current filters.' : t('standalone.noTasksMatchFilters') }), _jsx("p", { style: { fontSize: '12px', color: 'var(--text-secondary)' }, children: groupBy === 'hierarchy' && hierarchyRootTaskId
1790
+ ? `Focused on ${selectedHierarchyRootTask?.title || hierarchyRootTaskId}. Try clearing subtree focus or broadening filters.`
1791
+ : t('standalone.showingWithLinksHint', { parentFilterLabel }) })] })) : (_jsx(TaskKanban, { tasks: boardVisibleTasks, allTasks: kanbanAllTasks, hierarchyTasks: hierarchyTasks, columns: kanbanColumns, groupBy: groupBy, scheduleDates: scheduleDates, searchQuery: searchQuery, filterCategories: filterCategories, filterTypes: filterTypes, filterPriorities: filterPriorities, filterStatus: filterStatus, filterAssignees: filterAssignees, assigneeFilterEnabled: assigneeFilterEnabled, scheduleFilteredTaskIds: Array.from(scheduleFilteredTaskIds), copiedId: copiedId, availableSpecialists: availableSpecialists, taxonomies: taxonomies, priorities: priorities, approaches: approaches, onUpdateTask: handleUpdateTask, onTaskClick: (task) => handleEdit(task), onOpenTaskById: handleOpenTaskById, onCopyId: handleCopyId, onToggleInProgress: handleToggleInProgress, onToggleReview: handleToggleReview, onToggleComplete: handleToggleComplete, onToggleCancel: handleToggleCancel, onArchiveTask: handleArchiveTask, onUnarchive: handleUnarchive, onDelete: handleDelete, onUnlinkChildTask: handleUnlinkChildTask, onClearParentTask: handleClearParentTask, onAddTaskToColumn: handleAddTaskToColumn, onScheduleParentDrop: handleScheduleParentDrop, onScheduleDaySelected: (dateOnly) => {
1792
+ setScheduleSelectedDate(dateOnly);
1793
+ const parsed = parseDateOnlyLocal(dateOnly);
1794
+ if (parsed) {
1795
+ setScheduleCalendarMonth(`${parsed.getFullYear()}-${String(parsed.getMonth() + 1).padStart(2, '0')}`);
1796
+ }
1797
+ }, persistedScrollLeft: groupBy === 'schedule' ? scheduleScrollLeft : undefined, onScrollLeftChange: (scrollLeft) => {
1798
+ if (groupBy !== 'schedule')
1799
+ return;
1800
+ setScheduleScrollLeft(scrollLeft);
1801
+ }, emptyColumnMode: emptyColumnMode, categories: activeCategories, compressed: compressedCards, recentlyChangedTaskIds: props.recentlyChangedTaskIds, hierarchyChildOrderMode: hierarchyChildOrderMode, sortBy: sortBy, sortOrder: props.sortOrder }, `${groupBy}-${kanbanColumns.map(c => String(c.value)).join('|')}-${scheduleDates.mon}`)), groupBy === 'schedule' && !scheduleSidebarOpen && (_jsx("button", { type: "button", className: styles.headerActionBtn, onClick: () => setScheduleSidebarOpen(true), title: "Show Schedule Sidebar", style: {
1802
+ position: 'absolute',
1803
+ top: '10px',
1804
+ right: '10px',
1805
+ zIndex: 20,
1806
+ border: '1px solid var(--border-color)',
1807
+ background: 'var(--bg-secondary)'
1808
+ }, children: _jsx(PanelRightOpen, { size: 16 }) })), groupBy === 'hierarchy' && !hierarchySidebarOpen && (_jsx("button", { type: "button", className: styles.headerActionBtn, onClick: () => setHierarchySidebarOpen(true), title: "Show Hierarchy Sidebar", style: {
1809
+ position: 'absolute',
1810
+ top: '10px',
1811
+ right: '10px',
1812
+ zIndex: 20,
1813
+ border: '1px solid var(--border-color)',
1814
+ background: 'var(--bg-secondary)'
1815
+ }, children: _jsx(PanelRightOpen, { size: 16 }) }))] }), groupBy === 'hierarchy' && (_jsxs("aside", { className: styles.appScrollbar, style: {
1816
+ position: 'absolute',
1817
+ top: 0,
1818
+ right: 0,
1819
+ bottom: 0,
1820
+ width: '320px',
1821
+ minWidth: '320px',
1822
+ borderLeft: '1px solid var(--border-color)',
1823
+ background: 'var(--bg-secondary)',
1824
+ padding: '12px',
1825
+ display: 'flex',
1826
+ flexDirection: 'column',
1827
+ gap: '12px',
1828
+ zIndex: 40,
1829
+ overflowY: 'auto',
1830
+ transform: hierarchySidebarOpen ? 'translateX(0)' : 'translateX(108%)',
1831
+ pointerEvents: hierarchySidebarOpen ? 'auto' : 'none',
1832
+ transition: 'transform 220ms cubic-bezier(0.4, 0, 0.2, 1)',
1833
+ willChange: 'transform'
1834
+ }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', fontWeight: 600 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx(Layers, { size: 16 }), _jsx("span", { children: "Hierarchy Controls" })] }), _jsx("button", { type: "button", className: styles.headerActionBtn, onClick: () => setHierarchySidebarOpen(false), title: "Hide Hierarchy Sidebar", children: _jsx(PanelRightClose, { size: 16 }) })] }), _jsxs("div", { style: {
1835
+ border: '1px solid var(--border-color)',
1836
+ borderRadius: '10px',
1837
+ padding: '10px',
1838
+ display: 'flex',
1839
+ flexDirection: 'column',
1840
+ gap: '8px'
1841
+ }, children: [_jsx("div", { style: { fontSize: '12px', fontWeight: 600 }, children: "Focus Subtree" }), _jsx("input", { type: "text", className: styles.input, placeholder: "Search parent tasks...", value: hierarchyRootSearch, onChange: (e) => setHierarchyRootSearch(e.target.value), onKeyDown: (e) => {
1842
+ if (e.key === 'Enter') {
1843
+ e.preventDefault();
1844
+ applyHierarchySearchSelection();
1845
+ }
1846
+ } }), _jsx("div", { style: {
1847
+ border: '1px solid var(--border-color)',
1848
+ borderRadius: '8px',
1849
+ padding: '8px',
1850
+ display: 'flex',
1851
+ flexDirection: 'column',
1852
+ gap: '6px',
1853
+ minHeight: '190px'
1854
+ }, children: hierarchyRootSearch.trim().length > 0 ? (_jsxs("div", { className: styles.appScrollbar, style: {
1855
+ maxHeight: '180px',
1856
+ minHeight: '180px',
1857
+ overflowY: 'auto',
1858
+ border: '1px solid var(--border-color)',
1859
+ borderRadius: '8px',
1860
+ padding: '6px',
1861
+ display: 'flex',
1862
+ flexDirection: 'column',
1863
+ gap: '6px'
1864
+ }, children: [hierarchySearchMatches.slice(0, 24).map((task) => (_jsxs("button", { type: "button", className: styles.headerActionBtn, onClick: () => {
1865
+ setHierarchyRootTaskId(task.id);
1866
+ setHierarchyRootSearch('');
1867
+ if (parentTaskFilterMode === 'unlinked') {
1868
+ setParentTaskFilterMode('all');
1869
+ }
1870
+ }, style: { justifyContent: 'space-between', border: '1px solid var(--border-color)' }, title: task.title, children: [_jsx("span", { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: task.title }), _jsx("span", { style: { opacity: 0.75 }, children: task.id.split('-').slice(-1)[0] })] }, task.id))), hierarchySearchMatches.length === 0 && (_jsxs("div", { style: { fontSize: '12px', color: 'var(--text-secondary)', padding: '4px 2px' }, children: ["No matches for \"", hierarchyRootSearch, "\"."] }))] })) : hierarchyRootTaskId ? (_jsxs(_Fragment, { children: [_jsx("div", { style: { fontSize: '13px', fontWeight: 600, color: 'var(--text-primary)' }, title: selectedHierarchyRootTask?.title || hierarchyRootTaskId, children: selectedHierarchyRootTask?.title || hierarchyRootTaskId }), hierarchySubtreeStats && (_jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px', fontSize: '12px', color: 'var(--text-secondary)' }, children: [_jsxs("div", { style: { border: '1px solid var(--border-color)', borderRadius: '8px', padding: '6px' }, children: [_jsx("strong", { style: { color: 'var(--text-primary)' }, children: hierarchySubtreeStats.total }), " total"] }), _jsxs("div", { style: { border: '1px solid var(--border-color)', borderRadius: '8px', padding: '6px' }, children: [_jsx("strong", { style: { color: 'var(--text-primary)' }, children: hierarchySubtreeStats.active }), " active"] }), _jsxs("div", { style: { border: '1px solid var(--border-color)', borderRadius: '8px', padding: '6px' }, children: [_jsx("strong", { style: { color: 'var(--text-primary)' }, children: hierarchySubtreeStats.terminal }), " done/cancelled"] }), _jsxs("div", { style: { border: '1px solid var(--border-color)', borderRadius: '8px', padding: '6px' }, children: [_jsx("strong", { style: { color: hierarchySubtreeStats.overdue > 0 ? 'var(--color-error, #ef4444)' : 'var(--text-primary)' }, children: hierarchySubtreeStats.overdue }), " overdue"] }), _jsxs("div", { style: { gridColumn: '1 / -1', border: '1px solid var(--border-color)', borderRadius: '8px', padding: '6px' }, children: [_jsx("strong", { style: { color: 'var(--text-primary)' }, children: hierarchySubtreeStats.depth }), " levels deep"] })] })), _jsx("button", { className: styles.headerActionBtn, onClick: () => {
1871
+ setHierarchyRootTaskId('');
1872
+ setHierarchyRootSearch('');
1873
+ }, style: { justifyContent: 'center', border: '1px solid var(--border-color)', background: 'rgba(59,130,246,0.12)' }, children: "Clear subtree focus" })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px', fontSize: '12px', color: 'var(--text-secondary)' }, children: [_jsxs("div", { style: { border: '1px solid var(--border-color)', borderRadius: '8px', padding: '6px' }, children: [_jsx("strong", { style: { color: 'var(--text-primary)' }, children: hierarchyVisibleTasks.length }), " visible"] }), _jsxs("div", { style: { border: '1px solid var(--border-color)', borderRadius: '8px', padding: '6px' }, children: [_jsx("strong", { style: { color: 'var(--text-primary)' }, children: hierarchyVisibleRootCount }), " root parents"] }), _jsxs("div", { style: { border: '1px solid var(--border-color)', borderRadius: '8px', padding: '6px' }, children: [_jsx("strong", { style: { color: 'var(--text-primary)' }, children: hierarchyVisibleUnlinkedCount }), " unlinked"] }), _jsxs("div", { style: { border: '1px solid var(--border-color)', borderRadius: '8px', padding: '6px' }, children: [_jsx("strong", { style: { color: 'var(--text-primary)' }, children: hierarchyDepthTasks.length > 0 ? maxHierarchyDepth + 1 : 0 }), " levels"] })] }), _jsx("div", { style: { fontSize: '12px', color: 'var(--text-secondary)' }, children: hierarchyVisibleLevelCounts.length > 0
1874
+ ? hierarchyVisibleLevelCounts.map((entry) => `L${entry.level}: ${entry.count}`).join(' • ')
1875
+ : 'No linked hierarchy tasks in current filters.' })] })) })] }), _jsxs("div", { style: {
1876
+ border: '1px solid var(--border-color)',
1877
+ borderRadius: '10px',
1878
+ padding: '10px',
1879
+ display: 'flex',
1880
+ flexDirection: 'column',
1881
+ gap: '8px'
1882
+ }, children: [_jsx("div", { style: { fontSize: '12px', fontWeight: 600 }, children: "Display" }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }, children: [_jsx("input", { type: "checkbox", checked: hierarchyShowUnlinkedColumn, onChange: (e) => {
1883
+ const next = e.target.checked;
1884
+ setHierarchyShowUnlinkedColumn(next);
1885
+ if (!next && parentTaskFilterMode === 'unlinked') {
1886
+ setParentTaskFilterMode('all');
1887
+ }
1888
+ } }), "Show unlinked column"] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }, children: [_jsx("input", { type: "checkbox", checked: hierarchyChildOrderMode === 'manual', onChange: (e) => setHierarchyChildOrderMode(e.target.checked ? 'manual' : 'auto') }), "Sort by parent task order"] }), _jsx("div", { style: { fontSize: '11px', color: 'var(--text-tertiary)' }, children: "Keeps siblings in the order defined on the parent task." })] })] })), groupBy === 'schedule' && (_jsxs("aside", { className: styles.appScrollbar, style: {
1889
+ position: 'absolute',
1890
+ top: 0,
1891
+ right: 0,
1892
+ bottom: 0,
1893
+ width: '320px',
1894
+ minWidth: '320px',
1895
+ borderLeft: '1px solid var(--border-color)',
1896
+ background: 'var(--bg-secondary)',
1897
+ padding: '12px',
1898
+ display: 'flex',
1899
+ flexDirection: 'column',
1900
+ gap: '12px',
1901
+ zIndex: 40,
1902
+ overflowY: 'auto',
1903
+ transform: scheduleSidebarOpen ? 'translateX(0)' : 'translateX(108%)',
1904
+ pointerEvents: scheduleSidebarOpen ? 'auto' : 'none',
1905
+ transition: 'transform 220ms cubic-bezier(0.4, 0, 0.2, 1)',
1906
+ willChange: 'transform'
1907
+ }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', fontWeight: 600 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx(CalendarDays, { size: 16 }), _jsx("span", { children: "Schedule Controls" })] }), _jsx("button", { type: "button", className: styles.headerActionBtn, onClick: () => setScheduleSidebarOpen(false), title: "Hide Schedule Sidebar", children: _jsx(PanelRightClose, { size: 16 }) })] }), _jsxs("div", { style: { fontSize: '12px', color: 'var(--text-secondary)' }, children: ["Week of ", scheduleWeekLabel] }), _jsxs("div", { style: {
1908
+ border: '1px solid var(--border-color)',
1909
+ borderRadius: '10px',
1910
+ padding: '10px'
1911
+ }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }, children: [_jsx("button", { className: styles.headerActionBtn, onClick: () => {
1912
+ const prev = new Date(scheduleMonthDate);
1913
+ prev.setMonth(scheduleMonthDate.getMonth() - 1);
1914
+ setScheduleCalendarMonth(`${prev.getFullYear()}-${String(prev.getMonth() + 1).padStart(2, '0')}`);
1915
+ }, title: "Previous month", children: _jsx(ChevronLeft, { size: 14 }) }), _jsx("div", { style: { fontSize: '12px', fontWeight: 600 }, children: scheduleCalendarMonthLabel }), _jsx("button", { className: styles.headerActionBtn, onClick: () => {
1916
+ const next = new Date(scheduleMonthDate);
1917
+ next.setMonth(scheduleMonthDate.getMonth() + 1);
1918
+ setScheduleCalendarMonth(`${next.getFullYear()}-${String(next.getMonth() + 1).padStart(2, '0')}`);
1919
+ }, title: "Next month", children: _jsx(ChevronRight, { size: 14 }) })] }), _jsx("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(7, minmax(0, 1fr))', gap: '4px', marginBottom: '4px' }, children: calendarWeekdayLabels.map((label) => (_jsx("div", { style: { fontSize: '11px', color: 'var(--text-secondary)', textAlign: 'center' }, children: label }, label))) }), _jsx("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(7, minmax(0, 1fr))', gap: '4px' }, children: calendarCells.map((date) => {
1920
+ const dateOnly = toDateOnlyLocal(date);
1921
+ const isOutsideMonth = date.getMonth() !== scheduleMonthDate.getMonth();
1922
+ const weekStartForCell = startOfWeek(date, globalWeekStartsOn);
1923
+ const isSelectedWeek = toDateOnlyLocal(weekStartForCell) === toDateOnlyLocal(scheduleWeekStart);
1924
+ const isSelectedDate = dateOnly === scheduleSelectedDate;
1925
+ const isToday = dateOnly === todayDateOnly;
1926
+ const isPast = dateOnly < todayDateOnly;
1927
+ const signal = calendarTaskSignals.get(dateOnly);
1928
+ const hasScheduledTasks = !!signal;
1929
+ const hasExpiredScheduledTasks = !!signal?.hasExpired;
1930
+ return (_jsxs("button", { onClick: () => {
1931
+ setScheduleSelectedDate(dateOnly);
1932
+ setScheduleCalendarMonth(`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`);
1933
+ }, style: {
1934
+ border: isSelectedDate
1935
+ ? '1px solid #2563eb'
1936
+ : isToday
1937
+ ? '1px solid rgba(245, 158, 11, 0.95)'
1938
+ : '1px solid transparent',
1939
+ borderRadius: '8px',
1940
+ padding: '6px 0',
1941
+ fontSize: '12px',
1942
+ position: 'relative',
1943
+ background: isSelectedDate
1944
+ ? 'rgba(59,130,246,0.22)'
1945
+ : isToday
1946
+ ? 'rgba(245,158,11,0.16)'
1947
+ : isSelectedWeek
1948
+ ? 'rgba(59,130,246,0.18)'
1949
+ : 'transparent',
1950
+ color: isOutsideMonth ? 'var(--text-tertiary)' : 'var(--text-primary)',
1951
+ cursor: 'pointer',
1952
+ fontWeight: isSelectedDate || isToday ? 700 : 500,
1953
+ opacity: isPast ? 0.42 : 1
1954
+ }, title: `${isToday ? `${dateOnly} (Today)` : dateOnly}`
1955
+ + (hasScheduledTasks ? ` • ${signal?.count} scheduled` : '')
1956
+ + (hasExpiredScheduledTasks ? ' • includes expired' : ''), children: [date.getDate(), hasScheduledTasks && (_jsx("span", { style: {
1957
+ position: 'absolute',
1958
+ left: '50%',
1959
+ bottom: '3px',
1960
+ transform: 'translateX(-50%)',
1961
+ width: '5px',
1962
+ height: '5px',
1963
+ borderRadius: '999px',
1964
+ background: hasExpiredScheduledTasks ? '#ef4444' : '#3b82f6',
1965
+ opacity: isOutsideMonth ? 0.6 : 0.95
1966
+ } }))] }, dateOnly));
1967
+ }) }), _jsx("div", { style: { display: 'flex', justifyContent: 'space-between', marginTop: '10px' }, children: _jsx("button", { className: styles.headerActionBtn, onClick: () => {
1968
+ const now = new Date();
1969
+ setScheduleSelectedDate(toDateOnlyLocal(now));
1970
+ setScheduleCalendarMonth(`${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`);
1971
+ }, title: "Jump to current week", children: "Today" }) })] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }, children: [_jsx("input", { type: "checkbox", checked: scheduleShowWeekends, onChange: (e) => setScheduleShowWeekends(e.target.checked) }), "Show weekends"] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }, children: [_jsx("input", { type: "checkbox", checked: scheduleShowBacklog, onChange: (e) => setScheduleShowBacklog(e.target.checked) }), "Show backlog"] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }, children: [_jsx("input", { type: "checkbox", checked: scheduleOnlyExpired, onChange: (e) => setScheduleOnlyExpired(e.target.checked) }), "Only expired/overdue"] }), expiredScheduledCount > 0 && (_jsxs("div", { style: {
1972
+ border: '1px solid var(--border-color)',
1973
+ borderRadius: '10px',
1974
+ padding: '10px',
1975
+ display: 'flex',
1976
+ flexDirection: 'column',
1977
+ gap: '8px'
1978
+ }, children: [_jsx("div", { style: { fontSize: '12px', fontWeight: 600 }, children: "Expired Tasks" }), _jsxs("div", { style: { fontSize: '12px', color: 'var(--text-secondary)', display: 'flex', flexDirection: 'column', gap: '2px' }, children: [_jsxs("span", { children: [expiredScheduledCount, " expired"] }), _jsxs("span", { children: [overdueDueCount, " overdue"] })] }), _jsx("button", { className: styles.headerActionBtn, onClick: handleMoveExpiredToSelectedWeek, disabled: scheduleBulkBusy || expiredLeafCandidates.length === 0, title: "Move expired leaf tasks to the selected week while keeping weekday alignment", style: { justifyContent: 'flex-start', border: '1px solid var(--border-color)' }, children: scheduleBulkBusy ? 'Working...' : 'Schedule to selected week' }), _jsx("button", { className: styles.headerActionBtn, onClick: handleMoveExpiredToBacklog, disabled: scheduleBulkBusy || expiredRecoveryCandidates.length === 0, title: "Unschedule expired/overdue tasks back to backlog", style: { justifyContent: 'flex-start', border: '1px solid var(--border-color)' }, children: scheduleBulkBusy ? 'Working...' : 'Unschedule to backlog' })] }))] }))] })), _jsxs(Modal, { isOpen: activeTab === 'add', onClose: handleCloseModal, title: editingTaskId ? 'Edit Task' : 'New Task', size: "md", theme: currentTheme, draggable: true, closeOnOverlayClick: false, children: [_jsx(TaskActionHeader, { editingTaskId: editingTaskId, loading: loading, title: title, currentTask: currentTask, handleSubmit: handleSubmit, resetForm: resetForm, handleCopyId: handleCopyId, copiedId: copiedId, tasks: tasks, handleToggleInProgress: handleToggleInProgress, handleToggleReview: handleToggleReview, handleToggleComplete: handleToggleComplete, handleToggleCancel: handleToggleCancel, handleArchiveTask: handleArchiveTask }), _jsx(TaskForm, { editingTaskId: editingTaskId, error: error, title: title, description: description, category: category, type: type, priority: priority, complexity: complexity, manualComplexityEnabled: manualComplexityEnabled, approach: approach, assignee: assignee, scheduledDate: scheduledDate, dueDate: dueDate, parentTaskIdInput: parentTaskIdInput, addChildTaskIdInput: addChildTaskIdInput, formTaxonomies: formTaxonomies, onTaxonomyChange: (id, val) => setFormTaxonomies(prev => ({ ...prev, [id]: val })), onParentTaskIdInputChange: setParentTaskIdInput, onAddChildTaskIdInputChange: setAddChildTaskIdInput, taxonomies: taxonomies, selectedSpecialists: selectedSpecialists, comments: comments, newCommentText: newCommentText, contextFiles: screenshots, contextLinkBrowsePath: contextLinkBrowsePath, descriptionFocused: descriptionFocused, showMarkdownHelp: showMarkdownHelp, showSpecialists: showSpecialists, showChecklist: showChecklist, checklistEnabled: checklistDropdownEnabled, showTaskRelationships: showTaskRelationships, showChildTasks: showChildTasks, showComments: showComments, categories: activeCategories, types: activeTypes, priorities: priorities, approaches: approaches, availableSpecialists: availableSpecialists, copiedId: copiedId, onTitleChange: setTitle, onDescriptionChange: setDescription, onCategoryChange: setCategory, onTypeChange: setType, onPriorityChange: setPriority, onComplexityChange: setComplexity, onApproachChange: (val) => { setApproach(val); setFormTaxonomies(prev => ({ ...prev, approach: val })); }, onAssigneeChange: setAssignee, onScheduledDateChange: setScheduledDate, onDueDateChange: setDueDate, onSelectedSpecialistsChange: setSelectedSpecialists, onNewCommentTextChange: setNewCommentText, onDescriptionFocusedChange: setDescriptionFocused, onShowMarkdownHelpChange: setShowMarkdownHelp, onShowSpecialistsChange: setShowSpecialists, onShowChecklistChange: setShowChecklist, onShowTaskRelationshipsChange: setShowTaskRelationships, onShowChildTasksChange: setShowChildTasks, onShowCommentsChange: setShowComments, onOpenSettings: (section) => {
1979
+ setSettingsSection(section);
1980
+ setActiveTab('settings');
1981
+ }, onSubmit: handleSubmit, commentsEndRef: commentsEndRef, onAddComment: () => handleAddComment(newCommentText), onSetParentForCurrentTask: handleSetParentForCurrentTask, onAddChildToCurrentTask: handleAddChildToCurrentTask, onCreateChildForCurrentTask: handleCreateChildForCurrentTask, onUnlinkChildTask: handleUnlinkChildTask, onReorderChildTasks: handleReorderChildTasks, onOpenTaskById: handleOpenTaskById, onAddContextFile: (file) => {
1982
+ setScreenshots(prev => [...prev, file]);
1983
+ }, onBrowseContextLinkPath: () => {
1984
+ setBrowserTarget({ type: 'context-link' });
1985
+ fetchFolders('');
1986
+ setShowFolderBrowser(true);
1987
+ }, onRemoveContextFile: async (index) => {
1988
+ const screenshot = screenshots[index];
1989
+ const path = typeof screenshot === 'string' ? screenshot : screenshot.path;
1990
+ setScreenshots(prev => prev.filter((_, i) => i !== index));
1991
+ if (typeof path === 'string' && path.startsWith('/api/taskforce/context/')) {
1992
+ try {
1993
+ await fetch(path, { method: 'DELETE' });
1994
+ }
1995
+ catch { }
1996
+ }
1997
+ }, onUpdateContextCaption: (index, caption) => {
1998
+ setScreenshots(prev => prev.map((s, i) => {
1999
+ if (i !== index)
2000
+ return s;
2001
+ if (typeof s === 'string')
2002
+ return { path: s, caption, timestamp: new Date().toISOString() };
2003
+ return { ...s, caption };
2004
+ }));
2005
+ }, onCopyId: handleCopyId, onToggleInProgress: handleToggleInProgress, onToggleReview: handleToggleReview, onToggleComplete: handleToggleComplete, onToggleCancel: handleToggleCancel, onArchiveTask: handleArchiveTask, onUnarchive: handleUnarchive, currentTask: currentTask, currentTaskParent: currentTaskParent, currentTaskChildren: currentTaskChildren })] }), _jsx(Modal, { isOpen: activeTab === 'settings', onClose: handleCloseModal, title: "Settings", size: "md", theme: currentTheme, draggable: true, isSettings: true, closeOnOverlayClick: false, children: _jsx(TaskSettings, { settingsModel: settingsModel, onSectionChange: setSettingsSection }) }), _jsx(Modal, { isOpen: !!scheduleWarningPrompt, onClose: cancelScheduleWarning, title: "Date Warning", size: "sm", theme: currentTheme, draggable: false, closeOnOverlayClick: false, children: _jsxs("div", { className: styles.form, style: { padding: '16px', gap: '12px' }, children: [_jsx("p", { style: { margin: 0, fontSize: '13px', color: 'var(--text-secondary)' }, children: "Due date is before scheduled date." }), scheduleWarningPrompt && (_jsxs("p", { style: { margin: 0, fontSize: '12px', color: 'var(--text-muted)' }, children: ["Due: ", scheduleWarningPrompt.dueDate, " \u00B7 Scheduled: ", scheduleWarningPrompt.scheduledDate] })), _jsxs("div", { className: styles.formActions, children: [_jsx("button", { className: styles.cancelBtn, onClick: cancelScheduleWarning, children: "Go Back" }), _jsx("button", { className: styles.submitBtn, onClick: confirmScheduleWarning, children: "Save Anyway" })] })] }) }), _jsx(Modal, { isOpen: !!scheduleParentDropPrompt, onClose: cancelScheduleParentDrop, title: "Schedule Descendants", size: "sm", theme: currentTheme, draggable: false, closeOnOverlayClick: false, children: _jsxs("div", { className: styles.form, style: { padding: '16px', gap: '12px' }, children: [scheduleParentDropPrompt && (_jsxs(_Fragment, { children: [_jsxs("p", { style: { margin: 0, fontSize: '13px', color: 'var(--text-secondary)' }, children: ["Schedule unscheduled leaf tasks under ", _jsx("strong", { children: scheduleParentDropPrompt.parentTitle }), " to ", _jsx("strong", { children: scheduleParentDropPrompt.targetDate }), "?"] }), _jsxs("p", { style: { margin: 0, fontSize: '12px', color: 'var(--text-muted)' }, children: [scheduleParentDropPrompt.toScheduleLeafTaskIds.length, " leaf task", scheduleParentDropPrompt.toScheduleLeafTaskIds.length === 1 ? '' : 's', " will be scheduled.", scheduleParentDropPrompt.alreadyScheduledLeafCount > 0 ? ` ${scheduleParentDropPrompt.alreadyScheduledLeafCount} already scheduled leaf task${scheduleParentDropPrompt.alreadyScheduledLeafCount === 1 ? '' : 's'} will be skipped.` : ''] })] })), _jsxs("div", { className: styles.formActions, children: [_jsx("button", { className: styles.cancelBtn, onClick: cancelScheduleParentDrop, disabled: scheduleParentDropBusy, children: "Cancel" }), _jsx("button", { className: styles.submitBtn, onClick: confirmScheduleParentDrop, disabled: scheduleParentDropBusy, children: scheduleParentDropBusy ? 'Scheduling...' : 'Schedule' })] })] }) }), _jsx(Modal, { isOpen: showHelpModal, onClose: () => setShowHelpModal(false), title: "Quick Start", size: "md", theme: currentTheme, draggable: true, children: _jsxs("div", { className: modalStyles.form, style: { paddingTop: '18px' }, children: [_jsx("p", { style: { margin: 0, color: 'var(--text-secondary)' }, children: "Use this quick guide to get productive in Taskforce dashboard mode." }), _jsxs("div", { children: [_jsx("strong", { children: "1. Capture work" }), _jsxs("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: ["Click ", _jsx("code", { children: "Add Task" }), ", write a short title, and save."] })] }), _jsxs("div", { children: [_jsx("strong", { children: "2. Plan the board" }), _jsxs("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: ["Use ", _jsx("code", { children: "Sort" }), ", ", _jsx("code", { children: "Group" }), ", and ", _jsx("code", { children: "Filters" }), " in the header."] })] }), _jsxs("div", { children: [_jsx("strong", { children: "3. Execute and stage" }), _jsxs("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: ["Move tasks through ", _jsx("code", { children: "Task" }), ", ", _jsx("code", { children: "In Progress" }), ", ", _jsx("code", { children: "Review" }), ", and ", _jsx("code", { children: "Done" }), "."] })] }), _jsxs("div", { children: [_jsx("strong", { children: "4. Archive finished work" }), _jsx("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: "Archive completed/cancelled tasks to keep active board signal high." })] }), _jsxs("div", { className: modalStyles.formActions, style: { display: 'flex', justifyContent: 'flex-end', gap: '10px' }, children: [_jsx("button", { className: styles.cancelBtn, onClick: () => {
2006
+ setShowHelpModal(false);
2007
+ setActiveTab('settings');
2008
+ setSettingsSection('general');
2009
+ }, children: "Open Settings" }), _jsx("button", { className: styles.submitBtn, onClick: () => setShowHelpModal(false), children: "Close Tutorial" })] })] }) }), _jsx(Modal, { isOpen: showAccountHub, onClose: () => setShowAccountHub(false), title: "Account Hub", size: "md", theme: currentTheme, draggable: true, children: _jsxs("div", { className: modalStyles.form, style: { paddingTop: '18px' }, children: [_jsx("p", { style: { margin: 0, color: 'var(--text-secondary)' }, children: "Manage sign in, profile, and workspace controls from one place." }), _jsxs("div", { className: styles.accountHubCard, children: [_jsx("strong", { children: "Authentication" }), _jsx("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: runtimeMode === 'cloud'
2010
+ ? (authRequiredForApi
2011
+ ? (isAuthenticated ? 'Signed in. API access is enabled.' : 'Cloud mode requires sign in before app usage.')
2012
+ : 'Cloud runtime with optional authentication.')
2013
+ : 'Local runtime allows guest usage without sign in.' }), _jsx("p", { style: { margin: '6px 0 0 0', color: 'var(--text-secondary)' }, children: isAuthenticated
2014
+ ? (hasAuthIdentity
2015
+ ? _jsxs(_Fragment, { children: ["Signed in as ", _jsx("span", { className: styles.accountIdentityEmail, children: authIdentityLabel })] })
2016
+ : 'Signed in')
2017
+ : 'Signed in as: not signed in' }), runtimeMode === 'cloud' && authRequiredForApi && !isAuthenticated && (_jsx("p", { style: { marginTop: '10px', color: 'var(--text-secondary)' }, children: "Use the `/login` screen to sign in." }))] }), _jsxs("div", { className: styles.accountHubCard, children: [_jsx("strong", { children: "Profile & Account" }), _jsx("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: "Profile editing and account preferences will live here." })] }), _jsxs("div", { className: styles.accountHubCard, children: [_jsx("strong", { children: "Billing" }), runtimeMode !== 'cloud' ? (_jsx("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: "Billing actions are available in cloud runtime only." })) : !isAuthenticated ? (_jsx("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: "Sign in to manage subscription billing." })) : (_jsxs(_Fragment, { children: [billingLoading && (_jsx("p", { style: { margin: '6px 0 0 0', color: 'var(--text-secondary)' }, children: "Loading billing status..." })), billingError && (_jsx("p", { className: styles.loginError, style: { marginTop: '8px' }, children: billingError })), billingActionError && (_jsx("p", { className: styles.loginError, style: { marginTop: '8px' }, children: billingActionError })), billingNotice && (_jsx("p", { style: { margin: '8px 0 0 0', color: 'var(--text-secondary)' }, children: billingNotice })), _jsxs("p", { style: { margin: '6px 0 0 0', color: 'var(--text-secondary)' }, children: ["Plan: ", _jsx("code", { children: String(billingStatus?.planId || 'free') }), ' · ', "Entitlement: ", _jsx("code", { children: String(billingStatus?.entitlementState || 'free') })] }), _jsxs("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: ["Stripe status: ", _jsx("code", { children: String(billingStatus?.stripeStatus || 'n/a') }), billingStatus?.effectiveUntil ? ` · Effective until ${new Date(billingStatus.effectiveUntil).toLocaleString()}` : ''] }), _jsx("div", { style: { marginTop: '10px', display: 'flex', gap: '8px', flexWrap: 'wrap' }, children: _jsxs("label", { className: styles.accountMenuMeta, style: { display: 'inline-flex', alignItems: 'center', gap: '8px' }, children: ["Interval", _jsxs("select", { className: styles.input, value: billingIntervalChoice, onChange: (event) => setBillingIntervalChoice(event.target.value === 'year' ? 'year' : 'month'), disabled: billingActionBusy, children: [_jsx("option", { value: "month", children: "Monthly" }), _jsx("option", { value: "year", children: "Yearly" })] })] }) }), _jsxs("div", { style: { marginTop: '10px', display: 'flex', gap: '10px', justifyContent: 'flex-end', flexWrap: 'wrap' }, children: [_jsx("button", { className: styles.cancelBtn, onClick: () => { void loadBillingStatus(); }, disabled: billingActionBusy || billingLoading, children: "Refresh Billing" }), _jsx("button", { className: styles.cancelBtn, onClick: () => { void handleSwitchSubscriptionInterval(); }, disabled: billingActionBusy || !billingStatus?.stripeSubscriptionId, children: "Update Interval" }), _jsx("button", { className: styles.cancelBtn, onClick: () => { void handleOpenBillingPortal(); }, disabled: billingActionBusy || !billingStatus?.stripeCustomerId, children: "Manage Billing" }), _jsx("button", { className: styles.submitBtn, onClick: () => { void handleStartCheckout(); }, disabled: billingActionBusy, children: billingActionBusy ? 'Working...' : 'Start Checkout' })] })] }))] }), _jsxs("div", { className: styles.accountHubCard, children: [_jsx("strong", { children: "Workspace Audit Log" }), _jsx("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: "Audit entries are workspace-scoped for the active workspace, not global account history." }), _jsxs("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: ["Active workspace: ", _jsx("code", { children: currentWorkspaceId })] }), _jsx("div", { style: { marginTop: '10px', display: 'flex', gap: '10px', justifyContent: 'flex-end' }, children: _jsx("button", { className: styles.cancelBtn, onClick: () => {
2018
+ setShowAccountHub(false);
2019
+ void openTeamManagementModal();
2020
+ setTeamManagementTab('audit');
2021
+ }, disabled: !canOpenTeamManagement, children: "Open Workspace Audit" }) })] }), _jsxs("div", { className: styles.accountHubCard, children: [_jsx("strong", { children: "Workspace Cloud Sync" }), _jsx("p", { style: { margin: '4px 0 0 0', color: 'var(--text-secondary)' }, children: canManageWorkspaceSync
2022
+ ? (workspaceCloudSyncEnabled
2023
+ ? `Enabled${workspaceSyncSourceOfTruth ? ` · Source: ${workspaceSyncSourceOfTruth}` : ''}`
2024
+ : 'Disabled')
2025
+ : 'Sign in to cloud account to enable workspace sync.' }), workspaceSyncError && (_jsx("p", { className: styles.loginError, style: { marginTop: '8px' }, children: workspaceSyncError })), _jsx("div", { style: { marginTop: '10px', display: 'flex', gap: '10px', justifyContent: 'flex-end' }, children: _jsx("button", { className: styles.cancelBtn, disabled: !canManageWorkspaceSync || workspaceSyncBusy, onClick: async () => {
2026
+ if (!canManageWorkspaceSync)
2027
+ return;
2028
+ setWorkspaceSyncError(null);
2029
+ if (!workspaceCloudSyncEnabled) {
2030
+ setWorkspaceSyncBusy(true);
2031
+ const result = await saveWorkspaceCloudSyncSettings({
2032
+ enabled: true,
2033
+ // I6: start false; the orchestrator marks it true after first successful pull.
2034
+ onboardingCompleted: false
2035
+ });
2036
+ if (!result.success)
2037
+ setWorkspaceSyncError(result.error || 'Failed to enable sync.');
2038
+ setWorkspaceSyncBusy(false);
2039
+ return;
2040
+ }
2041
+ setWorkspaceSyncBusy(true);
2042
+ const result = await saveWorkspaceCloudSyncSettings({
2043
+ enabled: false,
2044
+ sourceOfTruth: workspaceSyncSourceOfTruth,
2045
+ onboardingCompleted: workspaceSyncOnboardingCompleted
2046
+ });
2047
+ if (!result.success)
2048
+ setWorkspaceSyncError(result.error || 'Failed to disable sync.');
2049
+ setWorkspaceSyncBusy(false);
2050
+ }, children: workspaceCloudSyncEnabled ? 'Disable Sync' : 'Enable Sync' }) })] }), _jsxs("div", { className: modalStyles.formActions, style: { display: 'flex', justifyContent: 'flex-end', gap: '10px' }, children: [_jsx("button", { className: styles.cancelBtn, onClick: () => {
2051
+ setShowAccountHub(false);
2052
+ setShowHelpModal(true);
2053
+ }, children: "Help & Tutorial" }), _jsx("button", { className: styles.submitBtn, onClick: () => setShowAccountHub(false), children: "Close" })] })] }) }), _jsx(Modal, { isOpen: showTeamManagementModal, onClose: () => setShowTeamManagementModal(false), title: "Team Management", size: "md", theme: currentTheme, draggable: true, children: _jsxs("div", { className: modalStyles.form, style: { paddingTop: '18px' }, children: [teamPlanMode !== 'team' && (_jsx("p", { className: styles.loginError, children: "Team management is only available in TEAM accounts." })), teamMgmtError && (_jsx("p", { className: styles.loginError, children: teamMgmtError })), _jsxs("div", { className: styles.settingsTabs, children: [_jsx("button", { className: `${styles.settingsTabBtn} ${teamManagementTab === 'members' ? styles.settingsTabBtnActive : ''}`, onClick: () => {
2054
+ setTeamManagementTab('members');
2055
+ void loadTeamUsers();
2056
+ }, children: "Members" }), _jsx("button", { className: `${styles.settingsTabBtn} ${teamManagementTab === 'invites' ? styles.settingsTabBtnActive : ''}`, onClick: () => setTeamManagementTab('invites'), children: "Invites" }), _jsx("button", { className: `${styles.settingsTabBtn} ${teamManagementTab === 'audit' ? styles.settingsTabBtnActive : ''}`, onClick: () => {
2057
+ setTeamManagementTab('audit');
2058
+ void loadTeamAuditPage(teamAuditPage);
2059
+ }, children: "Audit Log" })] }), teamManagementTab === 'members' && (_jsxs("div", { className: styles.accountHubCard, children: [teamUsersLoading && _jsx("p", { style: { margin: 0, color: 'var(--text-secondary)' }, children: "Loading members..." }), !teamUsersLoading && teamUsers.length === 0 && (_jsx("p", { style: { margin: 0, color: 'var(--text-secondary)' }, children: "No users found." })), !teamUsersLoading && teamUsers.map((member) => (_jsxs("div", { style: { borderTop: '1px solid var(--border-color)', paddingTop: '8px', marginTop: '8px' }, children: [_jsx("div", { style: { fontWeight: 600 }, children: member.displayName || member.email }), _jsxs("div", { className: styles.accountMenuMeta, children: [member.email, " \u00B7 ", member.status, member.disabled ? ' · deactivated' : ''] }), _jsxs("div", { style: { marginTop: '6px', display: 'flex', gap: '8px', flexWrap: 'wrap' }, children: [_jsxs("select", { className: styles.input, value: member.role, onChange: (event) => {
2060
+ void executeTeamUserAction(member.userId, async () => fetch(`/api/taskforce/admin/users/${encodeURIComponent(member.userId)}/role`, {
2061
+ method: 'PATCH',
2062
+ headers: { 'Content-Type': 'application/json', ...teamAdminHeaders() },
2063
+ credentials: 'include',
2064
+ body: JSON.stringify({
2065
+ workspaceId: currentWorkspaceId,
2066
+ role: event.target.value
2067
+ })
2068
+ }));
2069
+ }, disabled: teamActionBusyUserId === member.userId, style: { maxWidth: '160px' }, children: [_jsx("option", { value: "owner", children: "Owner" }), _jsx("option", { value: "admin", children: "Admin" }), _jsx("option", { value: "member", children: "Member" })] }), member.role === 'member' && (_jsxs("select", { className: styles.input, value: member.permissionMode, onChange: (event) => {
2070
+ void executeTeamUserAction(member.userId, async () => fetch(`/api/taskforce/admin/users/${encodeURIComponent(member.userId)}/permission-mode`, {
2071
+ method: 'PATCH',
2072
+ headers: { 'Content-Type': 'application/json', ...teamAdminHeaders() },
2073
+ credentials: 'include',
2074
+ body: JSON.stringify({
2075
+ workspaceId: currentWorkspaceId,
2076
+ mode: event.target.value
2077
+ })
2078
+ }));
2079
+ }, disabled: teamActionBusyUserId === member.userId, style: { maxWidth: '170px' }, children: [_jsx("option", { value: "read-write", children: "Read / Write" }), _jsx("option", { value: "read-only", children: "Read Only" })] })), _jsx("button", { className: styles.cancelBtn, disabled: teamActionBusyUserId === member.userId, onClick: () => {
2080
+ void executeTeamUserAction(member.userId, async () => fetch(`/api/taskforce/admin/users/${encodeURIComponent(member.userId)}/disable`, {
2081
+ method: 'PATCH',
2082
+ headers: { 'Content-Type': 'application/json', ...teamAdminHeaders() },
2083
+ credentials: 'include',
2084
+ body: JSON.stringify({
2085
+ workspaceId: currentWorkspaceId,
2086
+ disabled: !member.disabled
2087
+ })
2088
+ }));
2089
+ }, children: member.disabled ? 'Reactivate' : 'Deactivate' }), member.status === 'invited' && (_jsx("button", { className: styles.cancelBtn, disabled: teamActionBusyUserId === member.userId, onClick: () => {
2090
+ void executeTeamUserAction(member.userId, async () => fetch(`/api/taskforce/admin/users/${encodeURIComponent(member.userId)}/invite/revoke`, {
2091
+ method: 'POST',
2092
+ headers: { 'Content-Type': 'application/json', ...teamAdminHeaders() },
2093
+ credentials: 'include',
2094
+ body: JSON.stringify({ workspaceId: currentWorkspaceId })
2095
+ }));
2096
+ }, children: "Revoke Invite" })), _jsx("button", { className: styles.cancelBtn, disabled: teamActionBusyUserId === member.userId, onClick: () => {
2097
+ void executeTeamUserAction(member.userId, async () => fetch(`/api/taskforce/admin/users/${encodeURIComponent(member.userId)}?workspaceId=${encodeURIComponent(currentWorkspaceId)}`, {
2098
+ method: 'DELETE',
2099
+ headers: teamAdminHeaders(),
2100
+ credentials: 'include'
2101
+ }));
2102
+ }, children: "Remove" })] })] }, member.userId)))] })), teamManagementTab === 'invites' && (_jsxs("div", { className: styles.accountHubCard, children: [teamInviteFeedback && (_jsx("p", { style: { margin: '0 0 8px 0', color: 'var(--text-secondary)' }, children: teamInviteFeedback })), _jsxs("div", { className: styles.pathInputGroup, children: [_jsx("label", { className: styles.label, style: { minWidth: '90px' }, children: "Email" }), _jsx("input", { className: styles.input, value: teamInviteEmail, onChange: (event) => setTeamInviteEmail(event.target.value), placeholder: "name@company.com" })] }), _jsxs("div", { className: styles.pathInputGroup, children: [_jsx("label", { className: styles.label, style: { minWidth: '90px' }, children: "Role" }), _jsxs("select", { className: styles.input, value: teamInviteRole, onChange: (event) => setTeamInviteRole(event.target.value === 'admin' ? 'admin' : 'member'), style: { maxWidth: '150px' }, children: [_jsx("option", { value: "member", children: "Member" }), _jsx("option", { value: "admin", children: "Admin" })] }), teamInviteRole === 'member' && (_jsxs("select", { className: styles.input, value: teamInvitePermissionMode, onChange: (event) => setTeamInvitePermissionMode(event.target.value === 'read-only' ? 'read-only' : 'read-write'), style: { maxWidth: '170px' }, children: [_jsx("option", { value: "read-write", children: "Read / Write" }), _jsx("option", { value: "read-only", children: "Read Only" })] })), _jsx("button", { className: styles.submitBtn, disabled: teamInviteBusy || !teamInviteEmail.trim() || teamPlanMode !== 'team', onClick: () => { void submitTeamInvite(); }, children: teamInviteBusy ? 'Sending...' : 'Send Invite' })] }), _jsxs("div", { style: { marginTop: '12px' }, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: '6px' }, children: "Pending Invites" }), pendingInvites.length === 0 && (_jsx("p", { style: { margin: 0, color: 'var(--text-secondary)' }, children: "No pending invites." })), pendingInvites.map((invite) => (_jsxs("div", { style: { borderTop: '1px solid var(--border-color)', paddingTop: '8px', marginTop: '8px' }, children: [_jsx("div", { style: { fontWeight: 600 }, children: invite.displayName || invite.email }), _jsxs("div", { className: styles.accountMenuMeta, children: [invite.email, " \u00B7 ", invite.role] })] }, invite.userId)))] })] })), teamManagementTab === 'audit' && (_jsxs("div", { className: styles.accountHubCard, children: [teamAuditLoading && _jsx("p", { style: { margin: 0, color: 'var(--text-secondary)' }, children: "Loading audit entries..." }), !teamAuditLoading && teamAuditEvents.length === 0 && (_jsx("p", { style: { margin: 0, color: 'var(--text-secondary)' }, children: "No audit events for this workspace." })), !teamAuditLoading && (_jsx("div", { style: { maxHeight: '320px', overflowY: 'auto', paddingRight: '4px' }, children: teamAuditEvents.map((event) => (_jsxs("div", { style: { borderTop: '1px solid var(--border-color)', paddingTop: '8px', marginTop: '8px' }, children: [_jsx("div", { className: styles.accountMenuMeta, children: new Date(event.createdAt).toLocaleString() }), _jsx("div", { style: { fontWeight: 600 }, children: event.action }), _jsxs("div", { className: styles.accountMenuMeta, children: [event.actorUserId, " (", event.actorRole, ")"] })] }, event.id))) })), _jsxs("div", { style: { marginTop: '10px', display: 'flex', justifyContent: 'space-between', gap: '8px' }, children: [_jsx("button", { className: styles.cancelBtn, disabled: teamAuditLoading || teamAuditPage === 0, onClick: () => { void loadTeamAuditPage(teamAuditPage - 1); }, children: "Previous" }), _jsxs("span", { className: styles.accountMenuMeta, children: ["Page ", teamAuditPage + 1, " of ", teamAuditPages] }), _jsx("button", { className: styles.cancelBtn, disabled: teamAuditLoading || !teamAuditHasMore || teamAuditPage + 1 >= teamAuditPages, onClick: () => { void loadTeamAuditPage(teamAuditPage + 1); }, children: "Next" })] })] })), _jsx("div", { className: modalStyles.formActions, style: { display: 'flex', justifyContent: 'flex-end', gap: '10px' }, children: _jsx("button", { className: styles.cancelBtn, onClick: () => setShowTeamManagementModal(false), children: "Close" }) })] }) }), _jsx(Modal, { isOpen: showSyncStatusModal, onClose: () => {
2103
+ setShowSyncStatusModal(false);
2104
+ }, title: "Cloud Sync Status", size: "md", theme: currentTheme, draggable: true, children: _jsxs("div", { className: `${modalStyles.form} ${styles.syncStatusModal}`, children: [_jsxs("div", { className: styles.syncStatusTop, children: [_jsxs("div", { children: [_jsx("div", { className: styles.syncStatusHeading, children: "Current Status" }), _jsx("div", { className: styles.syncStatusSubtext, children: _jsx("code", { children: currentWorkspaceId }) }), workspaceBootstrapSyncInProgress && (_jsxs("div", { className: styles.syncStatusPausedHint, children: ["Initial sync in progress: ", workspaceBootstrapSyncAppliedChanges, " changes across ", workspaceBootstrapSyncPages, " page", workspaceBootstrapSyncPages === 1 ? '' : 's', "."] })), runtimeMode === 'local' && !isAuthenticated && (_jsx("div", { className: styles.syncStatusPausedHint, children: "Sync is paused. Sign in on cloud to resume." }))] }), _jsx("div", { className: styles.syncStatusBadge, style: {
2105
+ borderColor: modalSyncStatusMeta.border,
2106
+ background: modalSyncStatusMeta.background,
2107
+ color: modalSyncStatusMeta.color
2108
+ }, children: modalSyncStatusMeta.label })] }), _jsxs("div", { className: styles.syncStatusGrid, children: [_jsxs("div", { className: styles.syncStatusItem, children: [_jsx("span", { className: styles.syncStatusLabel, children: "Last successful sync" }), _jsx("span", { className: styles.syncStatusValue, children: formattedLastSyncTime })] }), _jsxs("div", { className: styles.syncStatusItem, children: [_jsx("span", { className: styles.syncStatusLabel, children: "Pending local changes" }), _jsx("span", { className: styles.syncStatusValue, children: userGlobalSyncPendingChanges })] }), _jsxs("div", { className: styles.syncStatusItem, children: [_jsx("span", { className: styles.syncStatusLabel, children: "Last pull from cloud" }), _jsx("span", { className: styles.syncStatusValue, children: formattedLastPullTime })] }), _jsxs("div", { className: styles.syncStatusItem, children: [_jsx("span", { className: styles.syncStatusLabel, children: "Last push to cloud" }), _jsx("span", { className: styles.syncStatusValue, children: formattedLastPushTime })] }), _jsxs("div", { className: styles.syncStatusItem, children: [_jsx("span", { className: styles.syncStatusLabel, children: "Scheduled retry" }), _jsx("span", { className: styles.syncStatusValue, children: workspaceRetryAt ? formattedRetryTime : 'None' })] }), _jsxs("div", { className: styles.syncStatusItem, children: [_jsx("span", { className: styles.syncStatusLabel, children: "Last error time" }), _jsx("span", { className: styles.syncStatusValue, children: workspaceLastErrorAt ? formattedLastErrorTime : 'None' })] })] }), _jsxs("div", { className: styles.syncStatusDetailsGrid, children: [_jsx("div", { className: styles.syncStatusTableWrap, children: _jsx("table", { className: styles.syncStatusTable, children: _jsxs("tbody", { children: [_jsxs("tr", { children: [_jsx("th", { children: "Runtime mode" }), _jsx("td", { children: runtimeMode })] }), _jsxs("tr", { children: [_jsx("th", { children: "Sync source of truth" }), _jsx("td", { children: workspaceSyncSourceOfTruth || 'unset' })] }), _jsxs("tr", { children: [_jsx("th", { children: "Ready" }), _jsx("td", { children: workspaceSyncReady ? 'yes' : 'no' })] }), _jsxs("tr", { children: [_jsx("th", { children: "Hydrated" }), _jsx("td", { children: workspaceCloudHydrated ? 'yes' : 'no' })] }), _jsxs("tr", { children: [_jsx("th", { children: "Failure count" }), _jsx("td", { children: workspaceSyncFailureCount })] })] }) }) }), _jsx("div", { className: styles.syncStatusTableWrap, children: _jsx("table", { className: styles.syncStatusTable, children: _jsxs("tbody", { children: [_jsxs("tr", { children: [_jsx("th", { children: "Auth failures" }), _jsx("td", { children: workspaceSyncAuthFailureCount })] }), _jsxs("tr", { children: [_jsx("th", { children: "Last failure source" }), _jsx("td", { children: workspaceSyncLastFailureSource || 'none' })] }), _jsxs("tr", { children: [_jsx("th", { children: "Last failure status" }), _jsx("td", { children: workspaceSyncLastFailureStatusCode ?? 'none' })] }), _jsxs("tr", { children: [_jsx("th", { children: "Retries scheduled" }), _jsx("td", { children: workspaceSyncRetryScheduledCount })] })] }) }) })] }), _jsx("div", { className: styles.syncStatusTableWrap, children: _jsx("table", { className: styles.syncStatusTable, children: _jsxs("tbody", { children: [_jsxs("tr", { children: [_jsx("th", { className: styles.syncStatusTableNarrowLabel, children: "Last error" }), _jsx("td", { className: styles.syncStatusTableError, style: { color: (workspaceLastErrorMessage || userGlobalSyncError) ? '#fda4af' : 'var(--text-secondary)' }, children: workspaceLastErrorMessage || userGlobalSyncError || 'None' })] }), _jsxs("tr", { children: [_jsx("th", { className: styles.syncStatusTableNarrowLabel, children: "Last warning" }), _jsx("td", { style: { color: workspaceLastWarningMessage ? '#fcd34d' : 'var(--text-secondary)' }, children: workspaceLastWarningMessage || 'None' })] })] }) }) }), _jsxs("div", { className: `${modalStyles.formActions} ${styles.syncStatusActions}`, children: [_jsx("button", { className: styles.cancelBtn, onClick: () => void resetWorkspaceSyncCursorAndPull(), disabled: userGlobalSyncStatus === 'syncing', title: "Clear pull cursor and fetch full cloud state into local workspace", children: "Reset Cursor + Full Pull" }), runtimeMode === 'local' && !isAuthenticated && (_jsx("button", { className: styles.submitBtn, onClick: () => {
2109
+ openSharedLoginRoute('login');
2110
+ }, children: "Sign In / Register" })), _jsx("button", { className: styles.cancelBtn, onClick: () => {
2111
+ setShowSyncStatusModal(false);
2112
+ }, children: "Close" }), _jsx("button", { className: styles.submitBtn, onClick: () => void retryWorkspaceCloudSync(), disabled: userGlobalSyncStatus === 'syncing', children: userGlobalSyncStatus === 'syncing' ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { size: 14, className: styles.spinner }), "Syncing..."] })) : (_jsxs(_Fragment, { children: [_jsx(RefreshCw, { size: 14 }), "Retry Now"] })) })] })] }) }), unsavedModalOpen && createPortal(_jsx("div", { className: `${modalStyles.overlay} ${modalStyles.unsavedOverlay}`, style: { zIndex: 2000 }, children: _jsxs("div", { className: `${modalStyles.modal} ${modalStyles.unsavedModal}`, "data-theme": currentTheme, children: [_jsx("div", { className: `${modalStyles.header} ${modalStyles.unsavedHeader}`, children: _jsxs("div", { className: `${modalStyles.headerTitle} ${modalStyles.unsavedTitle}`, children: [_jsx(AlertTriangle, { size: 20 }), "Unsaved Changes"] }) }), _jsx("div", { className: `${modalStyles.form} ${modalStyles.unsavedContent}`, children: _jsx("p", { className: modalStyles.unsavedText, children: "You have unsaved changes. Would you like to save them?" }) }), _jsxs("div", { className: `${modalStyles.formActions} ${modalStyles.unsavedActions}`, children: [_jsx("button", { className: styles.cancelBtn, onClick: () => setUnsavedModalOpen(false), title: "Close dialog and continue editing", children: "Keep Editing" }), _jsx("button", { className: styles.destructiveBtn, onClick: handleDiscard, title: "Discard unsaved changes and leave", children: "Discard" }), _jsxs("button", { className: styles.submitBtn, onClick: handleSaveAndNavigate, disabled: loading, title: "Save changes and leave", children: [loading ? _jsx(Loader2, { size: 16, className: styles.spinner }) : _jsx(Save, { size: 16 }), "Save"] })] })] }) }), document.body), showFolderBrowser && createPortal(_jsx("div", { className: styles.folderBrowserOverlay, children: _jsxs("div", { className: styles.folderBrowser, children: [_jsxs("div", { className: styles.browserHeader, children: [_jsxs("div", { className: styles.browserPathInfo, children: [_jsx(Folder, { size: 14 }), _jsx("span", { children: currentBrowsePath || 'Project Root' })] }), _jsxs("div", { className: styles.browserActions, children: [currentBrowsePath && (_jsx("button", { className: styles.helpLink, onClick: () => {
2113
+ const parts = currentBrowsePath.split('/').filter(Boolean);
2114
+ parts.pop();
2115
+ fetchFolders(parts.length ? parts.join('/') + '/' : '');
2116
+ }, children: "Back" })), _jsx("button", { className: styles.helpLink, onClick: () => setShowFolderBrowser(false), children: "Close" })] })] }), _jsxs("div", { className: styles.browserList, children: [browserTarget && typeof browserTarget === 'object' && browserTarget.type !== 'context-link' && (_jsxs("div", { className: `${styles.browserItem} ${styles.browserItemFile} ${styles.browserSelectCurrent}`, onClick: () => handleSelectPath(currentBrowsePath), children: [_jsx(Check, { size: 14 }), " Select Current: ./", currentBrowsePath || '(root)'] })), folders.map(folder => (_jsxs("div", { className: styles.browserItem, onClick: () => fetchFolders(currentBrowsePath + folder + '/'), children: [_jsx(Folder, { size: 14 }), " ", folder, "/"] }, folder))), files.map(file => (_jsxs("div", { className: `${styles.browserItem} ${styles.browserItemFile}`, onClick: () => handleSelectPath(currentBrowsePath + file), children: [_jsx(FileText, { size: 14 }), " ", file] }, file))), folders.length === 0 && files.length === 0 && (_jsx("div", { className: `${styles.browserItem} ${styles.browserEmpty}`, children: "No items found" }))] })] }) }), document.body), _jsx(Modal, { isOpen: !!cancellationPromptTask, onClose: handleCancelCancellation, title: "Cancel Task", size: "sm", theme: currentTheme, draggable: true, footer: _jsxs(_Fragment, { children: [_jsx("button", { className: styles.cancelBtn, onClick: handleCancelCancellation, title: "Close and continue working on this task", children: "Back" }), _jsxs("button", { className: `${styles.warningBtn} ${styles.cancelTaskConfirmBtn}`, onClick: handleConfirmCancellation, title: "Keep the task, set status to cancelled, and save a cancellation reason", style: { gap: '8px' }, disabled: cancellationOpenDescendantIds.length > 0 && !cancellationMode, children: [_jsx(Ban, { size: 16 }), " Confirm Cancellation"] }), _jsxs("button", { className: `${styles.destructiveBtn} ${styles.cancelTaskDeleteBtn}`, onClick: handleDeleteFromCancellation, title: "Permanently delete this task and all of its data", style: { gap: '8px' }, children: [_jsx(Trash2, { size: 16 }), " Delete Permanently"] })] }), children: _jsxs("div", { className: styles.form, style: { padding: '20px 24px', display: 'flex', flexDirection: 'column', gap: '8px', overflow: 'hidden' }, children: [cancellationOpenDescendantIds.length > 0 && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: '8px' }, children: [_jsxs("p", { style: { margin: 0, fontSize: '14px', color: 'var(--text-secondary)' }, children: ["This parent has ", cancellationOpenDescendantIds.length, " open descendant task", cancellationOpenDescendantIds.length === 1 ? '' : 's', "."] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx("input", { type: "radio", name: "cancel-mode", checked: cancellationMode === 'cascade', onChange: () => setCancellationMode('cascade') }), "Cancel parent and cancel open descendants"] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px' }, children: [_jsx("input", { type: "radio", name: "cancel-mode", checked: cancellationMode === 'unlink', onChange: () => setCancellationMode('unlink') }), "Cancel parent and unlink open descendants"] })] })), _jsx("p", { style: { margin: 0, fontSize: '14px', color: 'var(--text-secondary)' }, children: "Why is this task being cancelled?" }), _jsx("textarea", { className: styles.textarea, placeholder: "Reason for record (e.g., No longer relevant, duplicate, etc.)", value: localCancellationReason, onChange: (e) => setLocalCancellationReason(e.target.value), autoFocus: true, style: { minHeight: '60px', width: '100%', resize: 'none', boxSizing: 'border-box' }, rows: 2 })] }) })] }));
2117
+ }