@amsterdamdatalabs/enact-factory 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +566 -0
- package/dist/adapters/agenticLoop.d.ts +90 -0
- package/dist/adapters/agenticLoop.d.ts.map +1 -0
- package/dist/adapters/agenticLoop.js +219 -0
- package/dist/adapters/agenticLoop.js.map +1 -0
- package/dist/adapters/base.d.ts +16 -0
- package/dist/adapters/base.d.ts.map +1 -0
- package/dist/adapters/base.js +135 -0
- package/dist/adapters/base.js.map +1 -0
- package/dist/adapters/claude.d.ts +13 -0
- package/dist/adapters/claude.d.ts.map +1 -0
- package/dist/adapters/claude.js +318 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/codex.d.ts +14 -0
- package/dist/adapters/codex.d.ts.map +1 -0
- package/dist/adapters/codex.js +366 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/cryptoQuantAdapter.d.ts +85 -0
- package/dist/adapters/cryptoQuantAdapter.d.ts.map +1 -0
- package/dist/adapters/cryptoQuantAdapter.js +238 -0
- package/dist/adapters/cryptoQuantAdapter.js.map +1 -0
- package/dist/adapters/cursor.d.ts +13 -0
- package/dist/adapters/cursor.d.ts.map +1 -0
- package/dist/adapters/cursor.js +300 -0
- package/dist/adapters/cursor.js.map +1 -0
- package/dist/adapters/envPath.d.ts +20 -0
- package/dist/adapters/envPath.d.ts.map +1 -0
- package/dist/adapters/envPath.js +49 -0
- package/dist/adapters/envPath.js.map +1 -0
- package/dist/adapters/hermes.d.ts +13 -0
- package/dist/adapters/hermes.d.ts.map +1 -0
- package/dist/adapters/hermes.js +283 -0
- package/dist/adapters/hermes.js.map +1 -0
- package/dist/adapters/index.d.ts +18 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +56 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/opencode.d.ts +13 -0
- package/dist/adapters/opencode.d.ts.map +1 -0
- package/dist/adapters/opencode.js +282 -0
- package/dist/adapters/opencode.js.map +1 -0
- package/dist/adapters/processRegistry.d.ts +38 -0
- package/dist/adapters/processRegistry.d.ts.map +1 -0
- package/dist/adapters/processRegistry.js +147 -0
- package/dist/adapters/processRegistry.js.map +1 -0
- package/dist/adapters/responses.d.ts +16 -0
- package/dist/adapters/responses.d.ts.map +1 -0
- package/dist/adapters/responses.js +244 -0
- package/dist/adapters/responses.js.map +1 -0
- package/dist/adapters/streamBuffer.d.ts +59 -0
- package/dist/adapters/streamBuffer.d.ts.map +1 -0
- package/dist/adapters/streamBuffer.js +123 -0
- package/dist/adapters/streamBuffer.js.map +1 -0
- package/dist/adapters/tools.d.ts +30 -0
- package/dist/adapters/tools.d.ts.map +1 -0
- package/dist/adapters/tools.js +219 -0
- package/dist/adapters/tools.js.map +1 -0
- package/dist/adapters/types.d.ts +82 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +6 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/agents/agentBus.d.ts +160 -0
- package/dist/agents/agentBus.d.ts.map +1 -0
- package/dist/agents/agentBus.js +350 -0
- package/dist/agents/agentBus.js.map +1 -0
- package/dist/agents/agentPair.d.ts +215 -0
- package/dist/agents/agentPair.d.ts.map +1 -0
- package/dist/agents/agentPair.js +456 -0
- package/dist/agents/agentPair.js.map +1 -0
- package/dist/agents/auditor.d.ts +27 -0
- package/dist/agents/auditor.d.ts.map +1 -0
- package/dist/agents/auditor.js +238 -0
- package/dist/agents/auditor.js.map +1 -0
- package/dist/agents/cliStreamParser.d.ts +18 -0
- package/dist/agents/cliStreamParser.d.ts.map +1 -0
- package/dist/agents/cliStreamParser.js +156 -0
- package/dist/agents/cliStreamParser.js.map +1 -0
- package/dist/agents/documenter.d.ts +31 -0
- package/dist/agents/documenter.d.ts.map +1 -0
- package/dist/agents/documenter.js +286 -0
- package/dist/agents/documenter.js.map +1 -0
- package/dist/agents/draftAnalyzer.d.ts +50 -0
- package/dist/agents/draftAnalyzer.d.ts.map +1 -0
- package/dist/agents/draftAnalyzer.js +289 -0
- package/dist/agents/draftAnalyzer.js.map +1 -0
- package/dist/agents/evaluator.d.ts +61 -0
- package/dist/agents/evaluator.d.ts.map +1 -0
- package/dist/agents/evaluator.js +338 -0
- package/dist/agents/evaluator.js.map +1 -0
- package/dist/agents/executor.d.ts +33 -0
- package/dist/agents/executor.d.ts.map +1 -0
- package/dist/agents/executor.js +130 -0
- package/dist/agents/executor.js.map +1 -0
- package/dist/agents/index.d.ts +10 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +10 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/pairMetrics.d.ts +63 -0
- package/dist/agents/pairMetrics.d.ts.map +1 -0
- package/dist/agents/pairMetrics.js +232 -0
- package/dist/agents/pairMetrics.js.map +1 -0
- package/dist/agents/pairPipeline.d.ts +184 -0
- package/dist/agents/pairPipeline.d.ts.map +1 -0
- package/dist/agents/pairPipeline.js +934 -0
- package/dist/agents/pairPipeline.js.map +1 -0
- package/dist/agents/pairWebhook.d.ts +59 -0
- package/dist/agents/pairWebhook.d.ts.map +1 -0
- package/dist/agents/pairWebhook.js +242 -0
- package/dist/agents/pairWebhook.js.map +1 -0
- package/dist/agents/pipelineFormat.d.ts +8 -0
- package/dist/agents/pipelineFormat.d.ts.map +1 -0
- package/dist/agents/pipelineFormat.js +65 -0
- package/dist/agents/pipelineFormat.js.map +1 -0
- package/dist/agents/pipelineGuards.d.ts +23 -0
- package/dist/agents/pipelineGuards.d.ts.map +1 -0
- package/dist/agents/pipelineGuards.js +257 -0
- package/dist/agents/pipelineGuards.js.map +1 -0
- package/dist/agents/reviewer.d.ts +37 -0
- package/dist/agents/reviewer.d.ts.map +1 -0
- package/dist/agents/reviewer.js +214 -0
- package/dist/agents/reviewer.js.map +1 -0
- package/dist/agents/skillDocumenter.d.ts +23 -0
- package/dist/agents/skillDocumenter.d.ts.map +1 -0
- package/dist/agents/skillDocumenter.js +219 -0
- package/dist/agents/skillDocumenter.js.map +1 -0
- package/dist/agents/tester.d.ts +37 -0
- package/dist/agents/tester.d.ts.map +1 -0
- package/dist/agents/tester.js +309 -0
- package/dist/agents/tester.js.map +1 -0
- package/dist/automation/autonomousRunner.d.ts +145 -0
- package/dist/automation/autonomousRunner.d.ts.map +1 -0
- package/dist/automation/autonomousRunner.js +1272 -0
- package/dist/automation/autonomousRunner.js.map +1 -0
- package/dist/automation/dailyReporter.d.ts +26 -0
- package/dist/automation/dailyReporter.d.ts.map +1 -0
- package/dist/automation/dailyReporter.js +130 -0
- package/dist/automation/dailyReporter.js.map +1 -0
- package/dist/automation/index.d.ts +5 -0
- package/dist/automation/index.d.ts.map +1 -0
- package/dist/automation/index.js +5 -0
- package/dist/automation/index.js.map +1 -0
- package/dist/automation/longRunningMonitor.d.ts +26 -0
- package/dist/automation/longRunningMonitor.d.ts.map +1 -0
- package/dist/automation/longRunningMonitor.js +356 -0
- package/dist/automation/longRunningMonitor.js.map +1 -0
- package/dist/automation/prOwnership.d.ts +18 -0
- package/dist/automation/prOwnership.d.ts.map +1 -0
- package/dist/automation/prOwnership.js +61 -0
- package/dist/automation/prOwnership.js.map +1 -0
- package/dist/automation/runnerExecution.d.ts +57 -0
- package/dist/automation/runnerExecution.d.ts.map +1 -0
- package/dist/automation/runnerExecution.js +701 -0
- package/dist/automation/runnerExecution.js.map +1 -0
- package/dist/automation/runnerState.d.ts +170 -0
- package/dist/automation/runnerState.d.ts.map +1 -0
- package/dist/automation/runnerState.js +496 -0
- package/dist/automation/runnerState.js.map +1 -0
- package/dist/automation/runnerTypes.d.ts +57 -0
- package/dist/automation/runnerTypes.d.ts.map +1 -0
- package/dist/automation/runnerTypes.js +5 -0
- package/dist/automation/runnerTypes.js.map +1 -0
- package/dist/automation/scheduler.d.ts +75 -0
- package/dist/automation/scheduler.d.ts.map +1 -0
- package/dist/automation/scheduler.js +402 -0
- package/dist/automation/scheduler.js.map +1 -0
- package/dist/azdo/azdo.d.ts +70 -0
- package/dist/azdo/azdo.d.ts.map +1 -0
- package/dist/azdo/azdo.js +328 -0
- package/dist/azdo/azdo.js.map +1 -0
- package/dist/azdo/index.d.ts +3 -0
- package/dist/azdo/index.d.ts.map +1 -0
- package/dist/azdo/index.js +3 -0
- package/dist/azdo/index.js.map +1 -0
- package/dist/azdo/projectUpdater.d.ts +13 -0
- package/dist/azdo/projectUpdater.d.ts.map +1 -0
- package/dist/azdo/projectUpdater.js +155 -0
- package/dist/azdo/projectUpdater.js.map +1 -0
- package/dist/azureDevOps/client.d.ts +75 -0
- package/dist/azureDevOps/client.d.ts.map +1 -0
- package/dist/azureDevOps/client.js +150 -0
- package/dist/azureDevOps/client.js.map +1 -0
- package/dist/azureDevOps/hierarchy.d.ts +119 -0
- package/dist/azureDevOps/hierarchy.d.ts.map +1 -0
- package/dist/azureDevOps/hierarchy.js +470 -0
- package/dist/azureDevOps/hierarchy.js.map +1 -0
- package/dist/azureDevOps/mapper.d.ts +101 -0
- package/dist/azureDevOps/mapper.d.ts.map +1 -0
- package/dist/azureDevOps/mapper.js +438 -0
- package/dist/azureDevOps/mapper.js.map +1 -0
- package/dist/azureDevOps/stateMapping.d.ts +15 -0
- package/dist/azureDevOps/stateMapping.d.ts.map +1 -0
- package/dist/azureDevOps/stateMapping.js +141 -0
- package/dist/azureDevOps/stateMapping.js.map +1 -0
- package/dist/cli/authHandler.d.ts +13 -0
- package/dist/cli/authHandler.d.ts.map +1 -0
- package/dist/cli/authHandler.js +70 -0
- package/dist/cli/authHandler.js.map +1 -0
- package/dist/cli/checkHandler.d.ts +27 -0
- package/dist/cli/checkHandler.d.ts.map +1 -0
- package/dist/cli/checkHandler.js +560 -0
- package/dist/cli/checkHandler.js.map +1 -0
- package/dist/cli/daemon.d.ts +30 -0
- package/dist/cli/daemon.d.ts.map +1 -0
- package/dist/cli/daemon.js +141 -0
- package/dist/cli/daemon.js.map +1 -0
- package/dist/cli/factoryCommands.d.ts +3 -0
- package/dist/cli/factoryCommands.d.ts.map +1 -0
- package/dist/cli/factoryCommands.js +165 -0
- package/dist/cli/factoryCommands.js.map +1 -0
- package/dist/cli/promptHandler.d.ts +13 -0
- package/dist/cli/promptHandler.d.ts.map +1 -0
- package/dist/cli/promptHandler.js +193 -0
- package/dist/cli/promptHandler.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +320 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/agentLifecycle.d.ts +322 -0
- package/dist/core/agentLifecycle.d.ts.map +1 -0
- package/dist/core/agentLifecycle.js +230 -0
- package/dist/core/agentLifecycle.js.map +1 -0
- package/dist/core/areaMapping.d.ts +9 -0
- package/dist/core/areaMapping.d.ts.map +1 -0
- package/dist/core/areaMapping.js +37 -0
- package/dist/core/areaMapping.js.map +1 -0
- package/dist/core/config.d.ts +469 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +780 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/dashboardContract.d.ts +204 -0
- package/dist/core/dashboardContract.d.ts.map +1 -0
- package/dist/core/dashboardContract.js +205 -0
- package/dist/core/dashboardContract.js.map +1 -0
- package/dist/core/devopsModel.d.ts +138 -0
- package/dist/core/devopsModel.d.ts.map +1 -0
- package/dist/core/devopsModel.js +137 -0
- package/dist/core/devopsModel.js.map +1 -0
- package/dist/core/envFile.d.ts +11 -0
- package/dist/core/envFile.d.ts.map +1 -0
- package/dist/core/envFile.js +104 -0
- package/dist/core/envFile.js.map +1 -0
- package/dist/core/eventHub.d.ts +220 -0
- package/dist/core/eventHub.d.ts.map +1 -0
- package/dist/core/eventHub.js +136 -0
- package/dist/core/eventHub.js.map +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +7 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/laneExecutionState.d.ts +29 -0
- package/dist/core/laneExecutionState.d.ts.map +1 -0
- package/dist/core/laneExecutionState.js +18 -0
- package/dist/core/laneExecutionState.js.map +1 -0
- package/dist/core/laneStatus.d.ts +49 -0
- package/dist/core/laneStatus.d.ts.map +1 -0
- package/dist/core/laneStatus.js +153 -0
- package/dist/core/laneStatus.js.map +1 -0
- package/dist/core/prSidecar.d.ts +96 -0
- package/dist/core/prSidecar.d.ts.map +1 -0
- package/dist/core/prSidecar.js +33 -0
- package/dist/core/prSidecar.js.map +1 -0
- package/dist/core/runtimeConfig.d.ts +6 -0
- package/dist/core/runtimeConfig.d.ts.map +1 -0
- package/dist/core/runtimeConfig.js +24 -0
- package/dist/core/runtimeConfig.js.map +1 -0
- package/dist/core/scmProvider.d.ts +19 -0
- package/dist/core/scmProvider.d.ts.map +1 -0
- package/dist/core/scmProvider.js +38 -0
- package/dist/core/scmProvider.js.map +1 -0
- package/dist/core/service.d.ts +10 -0
- package/dist/core/service.d.ts.map +1 -0
- package/dist/core/service.js +297 -0
- package/dist/core/service.js.map +1 -0
- package/dist/core/traceCollector.d.ts +105 -0
- package/dist/core/traceCollector.d.ts.map +1 -0
- package/dist/core/traceCollector.js +141 -0
- package/dist/core/traceCollector.js.map +1 -0
- package/dist/core/types.d.ts +432 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/workItemMapper.d.ts +39 -0
- package/dist/core/workItemMapper.d.ts.map +1 -0
- package/dist/core/workItemMapper.js +427 -0
- package/dist/core/workItemMapper.js.map +1 -0
- package/dist/core/workItemModel.d.ts +120 -0
- package/dist/core/workItemModel.d.ts.map +1 -0
- package/dist/core/workItemModel.js +104 -0
- package/dist/core/workItemModel.js.map +1 -0
- package/dist/core/workItemPayload.d.ts +195 -0
- package/dist/core/workItemPayload.d.ts.map +1 -0
- package/dist/core/workItemPayload.js +24 -0
- package/dist/core/workItemPayload.js.map +1 -0
- package/dist/core/workspaceConfig.d.ts +57 -0
- package/dist/core/workspaceConfig.d.ts.map +1 -0
- package/dist/core/workspaceConfig.js +184 -0
- package/dist/core/workspaceConfig.js.map +1 -0
- package/dist/doctor.d.ts +18 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +34 -0
- package/dist/doctor.js.map +1 -0
- package/dist/factory/activeSkill.d.ts +11 -0
- package/dist/factory/activeSkill.d.ts.map +1 -0
- package/dist/factory/activeSkill.js +44 -0
- package/dist/factory/activeSkill.js.map +1 -0
- package/dist/factory/assignment.d.ts +54 -0
- package/dist/factory/assignment.d.ts.map +1 -0
- package/dist/factory/assignment.js +94 -0
- package/dist/factory/assignment.js.map +1 -0
- package/dist/factory/auditLog.d.ts +10 -0
- package/dist/factory/auditLog.d.ts.map +1 -0
- package/dist/factory/auditLog.js +38 -0
- package/dist/factory/auditLog.js.map +1 -0
- package/dist/factory/closureRequirements.d.ts +12 -0
- package/dist/factory/closureRequirements.d.ts.map +1 -0
- package/dist/factory/closureRequirements.js +30 -0
- package/dist/factory/closureRequirements.js.map +1 -0
- package/dist/factory/delegationPrompt.d.ts +3 -0
- package/dist/factory/delegationPrompt.d.ts.map +1 -0
- package/dist/factory/delegationPrompt.js +16 -0
- package/dist/factory/delegationPrompt.js.map +1 -0
- package/dist/factory/http.d.ts +3 -0
- package/dist/factory/http.d.ts.map +1 -0
- package/dist/factory/http.js +555 -0
- package/dist/factory/http.js.map +1 -0
- package/dist/factory/lifecyclePushMap.d.ts +4 -0
- package/dist/factory/lifecyclePushMap.d.ts.map +1 -0
- package/dist/factory/lifecyclePushMap.js +7 -0
- package/dist/factory/lifecyclePushMap.js.map +1 -0
- package/dist/factory/missions.d.ts +125 -0
- package/dist/factory/missions.d.ts.map +1 -0
- package/dist/factory/missions.js +304 -0
- package/dist/factory/missions.js.map +1 -0
- package/dist/factory/mode.d.ts +9 -0
- package/dist/factory/mode.d.ts.map +1 -0
- package/dist/factory/mode.js +30 -0
- package/dist/factory/mode.js.map +1 -0
- package/dist/factory/operatorActiveSkill.d.ts +15 -0
- package/dist/factory/operatorActiveSkill.d.ts.map +1 -0
- package/dist/factory/operatorActiveSkill.js +95 -0
- package/dist/factory/operatorActiveSkill.js.map +1 -0
- package/dist/factory/paseoDispatcher.d.ts +52 -0
- package/dist/factory/paseoDispatcher.d.ts.map +1 -0
- package/dist/factory/paseoDispatcher.js +122 -0
- package/dist/factory/paseoDispatcher.js.map +1 -0
- package/dist/factory/paseoLifecycle.d.ts +32 -0
- package/dist/factory/paseoLifecycle.d.ts.map +1 -0
- package/dist/factory/paseoLifecycle.js +260 -0
- package/dist/factory/paseoLifecycle.js.map +1 -0
- package/dist/factory/paths.d.ts +31 -0
- package/dist/factory/paths.d.ts.map +1 -0
- package/dist/factory/paths.js +139 -0
- package/dist/factory/paths.js.map +1 -0
- package/dist/factory/progressWatchdog.d.ts +58 -0
- package/dist/factory/progressWatchdog.d.ts.map +1 -0
- package/dist/factory/progressWatchdog.js +160 -0
- package/dist/factory/progressWatchdog.js.map +1 -0
- package/dist/factory/roster.d.ts +59 -0
- package/dist/factory/roster.d.ts.map +1 -0
- package/dist/factory/roster.js +116 -0
- package/dist/factory/roster.js.map +1 -0
- package/dist/factory/runtime.d.ts +44 -0
- package/dist/factory/runtime.d.ts.map +1 -0
- package/dist/factory/runtime.js +238 -0
- package/dist/factory/runtime.js.map +1 -0
- package/dist/factory/sync.d.ts +29 -0
- package/dist/factory/sync.d.ts.map +1 -0
- package/dist/factory/sync.js +77 -0
- package/dist/factory/sync.js.map +1 -0
- package/dist/factory/workitemQueues.d.ts +37 -0
- package/dist/factory/workitemQueues.d.ts.map +1 -0
- package/dist/factory/workitemQueues.js +99 -0
- package/dist/factory/workitemQueues.js.map +1 -0
- package/dist/factory/workitemTriage.d.ts +9 -0
- package/dist/factory/workitemTriage.d.ts.map +1 -0
- package/dist/factory/workitemTriage.js +81 -0
- package/dist/factory/workitemTriage.js.map +1 -0
- package/dist/hooks.d.ts +18 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +96 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +90 -0
- package/dist/index.js.map +1 -0
- package/dist/install/agentCatalog.d.ts +7 -0
- package/dist/install/agentCatalog.d.ts.map +1 -0
- package/dist/install/agentCatalog.js +28 -0
- package/dist/install/agentCatalog.js.map +1 -0
- package/dist/install/bundlePaths.d.ts +10 -0
- package/dist/install/bundlePaths.d.ts.map +1 -0
- package/dist/install/bundlePaths.js +30 -0
- package/dist/install/bundlePaths.js.map +1 -0
- package/dist/install/codex.d.ts +43 -0
- package/dist/install/codex.d.ts.map +1 -0
- package/dist/install/codex.js +207 -0
- package/dist/install/codex.js.map +1 -0
- package/dist/install/enactHome.d.ts +37 -0
- package/dist/install/enactHome.d.ts.map +1 -0
- package/dist/install/enactHome.js +152 -0
- package/dist/install/enactHome.js.map +1 -0
- package/dist/install/plugins.d.ts +115 -0
- package/dist/install/plugins.d.ts.map +1 -0
- package/dist/install/plugins.js +259 -0
- package/dist/install/plugins.js.map +1 -0
- package/dist/install/setup.d.ts +33 -0
- package/dist/install/setup.d.ts.map +1 -0
- package/dist/install/setup.js +167 -0
- package/dist/install/setup.js.map +1 -0
- package/dist/locale/en.d.ts +3 -0
- package/dist/locale/en.d.ts.map +1 -0
- package/dist/locale/en.js +435 -0
- package/dist/locale/en.js.map +1 -0
- package/dist/locale/index.d.ts +28 -0
- package/dist/locale/index.d.ts.map +1 -0
- package/dist/locale/index.js +84 -0
- package/dist/locale/index.js.map +1 -0
- package/dist/locale/prompts/en.d.ts +3 -0
- package/dist/locale/prompts/en.d.ts.map +1 -0
- package/dist/locale/prompts/en.js +254 -0
- package/dist/locale/prompts/en.js.map +1 -0
- package/dist/locale/types.d.ts +433 -0
- package/dist/locale/types.d.ts.map +1 -0
- package/dist/locale/types.js +5 -0
- package/dist/locale/types.js.map +1 -0
- package/dist/mcp/server.d.ts +489 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +597 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/orchestration/decisionEngine.d.ts +175 -0
- package/dist/orchestration/decisionEngine.d.ts.map +1 -0
- package/dist/orchestration/decisionEngine.js +471 -0
- package/dist/orchestration/decisionEngine.js.map +1 -0
- package/dist/orchestration/index.d.ts +5 -0
- package/dist/orchestration/index.d.ts.map +1 -0
- package/dist/orchestration/index.js +5 -0
- package/dist/orchestration/index.js.map +1 -0
- package/dist/orchestration/workItemParser.d.ts +67 -0
- package/dist/orchestration/workItemParser.d.ts.map +1 -0
- package/dist/orchestration/workItemParser.js +560 -0
- package/dist/orchestration/workItemParser.js.map +1 -0
- package/dist/orchestration/workItemScheduler.d.ts +141 -0
- package/dist/orchestration/workItemScheduler.d.ts.map +1 -0
- package/dist/orchestration/workItemScheduler.js +317 -0
- package/dist/orchestration/workItemScheduler.js.map +1 -0
- package/dist/orchestration/workflow.d.ts +145 -0
- package/dist/orchestration/workflow.d.ts.map +1 -0
- package/dist/orchestration/workflow.js +301 -0
- package/dist/orchestration/workflow.js.map +1 -0
- package/dist/providers/codexSessions.d.ts +93 -0
- package/dist/providers/codexSessions.d.ts.map +1 -0
- package/dist/providers/codexSessions.js +366 -0
- package/dist/providers/codexSessions.js.map +1 -0
- package/dist/registry/bsDetector.d.ts +24 -0
- package/dist/registry/bsDetector.d.ts.map +1 -0
- package/dist/registry/bsDetector.js +276 -0
- package/dist/registry/bsDetector.js.map +1 -0
- package/dist/registry/entityScanner.d.ts +36 -0
- package/dist/registry/entityScanner.d.ts.map +1 -0
- package/dist/registry/entityScanner.js +693 -0
- package/dist/registry/entityScanner.js.map +1 -0
- package/dist/registry/index.d.ts +9 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +13 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/schema.d.ts +307 -0
- package/dist/registry/schema.d.ts.map +1 -0
- package/dist/registry/schema.js +139 -0
- package/dist/registry/schema.js.map +1 -0
- package/dist/registry/sqliteStore.d.ts +101 -0
- package/dist/registry/sqliteStore.d.ts.map +1 -0
- package/dist/registry/sqliteStore.js +688 -0
- package/dist/registry/sqliteStore.js.map +1 -0
- package/dist/registry/workItemBridge.d.ts +8 -0
- package/dist/registry/workItemBridge.d.ts.map +1 -0
- package/dist/registry/workItemBridge.js +30 -0
- package/dist/registry/workItemBridge.js.map +1 -0
- package/dist/runners/cliRunner.d.ts +11 -0
- package/dist/runners/cliRunner.d.ts.map +1 -0
- package/dist/runners/cliRunner.js +193 -0
- package/dist/runners/cliRunner.js.map +1 -0
- package/dist/support/apiCache.d.ts +85 -0
- package/dist/support/apiCache.d.ts.map +1 -0
- package/dist/support/apiCache.js +163 -0
- package/dist/support/apiCache.js.map +1 -0
- package/dist/support/chat.d.ts +3 -0
- package/dist/support/chat.d.ts.map +1 -0
- package/dist/support/chat.js +305 -0
- package/dist/support/chat.js.map +1 -0
- package/dist/support/chatBackend.d.ts +25 -0
- package/dist/support/chatBackend.d.ts.map +1 -0
- package/dist/support/chatBackend.js +289 -0
- package/dist/support/chatBackend.js.map +1 -0
- package/dist/support/chatTui.d.ts +3 -0
- package/dist/support/chatTui.d.ts.map +1 -0
- package/dist/support/chatTui.js +1082 -0
- package/dist/support/chatTui.js.map +1 -0
- package/dist/support/costTracker.d.ts +29 -0
- package/dist/support/costTracker.d.ts.map +1 -0
- package/dist/support/costTracker.js +113 -0
- package/dist/support/costTracker.js.map +1 -0
- package/dist/support/dashboardHtml.d.ts +5 -0
- package/dist/support/dashboardHtml.d.ts.map +1 -0
- package/dist/support/dashboardHtml.js +2629 -0
- package/dist/support/dashboardHtml.js.map +1 -0
- package/dist/support/dev.d.ts +55 -0
- package/dist/support/dev.d.ts.map +1 -0
- package/dist/support/dev.js +298 -0
- package/dist/support/dev.js.map +1 -0
- package/dist/support/editParser.d.ts +37 -0
- package/dist/support/editParser.d.ts.map +1 -0
- package/dist/support/editParser.js +365 -0
- package/dist/support/editParser.js.map +1 -0
- package/dist/support/ghosttyThemeCatalog.generated.d.ts +2 -0
- package/dist/support/ghosttyThemeCatalog.generated.d.ts.map +1 -0
- package/dist/support/ghosttyThemeCatalog.generated.js +11116 -0
- package/dist/support/ghosttyThemeCatalog.generated.js.map +1 -0
- package/dist/support/gitStatus.d.ts +21 -0
- package/dist/support/gitStatus.d.ts.map +1 -0
- package/dist/support/gitStatus.js +108 -0
- package/dist/support/gitStatus.js.map +1 -0
- package/dist/support/gitTracker.d.ts +30 -0
- package/dist/support/gitTracker.d.ts.map +1 -0
- package/dist/support/gitTracker.js +143 -0
- package/dist/support/gitTracker.js.map +1 -0
- package/dist/support/index.d.ts +12 -0
- package/dist/support/index.d.ts.map +1 -0
- package/dist/support/index.js +12 -0
- package/dist/support/index.js.map +1 -0
- package/dist/support/planner.d.ts +64 -0
- package/dist/support/planner.d.ts.map +1 -0
- package/dist/support/planner.js +396 -0
- package/dist/support/planner.js.map +1 -0
- package/dist/support/projectMapper.d.ts +46 -0
- package/dist/support/projectMapper.d.ts.map +1 -0
- package/dist/support/projectMapper.js +273 -0
- package/dist/support/projectMapper.js.map +1 -0
- package/dist/support/pty-helper.py +117 -0
- package/dist/support/quotaTracker.d.ts +29 -0
- package/dist/support/quotaTracker.d.ts.map +1 -0
- package/dist/support/quotaTracker.js +89 -0
- package/dist/support/quotaTracker.js.map +1 -0
- package/dist/support/rateLimiter.d.ts +101 -0
- package/dist/support/rateLimiter.d.ts.map +1 -0
- package/dist/support/rateLimiter.js +219 -0
- package/dist/support/rateLimiter.js.map +1 -0
- package/dist/support/rollback.d.ts +61 -0
- package/dist/support/rollback.d.ts.map +1 -0
- package/dist/support/rollback.js +329 -0
- package/dist/support/rollback.js.map +1 -0
- package/dist/support/sharedShell.d.ts +17 -0
- package/dist/support/sharedShell.d.ts.map +1 -0
- package/dist/support/sharedShell.js +439 -0
- package/dist/support/sharedShell.js.map +1 -0
- package/dist/support/stuckDetector.d.ts +68 -0
- package/dist/support/stuckDetector.d.ts.map +1 -0
- package/dist/support/stuckDetector.js +174 -0
- package/dist/support/stuckDetector.js.map +1 -0
- package/dist/support/terminalBridge.d.ts +18 -0
- package/dist/support/terminalBridge.d.ts.map +1 -0
- package/dist/support/terminalBridge.js +553 -0
- package/dist/support/terminalBridge.js.map +1 -0
- package/dist/support/timeWindow.d.ts +60 -0
- package/dist/support/timeWindow.d.ts.map +1 -0
- package/dist/support/timeWindow.js +236 -0
- package/dist/support/timeWindow.js.map +1 -0
- package/dist/support/uiThemes.d.ts +44 -0
- package/dist/support/uiThemes.d.ts.map +1 -0
- package/dist/support/uiThemes.js +290 -0
- package/dist/support/uiThemes.js.map +1 -0
- package/dist/support/web.d.ts +29 -0
- package/dist/support/web.d.ts.map +1 -0
- package/dist/support/web.js +1097 -0
- package/dist/support/web.js.map +1 -0
- package/dist/support/worktreeManager.d.ts +20 -0
- package/dist/support/worktreeManager.d.ts.map +1 -0
- package/dist/support/worktreeManager.js +140 -0
- package/dist/support/worktreeManager.js.map +1 -0
- package/dist/task_state_model.py +55 -0
- package/dist/workItemState/store.d.ts +122 -0
- package/dist/workItemState/store.d.ts.map +1 -0
- package/dist/workItemState/store.js +438 -0
- package/dist/workItemState/store.js.map +1 -0
- package/dist/workItems/azdoBridge.d.ts +42 -0
- package/dist/workItems/azdoBridge.d.ts.map +1 -0
- package/dist/workItems/azdoBridge.js +143 -0
- package/dist/workItems/azdoBridge.js.map +1 -0
- package/dist/workItems/azdoSyncRuntime.d.ts +28 -0
- package/dist/workItems/azdoSyncRuntime.d.ts.map +1 -0
- package/dist/workItems/azdoSyncRuntime.js +158 -0
- package/dist/workItems/azdoSyncRuntime.js.map +1 -0
- package/dist/workItems/azureDevOpsSync.d.ts +128 -0
- package/dist/workItems/azureDevOpsSync.d.ts.map +1 -0
- package/dist/workItems/azureDevOpsSync.js +748 -0
- package/dist/workItems/azureDevOpsSync.js.map +1 -0
- package/dist/workItems/helpers.d.ts +11 -0
- package/dist/workItems/helpers.d.ts.map +1 -0
- package/dist/workItems/helpers.js +17 -0
- package/dist/workItems/helpers.js.map +1 -0
- package/dist/workItems/index.d.ts +21 -0
- package/dist/workItems/index.d.ts.map +1 -0
- package/dist/workItems/index.js +89 -0
- package/dist/workItems/index.js.map +1 -0
- package/dist/workItems/localWorkItemFetcher.d.ts +55 -0
- package/dist/workItems/localWorkItemFetcher.d.ts.map +1 -0
- package/dist/workItems/localWorkItemFetcher.js +209 -0
- package/dist/workItems/localWorkItemFetcher.js.map +1 -0
- package/dist/workItems/migrations/001_rename_workItem_to_work_item.sql +10 -0
- package/dist/workItems/postgresStore.d.ts +78 -0
- package/dist/workItems/postgresStore.d.ts.map +1 -0
- package/dist/workItems/postgresStore.js +937 -0
- package/dist/workItems/postgresStore.js.map +1 -0
- package/dist/workItems/schema.d.ts +257 -0
- package/dist/workItems/schema.d.ts.map +1 -0
- package/dist/workItems/schema.js +176 -0
- package/dist/workItems/schema.js.map +1 -0
- package/dist/workItems/sqliteStore.d.ts +124 -0
- package/dist/workItems/sqliteStore.d.ts.map +1 -0
- package/dist/workItems/sqliteStore.js +713 -0
- package/dist/workItems/sqliteStore.js.map +1 -0
- package/dist/workItems/workItemBoardHtml.d.ts +5 -0
- package/dist/workItems/workItemBoardHtml.d.ts.map +1 -0
- package/dist/workItems/workItemBoardHtml.js +2192 -0
- package/dist/workItems/workItemBoardHtml.js.map +1 -0
- package/package.json +99 -0
- package/templates/AGENTS.md +432 -0
- package/templates/BOOT.md +25 -0
- package/templates/BOOTSTRAP.md +50 -0
- package/templates/CHANGELOG_AUDIT.md +74 -0
- package/templates/HEARTBEAT.md +86 -0
- package/templates/IDENTITY.md +27 -0
- package/templates/PR_LAND.md +75 -0
- package/templates/PR_REVIEW.md +97 -0
- package/templates/SOUL.dev.md +52 -0
- package/templates/SOUL.md +81 -0
- package/templates/TOOLS.md +52 -0
- package/templates/USER.md +22 -0
- package/templates/WORKITEM_ANALYSIS.md +31 -0
- package/templates/agents/executor.md +26 -0
- package/templates/agents/plan.md +22 -0
- package/templates/agents/ralph.md +37 -0
- package/templates/agents/review.md +22 -0
- package/templates/agents/team.md +39 -0
|
@@ -0,0 +1,2192 @@
|
|
|
1
|
+
import { DEFAULT_DASHBOARD_SURFACE_CONFIG, } from '../core/dashboardContract.js';
|
|
2
|
+
import { DEFAULT_THEME_ID, THEME_RECENTS_LIMIT, THEME_RECENTS_STORAGE_KEY, THEME_STORAGE_KEY, serializedThemes, serializedThemeFavorites, themeCssVars, themeOptionsHtml, } from '../support/uiThemes.js';
|
|
3
|
+
import { renderSharedRightRail, renderSharedShellCss, renderSharedShellHeader, renderSharedWorkItemStatsBar, } from '../support/sharedShell.js';
|
|
4
|
+
// ============================================
|
|
5
|
+
// Enact Factory - Work Item Board HTML Template
|
|
6
|
+
// Created: 2026-04-03
|
|
7
|
+
// Updated: 2026-05-31 — shared shell with terminal right rail
|
|
8
|
+
// Purpose: Kanban board + shared operator shell + work item editing UI
|
|
9
|
+
// ============================================
|
|
10
|
+
function escAttr(value) {
|
|
11
|
+
return value
|
|
12
|
+
.replace(/&/g, '&')
|
|
13
|
+
.replace(/"/g, '"')
|
|
14
|
+
.replace(/</g, '<')
|
|
15
|
+
.replace(/>/g, '>');
|
|
16
|
+
}
|
|
17
|
+
function renderOptions(options, selected) {
|
|
18
|
+
return options
|
|
19
|
+
.map((option) => `<option value="${escAttr(option.value)}"${option.value === selected ? ' selected' : ''}>${escAttr(option.label)}</option>`)
|
|
20
|
+
.join('');
|
|
21
|
+
}
|
|
22
|
+
export function createWorkItemBoardHtml(config = DEFAULT_DASHBOARD_SURFACE_CONFIG) {
|
|
23
|
+
const repoOptionsHtml = renderOptions(config.repoOptions, config.selectedRepo === 'All' ? '' : config.selectedRepo);
|
|
24
|
+
const boardThemeOptionsHtml = themeOptionsHtml(DEFAULT_THEME_ID);
|
|
25
|
+
const boardThemeVars = themeCssVars(DEFAULT_THEME_ID);
|
|
26
|
+
const appThemesJson = serializedThemes();
|
|
27
|
+
const appThemeFavoritesJson = serializedThemeFavorites();
|
|
28
|
+
const sharedShellCss = renderSharedShellCss();
|
|
29
|
+
const sharedHeaderHtml = renderSharedShellHeader({
|
|
30
|
+
repoOptionsHtml,
|
|
31
|
+
themeOptionsHtml: boardThemeOptionsHtml,
|
|
32
|
+
themeOnChange: 'applyAppTheme(this.value)',
|
|
33
|
+
themeFilterOnInput: 'filterAppThemes(this.value)',
|
|
34
|
+
});
|
|
35
|
+
const sharedWorkItemStatsBarHtml = renderSharedWorkItemStatsBar({
|
|
36
|
+
switchHref: '/',
|
|
37
|
+
switchLabel: 'DASHBOARD',
|
|
38
|
+
});
|
|
39
|
+
const sharedRightRailHtml = renderSharedRightRail();
|
|
40
|
+
return `<!DOCTYPE html>
|
|
41
|
+
<html lang="ko">
|
|
42
|
+
<head>
|
|
43
|
+
<meta charset="UTF-8">
|
|
44
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
45
|
+
<title>Enact Factory :: Board</title>
|
|
46
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css">
|
|
47
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
|
|
48
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
|
|
49
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links/lib/xterm-addon-web-links.js"></script>
|
|
50
|
+
<style>
|
|
51
|
+
:root {
|
|
52
|
+
${boardThemeVars}
|
|
53
|
+
--panel-soft: var(--panelSoft);
|
|
54
|
+
--green-dim: var(--greenDim);
|
|
55
|
+
--green-mid: var(--greenMid);
|
|
56
|
+
--green-lo: var(--greenLo);
|
|
57
|
+
--cyan-dim: var(--cyanDim);
|
|
58
|
+
}
|
|
59
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
60
|
+
body {
|
|
61
|
+
font-family: 'Cascadia Code', 'JetBrains Mono', 'Fira Code', monospace;
|
|
62
|
+
background:
|
|
63
|
+
radial-gradient(circle at 12% 0%, var(--glow), transparent 32rem),
|
|
64
|
+
linear-gradient(180deg, var(--bodyTop) 0%, var(--bg) 48%, var(--bodyBottom) 100%);
|
|
65
|
+
color: var(--white);
|
|
66
|
+
font-size: 15px;
|
|
67
|
+
line-height: 1.45;
|
|
68
|
+
height: 100vh;
|
|
69
|
+
display: flex;
|
|
70
|
+
flex-direction: column;
|
|
71
|
+
overflow: hidden;
|
|
72
|
+
}
|
|
73
|
+
${sharedShellCss}
|
|
74
|
+
|
|
75
|
+
/* Toolbar — spans full width */
|
|
76
|
+
.toolbar {
|
|
77
|
+
background: color-mix(in srgb, var(--panel) 88%, transparent);
|
|
78
|
+
border-bottom: 1px solid var(--border);
|
|
79
|
+
padding: 6px 1rem;
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
gap: 0.5rem;
|
|
83
|
+
flex-shrink: 0;
|
|
84
|
+
}
|
|
85
|
+
.btn {
|
|
86
|
+
font-family: inherit;
|
|
87
|
+
font-size: 10px;
|
|
88
|
+
padding: 3px 10px;
|
|
89
|
+
background: color-mix(in srgb, var(--elevated) 72%, transparent);
|
|
90
|
+
border: 1px solid var(--green-lo);
|
|
91
|
+
color: var(--green-mid);
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
letter-spacing: 0.06em;
|
|
94
|
+
transition: all 0.15s;
|
|
95
|
+
}
|
|
96
|
+
.btn:hover:not(:disabled) { border-color: var(--green); color: var(--green); background: var(--green-dim); }
|
|
97
|
+
.btn:disabled { opacity: 0.4; cursor: default; }
|
|
98
|
+
.btn-active { border-color: var(--amber); color: var(--amber); }
|
|
99
|
+
.btn-active:hover:not(:disabled) { background: var(--warnBg); border-color: var(--amber); }
|
|
100
|
+
.btn-danger { border-color: var(--dangerBorder); color: var(--red); }
|
|
101
|
+
.btn-danger:hover:not(:disabled) { background: var(--dangerBg); border-color: var(--red); }
|
|
102
|
+
#turbo-btn { border-color: var(--warnBorder); color: var(--amber); transition: all 0.3s; }
|
|
103
|
+
#turbo-btn:hover:not(:disabled) { background: var(--warnBg); border-color: var(--amber); }
|
|
104
|
+
#turbo-btn.turbo-active { background: var(--warnBg); border-color: var(--amber); color: var(--amber); box-shadow: 0 0 8px color-mix(in srgb, var(--amber) 35%, transparent); animation: turbo-pulse 2s infinite; }
|
|
105
|
+
.btn-primary { color: var(--cyan); border-color: var(--cyan-dim); }
|
|
106
|
+
.btn-primary:hover:not(:disabled) { background: var(--cyan-dim); }
|
|
107
|
+
.btn-toggle { font-size: 10px; padding: 2px 8px; }
|
|
108
|
+
.btn-toggle.active { background: var(--green-dim); color: var(--green); }
|
|
109
|
+
.filter-select {
|
|
110
|
+
background: var(--panel-soft);
|
|
111
|
+
color: var(--white);
|
|
112
|
+
border: 1px solid var(--border);
|
|
113
|
+
padding: 3px 6px;
|
|
114
|
+
font-family: inherit;
|
|
115
|
+
font-size: 11px;
|
|
116
|
+
border-radius: 3px;
|
|
117
|
+
}
|
|
118
|
+
.search-input {
|
|
119
|
+
background: var(--bg3);
|
|
120
|
+
color: var(--white);
|
|
121
|
+
border: 1px solid var(--border);
|
|
122
|
+
padding: 3px 8px;
|
|
123
|
+
font-family: inherit;
|
|
124
|
+
font-size: 11px;
|
|
125
|
+
width: 200px;
|
|
126
|
+
border-radius: 3px;
|
|
127
|
+
}
|
|
128
|
+
.search-input::placeholder { color: var(--dim); }
|
|
129
|
+
|
|
130
|
+
/* ===== MAIN CONTENT: board canvas + shared right rail ===== */
|
|
131
|
+
.main-grid {
|
|
132
|
+
display: grid;
|
|
133
|
+
grid-template-columns: 70% 30%;
|
|
134
|
+
flex: 1;
|
|
135
|
+
min-height: 0;
|
|
136
|
+
overflow: hidden;
|
|
137
|
+
}
|
|
138
|
+
.board-shell {
|
|
139
|
+
display: flex;
|
|
140
|
+
flex-direction: column;
|
|
141
|
+
border-right: 1px solid var(--border);
|
|
142
|
+
overflow: hidden;
|
|
143
|
+
background: linear-gradient(
|
|
144
|
+
180deg,
|
|
145
|
+
color-mix(in srgb, var(--panelSoft) 78%, transparent),
|
|
146
|
+
color-mix(in srgb, var(--panel) 94%, transparent)
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* Left pane: Kanban Board */
|
|
151
|
+
.board-pane {
|
|
152
|
+
display: flex;
|
|
153
|
+
flex-direction: column;
|
|
154
|
+
overflow: hidden;
|
|
155
|
+
flex: 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Kanban board — 6 equal-width columns via CSS grid */
|
|
159
|
+
.board {
|
|
160
|
+
flex: 1;
|
|
161
|
+
display: grid;
|
|
162
|
+
grid-template-columns: repeat(6, 1fr);
|
|
163
|
+
gap: 2px;
|
|
164
|
+
padding: 8px;
|
|
165
|
+
overflow: hidden;
|
|
166
|
+
min-height: 0;
|
|
167
|
+
}
|
|
168
|
+
/* When "Show removed" is active, JS adds the 7th column class */
|
|
169
|
+
.board.show-removed {
|
|
170
|
+
grid-template-columns: repeat(7, 1fr);
|
|
171
|
+
}
|
|
172
|
+
.column {
|
|
173
|
+
background: var(--panel);
|
|
174
|
+
border: 1px solid var(--border);
|
|
175
|
+
border-radius: 4px;
|
|
176
|
+
display: flex;
|
|
177
|
+
flex-direction: column;
|
|
178
|
+
min-width: 0;
|
|
179
|
+
overflow: hidden;
|
|
180
|
+
}
|
|
181
|
+
.column.col-removed {
|
|
182
|
+
opacity: 0.6;
|
|
183
|
+
border-style: dashed;
|
|
184
|
+
}
|
|
185
|
+
.col-header {
|
|
186
|
+
padding: 6px 10px;
|
|
187
|
+
background: var(--panel-soft);
|
|
188
|
+
border-bottom: 1px solid var(--border);
|
|
189
|
+
font-size: 11px;
|
|
190
|
+
letter-spacing: 0.1em;
|
|
191
|
+
display: flex;
|
|
192
|
+
align-items: center;
|
|
193
|
+
gap: 6px;
|
|
194
|
+
}
|
|
195
|
+
.col-header .count {
|
|
196
|
+
background: var(--green-dim);
|
|
197
|
+
color: var(--green);
|
|
198
|
+
padding: 1px 6px;
|
|
199
|
+
border-radius: 8px;
|
|
200
|
+
font-size: 10px;
|
|
201
|
+
}
|
|
202
|
+
.col-body {
|
|
203
|
+
flex: 1;
|
|
204
|
+
overflow-y: auto;
|
|
205
|
+
padding: 4px;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* Work Item Card */
|
|
209
|
+
.card {
|
|
210
|
+
background: var(--bg3);
|
|
211
|
+
border: 1px solid var(--border);
|
|
212
|
+
border-radius: 3px;
|
|
213
|
+
padding: 8px;
|
|
214
|
+
margin-bottom: 4px;
|
|
215
|
+
cursor: pointer;
|
|
216
|
+
transition: border-color 0.15s;
|
|
217
|
+
}
|
|
218
|
+
.card:hover { border-color: var(--green-lo); }
|
|
219
|
+
.card-title { font-size: 12px; color: var(--white); margin-bottom: 4px; }
|
|
220
|
+
.card-meta { font-size: 10px; color: var(--dim); display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
|
|
221
|
+
.card-priority {
|
|
222
|
+
display: inline-block;
|
|
223
|
+
width: 6px;
|
|
224
|
+
height: 6px;
|
|
225
|
+
border-radius: 50%;
|
|
226
|
+
margin-right: 4px;
|
|
227
|
+
}
|
|
228
|
+
.p-urgent { background: var(--red); }
|
|
229
|
+
.p-high { background: var(--amber); }
|
|
230
|
+
.p-medium { background: var(--cyan); }
|
|
231
|
+
.p-low { background: var(--dim); }
|
|
232
|
+
.p-none { background: transparent; border: 1px solid var(--dim); }
|
|
233
|
+
.card-label {
|
|
234
|
+
font-size: 9px;
|
|
235
|
+
padding: 1px 4px;
|
|
236
|
+
border-radius: 2px;
|
|
237
|
+
background: var(--green-dim);
|
|
238
|
+
color: var(--green-mid);
|
|
239
|
+
}
|
|
240
|
+
.card-id { color: var(--dim); font-size: 9px; }
|
|
241
|
+
|
|
242
|
+
/* Parent breadcrumb */
|
|
243
|
+
.card-breadcrumb {
|
|
244
|
+
font-size: 9px;
|
|
245
|
+
color: var(--dim);
|
|
246
|
+
margin-bottom: 3px;
|
|
247
|
+
overflow: hidden;
|
|
248
|
+
text-overflow: ellipsis;
|
|
249
|
+
white-space: nowrap;
|
|
250
|
+
}
|
|
251
|
+
.card-breadcrumb.has-parent { color: var(--cyan-dim); }
|
|
252
|
+
|
|
253
|
+
/* Operator Lane badge */
|
|
254
|
+
.badge-lane {
|
|
255
|
+
font-size: 9px;
|
|
256
|
+
padding: 1px 5px;
|
|
257
|
+
border-radius: 2px;
|
|
258
|
+
font-weight: bold;
|
|
259
|
+
letter-spacing: 0.05em;
|
|
260
|
+
}
|
|
261
|
+
.lane-ralph { background: var(--warnBg); color: var(--amber); }
|
|
262
|
+
.lane-ultrawork { background: var(--cyanDim); color: var(--cyan); }
|
|
263
|
+
.lane-autopilot { background: color-mix(in srgb, var(--magenta, var(--cyan)) 18%, transparent); color: var(--magenta, var(--cyan)); }
|
|
264
|
+
.lane-team { background: var(--greenDim); color: var(--green); }
|
|
265
|
+
.lane-manual { background: var(--neutralBg); color: var(--dim); }
|
|
266
|
+
.lane-none { display: none; }
|
|
267
|
+
|
|
268
|
+
/* Failure Reason badge */
|
|
269
|
+
.badge-failure {
|
|
270
|
+
font-size: 9px;
|
|
271
|
+
padding: 1px 5px;
|
|
272
|
+
border-radius: 2px;
|
|
273
|
+
background: var(--red);
|
|
274
|
+
color: var(--white);
|
|
275
|
+
font-weight: bold;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* Six-dot lifecycle indicator */
|
|
279
|
+
.lifecycle-dots {
|
|
280
|
+
display: flex;
|
|
281
|
+
gap: 3px;
|
|
282
|
+
align-items: center;
|
|
283
|
+
}
|
|
284
|
+
.lifecycle-dots .dot {
|
|
285
|
+
width: 6px;
|
|
286
|
+
height: 6px;
|
|
287
|
+
border-radius: 50%;
|
|
288
|
+
border: 1px solid var(--dim);
|
|
289
|
+
background: transparent;
|
|
290
|
+
flex-shrink: 0;
|
|
291
|
+
}
|
|
292
|
+
.lifecycle-dots .dot.filled {
|
|
293
|
+
background: var(--green);
|
|
294
|
+
border-color: var(--green);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* Modal */
|
|
298
|
+
.modal-overlay {
|
|
299
|
+
display: none;
|
|
300
|
+
position: fixed;
|
|
301
|
+
inset: 0;
|
|
302
|
+
background: var(--overlay);
|
|
303
|
+
z-index: 100;
|
|
304
|
+
justify-content: center;
|
|
305
|
+
align-items: center;
|
|
306
|
+
}
|
|
307
|
+
.modal-overlay.active { display: flex; }
|
|
308
|
+
.modal {
|
|
309
|
+
background: var(--bg2);
|
|
310
|
+
border: 1px solid var(--green-dim);
|
|
311
|
+
border-radius: 6px;
|
|
312
|
+
width: 90%;
|
|
313
|
+
max-width: 600px;
|
|
314
|
+
max-height: 85vh;
|
|
315
|
+
overflow-y: auto;
|
|
316
|
+
padding: 1.5rem;
|
|
317
|
+
}
|
|
318
|
+
.modal h3 { color: var(--green); font-size: 14px; margin-bottom: 1rem; letter-spacing: 0.1em; }
|
|
319
|
+
.form-group { margin-bottom: 0.75rem; }
|
|
320
|
+
.form-group label { display: block; color: var(--dim); font-size: 10px; margin-bottom: 3px; letter-spacing: 0.05em; }
|
|
321
|
+
.form-group input, .form-group textarea, .form-group select {
|
|
322
|
+
width: 100%;
|
|
323
|
+
background: var(--bg3);
|
|
324
|
+
color: var(--white);
|
|
325
|
+
border: 1px solid var(--border);
|
|
326
|
+
padding: 6px 8px;
|
|
327
|
+
font-family: inherit;
|
|
328
|
+
font-size: 12px;
|
|
329
|
+
border-radius: 3px;
|
|
330
|
+
}
|
|
331
|
+
.form-group textarea { min-height: 80px; resize: vertical; }
|
|
332
|
+
.form-actions { display: flex; gap: 0.5rem; margin-top: 1rem; justify-content: flex-end; }
|
|
333
|
+
.form-actions .btn { padding: 5px 16px; }
|
|
334
|
+
|
|
335
|
+
/* Detail panel — responsive overlay over the right column */
|
|
336
|
+
.detail-panel {
|
|
337
|
+
display: none;
|
|
338
|
+
position: fixed;
|
|
339
|
+
top: 0;
|
|
340
|
+
right: 0;
|
|
341
|
+
width: 50vw;
|
|
342
|
+
min-width: 480px;
|
|
343
|
+
max-width: 900px;
|
|
344
|
+
height: 100%;
|
|
345
|
+
background: var(--bg2);
|
|
346
|
+
border-left: 1px solid var(--green-dim);
|
|
347
|
+
z-index: 90;
|
|
348
|
+
overflow-y: auto;
|
|
349
|
+
padding: 1.25rem 1.5rem;
|
|
350
|
+
}
|
|
351
|
+
.detail-panel.active { display: block; }
|
|
352
|
+
.detail-close { float: right; cursor: pointer; color: var(--dim); font-size: 16px; }
|
|
353
|
+
.detail-title { color: var(--green); font-size: 14px; margin-bottom: 0.5rem; margin-right: 2rem; }
|
|
354
|
+
.detail-section { margin-top: 1rem; }
|
|
355
|
+
.detail-section h4 { color: var(--cyan); font-size: 11px; letter-spacing: 0.05em; margin-bottom: 0.25rem; }
|
|
356
|
+
.detail-section p, .detail-section ul { color: var(--white); font-size: 12px; }
|
|
357
|
+
.detail-section ul { padding-left: 1rem; }
|
|
358
|
+
.event-item { font-size: 11px; color: var(--dim); padding: 3px 0; border-bottom: 1px solid var(--border); }
|
|
359
|
+
|
|
360
|
+
/* Hide scrollbars while preserving scroll behavior */
|
|
361
|
+
.col-body,
|
|
362
|
+
.modal,
|
|
363
|
+
.detail-panel,
|
|
364
|
+
.panel-body {
|
|
365
|
+
-ms-overflow-style: none; /* IE/Edge legacy */
|
|
366
|
+
scrollbar-width: none; /* Firefox */
|
|
367
|
+
}
|
|
368
|
+
.col-body::-webkit-scrollbar,
|
|
369
|
+
.modal::-webkit-scrollbar,
|
|
370
|
+
.detail-panel::-webkit-scrollbar,
|
|
371
|
+
.panel-body::-webkit-scrollbar {
|
|
372
|
+
width: 0;
|
|
373
|
+
height: 0;
|
|
374
|
+
display: none; /* Chromium/WebKit */
|
|
375
|
+
background: transparent;
|
|
376
|
+
}
|
|
377
|
+
.event-item .ev-type { color: var(--amber); }
|
|
378
|
+
|
|
379
|
+
</style>
|
|
380
|
+
</head>
|
|
381
|
+
<body>
|
|
382
|
+
${sharedHeaderHtml}
|
|
383
|
+
|
|
384
|
+
${sharedWorkItemStatsBarHtml}
|
|
385
|
+
|
|
386
|
+
<div class="toolbar">
|
|
387
|
+
<button class="btn btn-primary" onclick="openCreateModal()">+ NEW WORK ITEM</button>
|
|
388
|
+
<select class="filter-select" id="filter-area" onchange="applyAreaFilter()">
|
|
389
|
+
<option value="">all areas</option>
|
|
390
|
+
</select>
|
|
391
|
+
<select class="filter-select" id="filter-priority" onchange="applyPriorityFilter()">
|
|
392
|
+
<option value="">all priorities</option>
|
|
393
|
+
<option value="urgent">urgent</option>
|
|
394
|
+
<option value="high">high</option>
|
|
395
|
+
<option value="medium">medium</option>
|
|
396
|
+
<option value="low">low</option>
|
|
397
|
+
</select>
|
|
398
|
+
<input type="text" class="search-input" id="search-input" placeholder="search work items..." oninput="debounceSearch()">
|
|
399
|
+
<button class="btn btn-toggle" id="show-removed-btn" onclick="toggleShowRemoved()">Show removed</button>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
<!-- board canvas + shared right rail -->
|
|
403
|
+
<div class="main-grid">
|
|
404
|
+
|
|
405
|
+
<div class="board-shell">
|
|
406
|
+
<!-- LEFT PANE: Kanban Board -->
|
|
407
|
+
<div class="board-pane">
|
|
408
|
+
<div class="board" id="board">
|
|
409
|
+
<!-- Kanban columns rendered by JS -->
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
|
|
414
|
+
${sharedRightRailHtml}
|
|
415
|
+
|
|
416
|
+
</div>
|
|
417
|
+
|
|
418
|
+
<!-- Work Item creation modal -->
|
|
419
|
+
<div class="modal-overlay" id="create-modal">
|
|
420
|
+
<div class="modal">
|
|
421
|
+
<h3>NEW WORK ITEM</h3>
|
|
422
|
+
<div class="form-group">
|
|
423
|
+
<label>AREA</label>
|
|
424
|
+
<select id="new-project"></select>
|
|
425
|
+
</div>
|
|
426
|
+
<div class="form-group">
|
|
427
|
+
<label>TITLE</label>
|
|
428
|
+
<input type="text" id="new-title" placeholder="work item title">
|
|
429
|
+
</div>
|
|
430
|
+
<div class="form-group">
|
|
431
|
+
<label>DESCRIPTION</label>
|
|
432
|
+
<textarea id="new-desc" placeholder="details..."></textarea>
|
|
433
|
+
</div>
|
|
434
|
+
<div style="display:flex;gap:0.75rem">
|
|
435
|
+
<div class="form-group" style="flex:1">
|
|
436
|
+
<label>PRIORITY</label>
|
|
437
|
+
<select id="new-priority">
|
|
438
|
+
<option value="medium">medium</option>
|
|
439
|
+
<option value="urgent">urgent</option>
|
|
440
|
+
<option value="high">high</option>
|
|
441
|
+
<option value="low">low</option>
|
|
442
|
+
<option value="none">none</option>
|
|
443
|
+
</select>
|
|
444
|
+
</div>
|
|
445
|
+
<div class="form-group" style="flex:1">
|
|
446
|
+
<label>STATUS</label>
|
|
447
|
+
<select id="new-status">
|
|
448
|
+
<option value="New">New</option>
|
|
449
|
+
<option value="Approved">Approved</option>
|
|
450
|
+
<option value="Committed">Committed</option>
|
|
451
|
+
<option value="In Progress">In Progress</option>
|
|
452
|
+
<option value="In Testing">In Testing</option>
|
|
453
|
+
</select>
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
<div class="form-group">
|
|
457
|
+
<label>LABELS (comma separated)</label>
|
|
458
|
+
<input type="text" id="new-labels" placeholder="bug, feature, ...">
|
|
459
|
+
</div>
|
|
460
|
+
<div class="form-group">
|
|
461
|
+
<label>RELEVANT FILES (comma separated)</label>
|
|
462
|
+
<input type="text" id="new-files" placeholder="src/foo.ts, src/bar.ts">
|
|
463
|
+
</div>
|
|
464
|
+
<div class="form-actions">
|
|
465
|
+
<button class="btn" onclick="closeCreateModal()">CANCEL</button>
|
|
466
|
+
<button class="btn btn-primary" onclick="createWorkItem()">CREATE</button>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
|
|
471
|
+
<!-- Work Item detail panel -->
|
|
472
|
+
<div class="detail-panel" id="detail-panel">
|
|
473
|
+
<span class="detail-close" onclick="closeDetail()">×</span>
|
|
474
|
+
<div id="detail-content"></div>
|
|
475
|
+
</div>
|
|
476
|
+
|
|
477
|
+
<script>
|
|
478
|
+
const APP_THEMES = ${appThemesJson};
|
|
479
|
+
const THEME_FAVORITES = ${appThemeFavoritesJson};
|
|
480
|
+
const THEME_STORAGE = '${THEME_STORAGE_KEY}';
|
|
481
|
+
const THEME_RECENTS = '${THEME_RECENTS_STORAGE_KEY}';
|
|
482
|
+
const THEME_RECENT_LIMIT = ${THEME_RECENTS_LIMIT};
|
|
483
|
+
const FALLBACK_THEME = '${DEFAULT_THEME_ID}';
|
|
484
|
+
const TERMINAL_SESSION_STORAGE_KEY = "enact-factory:dashboard:terminal-session";
|
|
485
|
+
let appThemeQuery = '';
|
|
486
|
+
let terminalSocket = null;
|
|
487
|
+
let terminalInstance = null;
|
|
488
|
+
let terminalFitAddon = null;
|
|
489
|
+
let terminalReconnectTimer = null;
|
|
490
|
+
let terminalSessionId = null;
|
|
491
|
+
let terminalShuttingDown = false;
|
|
492
|
+
let laneRows = [];
|
|
493
|
+
let currentRepoScope = "All";
|
|
494
|
+
// Toolbar filters — applied CLIENT-SIDE over the already-loaded work items.
|
|
495
|
+
// Changing these must never trigger a network/AzDO fetch.
|
|
496
|
+
let currentAreaFilter = "";
|
|
497
|
+
let currentPriorityFilter = "";
|
|
498
|
+
let lastSyncAt = null;
|
|
499
|
+
|
|
500
|
+
function isAppThemeId(value) {
|
|
501
|
+
return typeof value === 'string' && Object.prototype.hasOwnProperty.call(APP_THEMES, value);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function getRecentThemeIds() {
|
|
505
|
+
try {
|
|
506
|
+
const raw = window.localStorage.getItem(THEME_RECENTS);
|
|
507
|
+
if (!raw) return [];
|
|
508
|
+
const parsed = JSON.parse(raw);
|
|
509
|
+
if (!Array.isArray(parsed)) return [];
|
|
510
|
+
return parsed.filter(isAppThemeId);
|
|
511
|
+
} catch (e) {
|
|
512
|
+
return [];
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function saveRecentThemeId(themeId) {
|
|
517
|
+
try {
|
|
518
|
+
const next = [themeId, ...getRecentThemeIds().filter((id) => id !== themeId)]
|
|
519
|
+
.slice(0, THEME_RECENT_LIMIT);
|
|
520
|
+
window.localStorage.setItem(THEME_RECENTS, JSON.stringify(next));
|
|
521
|
+
} catch (e) {
|
|
522
|
+
// ignore storage failures
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function themeCssName(rawKey) {
|
|
527
|
+
return '--' + rawKey.replace(/[A-Z]/g, (match) => '-' + match.toLowerCase());
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function themeMatchesQuery(theme, query) {
|
|
531
|
+
if (!query) return true;
|
|
532
|
+
const haystack = (theme.id + ' ' + theme.label).toLowerCase();
|
|
533
|
+
return haystack.includes(query);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function buildThemeOptionsHtml(selectedThemeId, query) {
|
|
537
|
+
const normalizedQuery = (query || '').trim().toLowerCase();
|
|
538
|
+
const allThemes = Object.values(APP_THEMES);
|
|
539
|
+
const enactTheme = allThemes.find((theme) => theme.id === 'enact');
|
|
540
|
+
const favoriteIds = new Set(THEME_FAVORITES.filter(isAppThemeId));
|
|
541
|
+
const recentIds = getRecentThemeIds()
|
|
542
|
+
.filter((id) => id !== 'enact');
|
|
543
|
+
const recentThemes = recentIds
|
|
544
|
+
.map((id) => APP_THEMES[id])
|
|
545
|
+
.filter(Boolean)
|
|
546
|
+
.sort((a, b) => recentIds.indexOf(a.id) - recentIds.indexOf(b.id));
|
|
547
|
+
const favorites = THEME_FAVORITES
|
|
548
|
+
.map((id) => APP_THEMES[id])
|
|
549
|
+
.filter(Boolean)
|
|
550
|
+
.filter((theme) => !recentThemes.some((recent) => recent.id === theme.id));
|
|
551
|
+
const excludedIds = new Set([
|
|
552
|
+
'enact',
|
|
553
|
+
...recentThemes.map((theme) => theme.id),
|
|
554
|
+
...favorites.map((theme) => theme.id),
|
|
555
|
+
]);
|
|
556
|
+
const remaining = allThemes
|
|
557
|
+
.filter((theme) => !excludedIds.has(theme.id))
|
|
558
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
559
|
+
|
|
560
|
+
const renderOption = (theme) =>
|
|
561
|
+
'<option value="' + theme.id + '"' + (theme.id === selectedThemeId ? ' selected' : '') + '>' + theme.label + '</option>';
|
|
562
|
+
|
|
563
|
+
const filterGroup = (themes) => themes.filter((theme) =>
|
|
564
|
+
theme.id === selectedThemeId ? true : themeMatchesQuery(theme, normalizedQuery),
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
const groups = [];
|
|
568
|
+
const enactThemes = enactTheme ? filterGroup([enactTheme]) : [];
|
|
569
|
+
const recentFiltered = filterGroup(recentThemes);
|
|
570
|
+
const favoritesFiltered = filterGroup(favorites);
|
|
571
|
+
const remainingFiltered = filterGroup(remaining);
|
|
572
|
+
if (enactThemes.length) groups.push('<optgroup label="Enact">' + enactThemes.map(renderOption).join('') + '</optgroup>');
|
|
573
|
+
if (recentFiltered.length) groups.push('<optgroup label="Recent">' + recentFiltered.map(renderOption).join('') + '</optgroup>');
|
|
574
|
+
if (favoritesFiltered.length) groups.push('<optgroup label="Ghostty Favorites">' + favoritesFiltered.map(renderOption).join('') + '</optgroup>');
|
|
575
|
+
if (remainingFiltered.length) groups.push('<optgroup label="Ghostty Themes">' + remainingFiltered.map(renderOption).join('') + '</optgroup>');
|
|
576
|
+
return groups.join('');
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function filterAppThemes(query) {
|
|
580
|
+
appThemeQuery = query || '';
|
|
581
|
+
const select = document.getElementById('app-theme-select');
|
|
582
|
+
const currentTheme = select ? select.value : (window.localStorage.getItem(THEME_STORAGE) || FALLBACK_THEME);
|
|
583
|
+
if (select) {
|
|
584
|
+
select.innerHTML = buildThemeOptionsHtml(currentTheme, appThemeQuery);
|
|
585
|
+
}
|
|
586
|
+
syncThemePickerUi(currentTheme);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function syncThemePickerUi(themeId) {
|
|
590
|
+
const select = document.getElementById('app-theme-select');
|
|
591
|
+
const current = document.getElementById('app-theme-current');
|
|
592
|
+
const launcher = document.getElementById('theme-launcher-btn');
|
|
593
|
+
const normalizedThemeId = isAppThemeId(themeId) ? themeId : (window.localStorage.getItem(THEME_STORAGE) || FALLBACK_THEME);
|
|
594
|
+
const label = APP_THEMES[normalizedThemeId]?.label || APP_THEMES[FALLBACK_THEME]?.label || 'Enact';
|
|
595
|
+
if (current) current.textContent = label;
|
|
596
|
+
if (select) select.value = normalizedThemeId;
|
|
597
|
+
if (launcher) launcher.setAttribute('aria-expanded', String(isThemePickerOpen()));
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function isThemePickerOpen() {
|
|
601
|
+
return document.getElementById('theme-popover')?.classList.contains('open') ?? false;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function closeThemePicker() {
|
|
605
|
+
const popover = document.getElementById('theme-popover');
|
|
606
|
+
if (!popover) return;
|
|
607
|
+
popover.classList.remove('open');
|
|
608
|
+
syncThemePickerUi(window.localStorage.getItem(THEME_STORAGE) || FALLBACK_THEME);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function toggleThemePicker(event) {
|
|
612
|
+
if (event) event.stopPropagation();
|
|
613
|
+
const popover = document.getElementById('theme-popover');
|
|
614
|
+
if (!popover) return;
|
|
615
|
+
const nextOpen = !popover.classList.contains('open');
|
|
616
|
+
popover.classList.toggle('open', nextOpen);
|
|
617
|
+
syncThemePickerUi(window.localStorage.getItem(THEME_STORAGE) || FALLBACK_THEME);
|
|
618
|
+
if (nextOpen) {
|
|
619
|
+
const filter = document.getElementById('app-theme-filter');
|
|
620
|
+
if (filter) filter.focus();
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function applyAppTheme(themeId) {
|
|
625
|
+
const root = document.documentElement;
|
|
626
|
+
const normalizedThemeId = isAppThemeId(themeId) ? themeId : FALLBACK_THEME;
|
|
627
|
+
const theme = APP_THEMES[normalizedThemeId];
|
|
628
|
+
if (!theme) return;
|
|
629
|
+
|
|
630
|
+
const vars = theme.vars || {};
|
|
631
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
632
|
+
const cssName = themeCssName(key);
|
|
633
|
+
root.style.setProperty(cssName, value);
|
|
634
|
+
root.style.setProperty('--' + key, value);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const legacyAlias = {
|
|
638
|
+
panelSoft: '--panel-soft',
|
|
639
|
+
greenDim: '--green-dim',
|
|
640
|
+
greenMid: '--green-mid',
|
|
641
|
+
greenLo: '--green-lo',
|
|
642
|
+
cyanDim: '--cyan-dim',
|
|
643
|
+
};
|
|
644
|
+
for (const [legacyKey, legacyVar] of Object.entries(legacyAlias)) {
|
|
645
|
+
root.style.setProperty(legacyVar, vars[legacyKey]);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const select = document.getElementById('app-theme-select');
|
|
649
|
+
if (select) {
|
|
650
|
+
select.innerHTML = buildThemeOptionsHtml(normalizedThemeId, appThemeQuery);
|
|
651
|
+
}
|
|
652
|
+
syncThemePickerUi(normalizedThemeId);
|
|
653
|
+
|
|
654
|
+
try {
|
|
655
|
+
window.localStorage.setItem(THEME_STORAGE, normalizedThemeId);
|
|
656
|
+
} catch (e) {
|
|
657
|
+
// ignore storage failures in private mode / restrictions
|
|
658
|
+
}
|
|
659
|
+
saveRecentThemeId(normalizedThemeId);
|
|
660
|
+
syncThemePickerUi(normalizedThemeId);
|
|
661
|
+
closeThemePicker();
|
|
662
|
+
|
|
663
|
+
if (terminalInstance) {
|
|
664
|
+
terminalInstance.options.theme = { ...theme.terminal };
|
|
665
|
+
if (typeof terminalInstance.refresh === 'function') {
|
|
666
|
+
terminalInstance.refresh(0, Math.max(0, terminalInstance.rows - 1));
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function bootAppTheme() {
|
|
672
|
+
let savedTheme = FALLBACK_THEME;
|
|
673
|
+
try {
|
|
674
|
+
savedTheme = window.localStorage.getItem(THEME_STORAGE) || FALLBACK_THEME;
|
|
675
|
+
} catch (e) {
|
|
676
|
+
// localStorage unavailable
|
|
677
|
+
}
|
|
678
|
+
applyAppTheme(savedTheme);
|
|
679
|
+
syncThemePickerUi(savedTheme);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function getActiveTheme() {
|
|
683
|
+
let themeId = FALLBACK_THEME;
|
|
684
|
+
try {
|
|
685
|
+
themeId = window.localStorage.getItem(THEME_STORAGE) || FALLBACK_THEME;
|
|
686
|
+
} catch (e) {
|
|
687
|
+
// localStorage unavailable
|
|
688
|
+
}
|
|
689
|
+
const normalizedThemeId = isAppThemeId(themeId) ? themeId : FALLBACK_THEME;
|
|
690
|
+
return APP_THEMES[normalizedThemeId] || APP_THEMES[FALLBACK_THEME];
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// fetchSvcStatus removed — replaced by pollHealth() + unified #app-status badge
|
|
694
|
+
|
|
695
|
+
function setTerminalStatus(text, tone) {
|
|
696
|
+
const el = document.getElementById("terminal-status");
|
|
697
|
+
if (!el) return;
|
|
698
|
+
el.textContent = text;
|
|
699
|
+
el.style.color =
|
|
700
|
+
tone === "ok" ? "var(--green)"
|
|
701
|
+
: tone === "warn" ? "var(--amber)"
|
|
702
|
+
: tone === "err" ? "var(--red)"
|
|
703
|
+
: "var(--dim)";
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function terminalSocketUrl() {
|
|
707
|
+
const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
708
|
+
const url = new URL(proto + "//" + window.location.host + "/api/terminal");
|
|
709
|
+
const remembered = window.sessionStorage.getItem(TERMINAL_SESSION_STORAGE_KEY);
|
|
710
|
+
if (terminalSessionId) {
|
|
711
|
+
url.searchParams.set("sessionId", terminalSessionId);
|
|
712
|
+
} else if (remembered) {
|
|
713
|
+
url.searchParams.set("sessionId", remembered);
|
|
714
|
+
}
|
|
715
|
+
if (terminalInstance) {
|
|
716
|
+
url.searchParams.set("cols", String(terminalInstance.cols || 120));
|
|
717
|
+
url.searchParams.set("rows", String(terminalInstance.rows || 30));
|
|
718
|
+
}
|
|
719
|
+
return url.toString();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function scheduleTerminalReconnect() {
|
|
723
|
+
if (terminalShuttingDown || terminalReconnectTimer) return;
|
|
724
|
+
terminalReconnectTimer = window.setTimeout(function() {
|
|
725
|
+
terminalReconnectTimer = null;
|
|
726
|
+
connectTerminalSocket();
|
|
727
|
+
}, 1500);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function handleTerminalMessage(message) {
|
|
731
|
+
if (!terminalInstance || !message || typeof message.type !== "string") return;
|
|
732
|
+
|
|
733
|
+
if (message.type === "terminal:connected") {
|
|
734
|
+
terminalSessionId = message.sessionId || null;
|
|
735
|
+
if (terminalSessionId) {
|
|
736
|
+
window.sessionStorage.setItem(TERMINAL_SESSION_STORAGE_KEY, terminalSessionId);
|
|
737
|
+
}
|
|
738
|
+
setTerminalStatus("CONNECTED", "ok");
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (message.type === "terminal:output") {
|
|
743
|
+
if (typeof message.data === "string") {
|
|
744
|
+
terminalInstance.write(message.data);
|
|
745
|
+
}
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (message.type === "terminal:resized") {
|
|
750
|
+
setTerminalStatus((message.cols || terminalInstance.cols) + "x" + (message.rows || terminalInstance.rows), "dim");
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (message.type === "terminal:exit") {
|
|
755
|
+
terminalSessionId = null;
|
|
756
|
+
window.sessionStorage.removeItem(TERMINAL_SESSION_STORAGE_KEY);
|
|
757
|
+
terminalInstance.writeln("\\r\\n\\x1b[33m[terminal exited]\\x1b[0m");
|
|
758
|
+
setTerminalStatus("EXITED", "warn");
|
|
759
|
+
scheduleTerminalReconnect();
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (message.type === "terminal:error") {
|
|
764
|
+
terminalInstance.writeln("\\r\\n\\x1b[31m[terminal error] " + (message.error || "unknown") + "\\x1b[0m");
|
|
765
|
+
setTerminalStatus("ERROR", "err");
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function connectTerminalSocket() {
|
|
770
|
+
if (!terminalInstance) return;
|
|
771
|
+
if (terminalSocket && (terminalSocket.readyState === WebSocket.OPEN || terminalSocket.readyState === WebSocket.CONNECTING)) {
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const attemptedSessionId = terminalSessionId || window.sessionStorage.getItem(TERMINAL_SESSION_STORAGE_KEY);
|
|
776
|
+
let opened = false;
|
|
777
|
+
setTerminalStatus("CONNECTING", "warn");
|
|
778
|
+
const socket = new WebSocket(terminalSocketUrl());
|
|
779
|
+
terminalSocket = socket;
|
|
780
|
+
|
|
781
|
+
socket.addEventListener("message", function(event) {
|
|
782
|
+
try {
|
|
783
|
+
const message = JSON.parse(typeof event.data === "string" ? event.data : "");
|
|
784
|
+
handleTerminalMessage(message);
|
|
785
|
+
} catch {
|
|
786
|
+
// ignore malformed bridge messages
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
socket.addEventListener("open", function() {
|
|
791
|
+
opened = true;
|
|
792
|
+
setTerminalStatus("HANDSHAKE", "dim");
|
|
793
|
+
if (terminalFitAddon) {
|
|
794
|
+
try { terminalFitAddon.fit(); } catch {}
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
socket.addEventListener("close", function() {
|
|
799
|
+
terminalSocket = null;
|
|
800
|
+
if (!terminalShuttingDown) {
|
|
801
|
+
if (!opened && attemptedSessionId) {
|
|
802
|
+
terminalSessionId = null;
|
|
803
|
+
window.sessionStorage.removeItem(TERMINAL_SESSION_STORAGE_KEY);
|
|
804
|
+
}
|
|
805
|
+
setTerminalStatus("RECONNECTING", "warn");
|
|
806
|
+
scheduleTerminalReconnect();
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
socket.addEventListener("error", function() {
|
|
811
|
+
setTerminalStatus("ERROR", "err");
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function initializeTerminal() {
|
|
816
|
+
const host = document.getElementById("terminal-host");
|
|
817
|
+
if (!host) return;
|
|
818
|
+
|
|
819
|
+
if (!window.Terminal || !window.FitAddon) {
|
|
820
|
+
host.innerHTML = '<div class="empty">xterm failed to load</div>';
|
|
821
|
+
setTerminalStatus("UNAVAILABLE", "err");
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const activeTheme = getActiveTheme();
|
|
826
|
+
const terminal = new window.Terminal({
|
|
827
|
+
cursorBlink: true,
|
|
828
|
+
cursorStyle: "bar",
|
|
829
|
+
fontFamily: "JetBrains Mono, Menlo, monospace",
|
|
830
|
+
fontSize: 12,
|
|
831
|
+
lineHeight: 1.25,
|
|
832
|
+
scrollback: 5000,
|
|
833
|
+
theme: { ...activeTheme.terminal }
|
|
834
|
+
});
|
|
835
|
+
const fitAddon = new window.FitAddon.FitAddon();
|
|
836
|
+
terminal.loadAddon(fitAddon);
|
|
837
|
+
if (window.WebLinksAddon && window.WebLinksAddon.WebLinksAddon) {
|
|
838
|
+
terminal.loadAddon(new window.WebLinksAddon.WebLinksAddon());
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
terminal.open(host);
|
|
842
|
+
terminalInstance = terminal;
|
|
843
|
+
terminalFitAddon = fitAddon;
|
|
844
|
+
|
|
845
|
+
try { fitAddon.fit(); } catch {}
|
|
846
|
+
host.parentElement.classList.add("ready");
|
|
847
|
+
terminal.focus();
|
|
848
|
+
setTerminalStatus("BOOTING", "dim");
|
|
849
|
+
|
|
850
|
+
terminal.onData(function(data) {
|
|
851
|
+
if (!terminalSocket || terminalSocket.readyState !== WebSocket.OPEN) return;
|
|
852
|
+
terminalSocket.send(JSON.stringify({ type: "terminal:input", data: data }));
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
terminal.onResize(function(size) {
|
|
856
|
+
if (!terminalSocket || terminalSocket.readyState !== WebSocket.OPEN) return;
|
|
857
|
+
terminalSocket.send(JSON.stringify({ type: "terminal:resize", cols: size.cols, rows: size.rows }));
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
host.addEventListener("click", function() {
|
|
861
|
+
terminal.focus();
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
window.addEventListener("resize", function() {
|
|
865
|
+
if (!terminalFitAddon || !terminalInstance) return;
|
|
866
|
+
try { terminalFitAddon.fit(); } catch {}
|
|
867
|
+
if (terminalSocket && terminalSocket.readyState === WebSocket.OPEN) {
|
|
868
|
+
terminalSocket.send(JSON.stringify({
|
|
869
|
+
type: "terminal:resize",
|
|
870
|
+
cols: terminalInstance.cols,
|
|
871
|
+
rows: terminalInstance.rows
|
|
872
|
+
}));
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
window.addEventListener("beforeunload", function() {
|
|
877
|
+
terminalShuttingDown = true;
|
|
878
|
+
if (terminalSocket && terminalSocket.readyState === WebSocket.OPEN) {
|
|
879
|
+
terminalSocket.close();
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
connectTerminalSocket();
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// svcAction and triggerHeartbeat removed — STOP/RESTART/HEARTBEAT buttons
|
|
887
|
+
// removed from unified-process model; use triggerSync() for a manual refresh.
|
|
888
|
+
|
|
889
|
+
// ---- Sync trigger ----
|
|
890
|
+
async function triggerSync() {
|
|
891
|
+
const btn = document.getElementById("sync-btn");
|
|
892
|
+
const origLabel = "⟳ SYNC";
|
|
893
|
+
if (btn) { btn.disabled = true; btn.textContent = "⟳ SYNCING"; }
|
|
894
|
+
try {
|
|
895
|
+
const res = await fetch("/api/workitems/sync", { method: "POST" });
|
|
896
|
+
const data = await res.json().catch(() => ({}));
|
|
897
|
+
if (data && data.ok) {
|
|
898
|
+
lastSyncAt = data.syncedAt || Date.now();
|
|
899
|
+
renderSyncAgo();
|
|
900
|
+
loadWorkItems();
|
|
901
|
+
updateBoardWorkItemSummary();
|
|
902
|
+
}
|
|
903
|
+
} catch(e) {
|
|
904
|
+
console.error("Sync failed:", e);
|
|
905
|
+
} finally {
|
|
906
|
+
if (btn) { btn.disabled = false; btn.textContent = origLabel; }
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function renderSyncAgo() {
|
|
911
|
+
const el = document.getElementById("sync-ago");
|
|
912
|
+
if (!el) return;
|
|
913
|
+
if (lastSyncAt === null) { el.textContent = ""; return; }
|
|
914
|
+
const secs = Math.round((Date.now() - lastSyncAt) / 1000);
|
|
915
|
+
if (secs > 90) {
|
|
916
|
+
el.textContent = "synced " + Math.round(secs / 60) + "m ago";
|
|
917
|
+
} else {
|
|
918
|
+
el.textContent = "synced " + secs + "s ago";
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// ---- App status badge (single badge combining SSE + health) ----
|
|
923
|
+
var _sseOpen = false;
|
|
924
|
+
var _healthOk = false;
|
|
925
|
+
var _healthChecked = false; // honest initial state: don't claim LIVE before first poll
|
|
926
|
+
|
|
927
|
+
function setAppStatus(state) {
|
|
928
|
+
var el = document.getElementById("app-status");
|
|
929
|
+
if (!el) return;
|
|
930
|
+
if (state === "live") {
|
|
931
|
+
el.textContent = "LIVE";
|
|
932
|
+
el.className = "live";
|
|
933
|
+
el.title = "LIVE — event stream open and backend healthy (DB reachable, sync ok)";
|
|
934
|
+
} else if (state === "connecting") {
|
|
935
|
+
el.textContent = "CONNECTING";
|
|
936
|
+
el.className = "reconnecting";
|
|
937
|
+
el.title = "CONNECTING — establishing event stream and checking backend health";
|
|
938
|
+
} else if (state === "reconnecting") {
|
|
939
|
+
el.textContent = "RECONNECTING";
|
|
940
|
+
el.className = "reconnecting";
|
|
941
|
+
el.title = "RECONNECTING — event stream lost; retrying automatically every 3s";
|
|
942
|
+
} else {
|
|
943
|
+
el.textContent = "DEGRADED";
|
|
944
|
+
el.className = "degraded";
|
|
945
|
+
el.title = "DEGRADED — app is running but the backend store (DB/sync) is unhealthy; retrying automatically";
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function refreshAppStatus() {
|
|
950
|
+
if (!_sseOpen) {
|
|
951
|
+
setAppStatus("reconnecting");
|
|
952
|
+
} else if (!_healthChecked) {
|
|
953
|
+
// SSE open but first health poll hasn't completed — don't lie LIVE yet
|
|
954
|
+
setAppStatus("connecting");
|
|
955
|
+
} else if (!_healthOk) {
|
|
956
|
+
setAppStatus("degraded");
|
|
957
|
+
} else {
|
|
958
|
+
setAppStatus("live");
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
async function pollHealth() {
|
|
963
|
+
try {
|
|
964
|
+
var res = await fetch("/api/health");
|
|
965
|
+
var data = await res.json();
|
|
966
|
+
// Backend is healthy only when DB is reachable AND sync is not degraded.
|
|
967
|
+
_healthOk = !!(data && data.ok === true && data.sync !== "degraded");
|
|
968
|
+
} catch {
|
|
969
|
+
_healthOk = false;
|
|
970
|
+
}
|
|
971
|
+
_healthChecked = true;
|
|
972
|
+
refreshAppStatus();
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// ---- SSE (board) ----
|
|
976
|
+
function connectBoardSSE(skipReplay) {
|
|
977
|
+
const url = skipReplay ? "/api/events?skipReplay=1" : "/api/events";
|
|
978
|
+
const es = new EventSource(url);
|
|
979
|
+
es.onopen = function() {
|
|
980
|
+
_sseOpen = true;
|
|
981
|
+
refreshAppStatus();
|
|
982
|
+
// Kick an immediate health check so the badge can't momentarily lie LIVE.
|
|
983
|
+
pollHealth();
|
|
984
|
+
};
|
|
985
|
+
es.onmessage = function(e) {
|
|
986
|
+
let ev; try { ev = JSON.parse(e.data); } catch { return; }
|
|
987
|
+
handleBoardEvent(ev);
|
|
988
|
+
};
|
|
989
|
+
es.onerror = function() {
|
|
990
|
+
_sseOpen = false;
|
|
991
|
+
refreshAppStatus();
|
|
992
|
+
es.close();
|
|
993
|
+
window.setTimeout(function() { connectBoardSSE(false); }, 3000);
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function handleBoardEvent(ev) {
|
|
998
|
+
if (ev.type === "sync:complete") {
|
|
999
|
+
lastSyncAt = ev.data?.syncedAt || Date.now();
|
|
1000
|
+
renderSyncAgo();
|
|
1001
|
+
loadWorkItems();
|
|
1002
|
+
} else if (ev.type === "heartbeat") {
|
|
1003
|
+
// Heartbeat event received — re-check health proactively (mirrors dashboard)
|
|
1004
|
+
pollHealth();
|
|
1005
|
+
} else if (ev.type === "stats") {
|
|
1006
|
+
// Live stats update — sync execution pause state
|
|
1007
|
+
if (ev.data && ev.data.executionPaused != null) {
|
|
1008
|
+
setExecutionState(!!ev.data.executionPaused, ev.data.runningWorkItems ?? 0);
|
|
1009
|
+
}
|
|
1010
|
+
const turboBtn = document.getElementById("turbo-btn");
|
|
1011
|
+
if (turboBtn && ev.data) {
|
|
1012
|
+
turboBtn.classList.toggle("turbo-active", !!ev.data.turboMode);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
async function toggleTurbo() {
|
|
1018
|
+
const btn = document.getElementById("turbo-btn");
|
|
1019
|
+
if (!btn) return;
|
|
1020
|
+
const isActive = btn.classList.contains("turbo-active");
|
|
1021
|
+
const newState = !isActive;
|
|
1022
|
+
if (newState && !confirm("Enable TURBO mode? (5min heartbeat, 20 daily cap, auto-expires in 4h)")) return;
|
|
1023
|
+
btn.disabled = true;
|
|
1024
|
+
try {
|
|
1025
|
+
const res = await fetch("/api/turbo", {
|
|
1026
|
+
method: "POST",
|
|
1027
|
+
headers: { "Content-Type": "application/json" },
|
|
1028
|
+
body: JSON.stringify({ enabled: newState }),
|
|
1029
|
+
});
|
|
1030
|
+
if (!res.ok) throw new Error("Failed");
|
|
1031
|
+
btn.classList.toggle("turbo-active", newState);
|
|
1032
|
+
} catch (e) {
|
|
1033
|
+
console.error("Turbo toggle failed:", e);
|
|
1034
|
+
}
|
|
1035
|
+
btn.disabled = false;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// ---- Execution pause control ----
|
|
1039
|
+
function setExecutionState(paused, runningCount) {
|
|
1040
|
+
const btn = document.getElementById("exec-toggle");
|
|
1041
|
+
const lbl = document.getElementById("exec-state");
|
|
1042
|
+
if (!btn || !lbl) return;
|
|
1043
|
+
if (paused) {
|
|
1044
|
+
btn.textContent = "▶ Resume";
|
|
1045
|
+
lbl.textContent = runningCount > 0 ? "⏸ Paused · " + runningCount + " draining" : "⏸ Paused";
|
|
1046
|
+
} else {
|
|
1047
|
+
btn.textContent = "⏸ Pause";
|
|
1048
|
+
lbl.textContent = "▶ Running";
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
async function toggleExecution() {
|
|
1053
|
+
const btn = document.getElementById("exec-toggle");
|
|
1054
|
+
const isPaused = btn && btn.textContent.trim().startsWith("▶");
|
|
1055
|
+
if (btn) btn.disabled = true;
|
|
1056
|
+
try {
|
|
1057
|
+
const endpoint = isPaused ? "/api/execution/resume" : "/api/execution/pause";
|
|
1058
|
+
const res = await fetch(endpoint, { method: "POST" });
|
|
1059
|
+
if (!res.ok) throw new Error("Execution toggle failed: " + res.status);
|
|
1060
|
+
const status = await fetch("/api/execution/status").then(r => r.json());
|
|
1061
|
+
setExecutionState(!!status.paused, status.inFlightCount ?? 0);
|
|
1062
|
+
} catch (e) {
|
|
1063
|
+
console.error("Execution toggle failed:", e);
|
|
1064
|
+
} finally {
|
|
1065
|
+
if (btn) btn.disabled = false;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
async function fetchExecStats() {
|
|
1070
|
+
try {
|
|
1071
|
+
const data = await fetch("/api/stats").then(r => r.json());
|
|
1072
|
+
if (data.executionPaused != null) {
|
|
1073
|
+
setExecutionState(!!data.executionPaused, data.runningWorkItems ?? 0);
|
|
1074
|
+
}
|
|
1075
|
+
// Sync turbo button state from stats
|
|
1076
|
+
const turboBtn = document.getElementById("turbo-btn");
|
|
1077
|
+
if (turboBtn) {
|
|
1078
|
+
turboBtn.classList.toggle("turbo-active", !!data.turboMode);
|
|
1079
|
+
}
|
|
1080
|
+
} catch {}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function normalizeLaneRecord(raw) {
|
|
1084
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1085
|
+
const wi = raw.workItem || {};
|
|
1086
|
+
const location = raw.repo || raw.repository || raw.repoPath || raw.worktree || raw.projectPath || wi.repo || wi.repository || wi.repositoryPath || "";
|
|
1087
|
+
const branch = raw.branch || raw.gitBranch || raw.worktreeBranch || wi.branch || wi.ref || "";
|
|
1088
|
+
const sessionId = raw.sessionId || (raw.session && raw.session.id) || raw.terminalSessionId || raw.ptySessionId;
|
|
1089
|
+
const pid = raw.pid || (raw.process && raw.process.pid) || raw.processId || "";
|
|
1090
|
+
const executionStatus = raw.executionStatus || raw.execution?.status || raw.status || raw.state || "idle";
|
|
1091
|
+
const assignmentStatus = raw.assignmentStatus || raw.assignment?.status || raw.assignmentState || raw.state || "unknown";
|
|
1092
|
+
const operatorLane = raw.operatorLane || raw.lane || raw.assignmentLane || "unassigned";
|
|
1093
|
+
const workItemId = raw.workItemId || wi.id || wi.identifier || wi.azdoId || "";
|
|
1094
|
+
const workItemTitle = raw.workItemTitle || raw.title || wi.title || "";
|
|
1095
|
+
const dispatcher = raw.dispatcher || raw.assignment?.dispatcher || "";
|
|
1096
|
+
const paseoAgentId = raw.paseoAgentId || raw.assignment?.paseoAgentId || "";
|
|
1097
|
+
const paseoStatus = raw.paseoStatus || raw.assignment?.paseoStatus || "";
|
|
1098
|
+
return {
|
|
1099
|
+
operatorLane,
|
|
1100
|
+
workItemId,
|
|
1101
|
+
workItemTitle,
|
|
1102
|
+
host: raw.host || raw.hostname || "",
|
|
1103
|
+
location: raw.paseoCwd || location,
|
|
1104
|
+
branch: raw.paseoWorktree || branch,
|
|
1105
|
+
sessionId,
|
|
1106
|
+
pid,
|
|
1107
|
+
executionStatus,
|
|
1108
|
+
assignmentStatus,
|
|
1109
|
+
dispatcher,
|
|
1110
|
+
paseoAgentId,
|
|
1111
|
+
paseoProvider: raw.paseoProvider || raw.assignment?.paseoProvider || "",
|
|
1112
|
+
paseoMode: raw.paseoMode || raw.assignment?.paseoMode || "",
|
|
1113
|
+
paseoStatus,
|
|
1114
|
+
paseoLastCheckedAt: raw.paseoLastCheckedAt || raw.assignment?.paseoLastCheckedAt || "",
|
|
1115
|
+
paseoSummary: raw.paseoSummary || raw.assignment?.paseoSummary || "",
|
|
1116
|
+
isTerminalSession:
|
|
1117
|
+
String(raw.sessionType || raw.kind || raw.type || raw.assignmentType || raw.connectorType || raw.connectionType || "").toLowerCase() === "terminal" ||
|
|
1118
|
+
/terminal/.test(String(raw.workItemTitle || raw.title || "").toLowerCase()) ||
|
|
1119
|
+
String(raw.isTerminal || "").toLowerCase() === "true",
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function normalizeTerminalSessionRecord(raw) {
|
|
1124
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1125
|
+
return {
|
|
1126
|
+
operatorLane: "terminal",
|
|
1127
|
+
workItemId: "",
|
|
1128
|
+
workItemTitle: raw.title || "embedded operator terminal",
|
|
1129
|
+
host: "",
|
|
1130
|
+
location: raw.cwd || "",
|
|
1131
|
+
branch: "",
|
|
1132
|
+
sessionId: raw.sessionId || "",
|
|
1133
|
+
pid: raw.pid || "",
|
|
1134
|
+
executionStatus: raw.exited ? "exited" : "connected",
|
|
1135
|
+
assignmentStatus: raw.exited ? "exited" : "connected",
|
|
1136
|
+
isTerminalSession: true,
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function renderLaneSummary(payload) {
|
|
1141
|
+
const list = document.getElementById("lane-list");
|
|
1142
|
+
const summary = document.getElementById("lane-summary");
|
|
1143
|
+
const lanes = Array.isArray(payload?.lanes) ? payload.lanes : [];
|
|
1144
|
+
const terminalSessions = Array.isArray(payload?.terminalSessions) ? payload.terminalSessions : [];
|
|
1145
|
+
const totalRows = lanes.length + terminalSessions.length;
|
|
1146
|
+
|
|
1147
|
+
if (summary) {
|
|
1148
|
+
if (payload?.summary && typeof payload.summary === "object") {
|
|
1149
|
+
const laneCount = payload.summary.totalLanes ?? lanes.length;
|
|
1150
|
+
const terminalCount = payload.summary.activeTerminalSessions ?? terminalSessions.length;
|
|
1151
|
+
summary.textContent = String(laneCount) + "L/" + String(terminalCount) + "T";
|
|
1152
|
+
} else {
|
|
1153
|
+
summary.textContent = String(totalRows) + " active";
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
if (!totalRows) {
|
|
1158
|
+
if (list) {
|
|
1159
|
+
list.innerHTML = '<div class="empty">no active operator lanes or terminal sessions</div>';
|
|
1160
|
+
}
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
if (!list) return;
|
|
1165
|
+
const combinedRows = lanes.concat(
|
|
1166
|
+
terminalSessions
|
|
1167
|
+
.map(normalizeTerminalSessionRecord)
|
|
1168
|
+
.filter(Boolean),
|
|
1169
|
+
);
|
|
1170
|
+
|
|
1171
|
+
const lines = combinedRows.map(function(l) {
|
|
1172
|
+
const workItem = l.workItemId && l.workItemTitle
|
|
1173
|
+
? escHtml(l.workItemId) + " · " + escHtml(l.workItemTitle)
|
|
1174
|
+
: l.workItemId
|
|
1175
|
+
? escHtml(l.workItemId)
|
|
1176
|
+
: l.workItemTitle
|
|
1177
|
+
? escHtml(l.workItemTitle)
|
|
1178
|
+
: "unassigned";
|
|
1179
|
+
const loc = [l.host ? "host:" + l.host : "", l.location ? "repo:" + l.location : "", l.branch ? "branch:" + l.branch : ""].filter(Boolean).join(" · ");
|
|
1180
|
+
const statusBits = [
|
|
1181
|
+
l.executionStatus ? "exec:" + l.executionStatus : "",
|
|
1182
|
+
l.assignmentStatus ? "assign:" + l.assignmentStatus : "",
|
|
1183
|
+
l.dispatcher ? "dispatch:" + l.dispatcher : "",
|
|
1184
|
+
l.paseoStatus ? "paseo:" + l.paseoStatus : "",
|
|
1185
|
+
].filter(Boolean).join(" · ");
|
|
1186
|
+
const sessionBits = [
|
|
1187
|
+
l.paseoAgentId ? "agent:" + l.paseoAgentId : "",
|
|
1188
|
+
l.paseoProvider ? "provider:" + l.paseoProvider : "",
|
|
1189
|
+
l.paseoMode ? "mode:" + l.paseoMode : "",
|
|
1190
|
+
l.sessionId ? "session:" + l.sessionId : "",
|
|
1191
|
+
l.pid ? "pid:" + l.pid : "",
|
|
1192
|
+
].filter(Boolean).join(" · ");
|
|
1193
|
+
const laneType = l.isTerminalSession ? "operator/terminal session" : "operator session";
|
|
1194
|
+
|
|
1195
|
+
return (
|
|
1196
|
+
'<div class="lane-row">' +
|
|
1197
|
+
'<div><span style="color:var(--green);font-size:10px;letter-spacing:0.08em;text-transform:uppercase;">' + escHtml(l.operatorLane) + '</span>' +
|
|
1198
|
+
' <span class="lane-pill">' + laneType + '</span></div>' +
|
|
1199
|
+
'<div style="color:var(--white);" title="' + escHtml(workItem) + '">' + workItem + '</div>' +
|
|
1200
|
+
(loc ? '<div class="lane-row-kv">' + loc + '</div>' : '') +
|
|
1201
|
+
(statusBits || sessionBits ? '<div class="lane-row-kv">' + (statusBits ? statusBits : '') + (statusBits && sessionBits ? ' | ' : '') + sessionBits + '</div>' : '') +
|
|
1202
|
+
'</div>'
|
|
1203
|
+
);
|
|
1204
|
+
}).join("");
|
|
1205
|
+
|
|
1206
|
+
list.innerHTML = lines;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
async function fetchLanes() {
|
|
1210
|
+
const list = document.getElementById("lane-list");
|
|
1211
|
+
const summary = document.getElementById("lane-summary");
|
|
1212
|
+
if (summary) summary.textContent = "...";
|
|
1213
|
+
if (list) list.innerHTML = '<div class="empty">loading lane truth...</div>';
|
|
1214
|
+
|
|
1215
|
+
try {
|
|
1216
|
+
const res = await fetch("/api/lanes");
|
|
1217
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
1218
|
+
const payload = await res.json();
|
|
1219
|
+
const lanePayload = Array.isArray(payload)
|
|
1220
|
+
? { lanes: payload, terminalSessions: [], summary: null }
|
|
1221
|
+
: payload;
|
|
1222
|
+
const rows = Array.isArray(lanePayload?.lanes) ? lanePayload.lanes : [];
|
|
1223
|
+
const normalizedLanes = [];
|
|
1224
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1225
|
+
const n = normalizeLaneRecord(rows[i]);
|
|
1226
|
+
if (n) normalizedLanes.push(n);
|
|
1227
|
+
}
|
|
1228
|
+
laneRows = normalizedLanes;
|
|
1229
|
+
renderLaneSummary({
|
|
1230
|
+
lanes: normalizedLanes,
|
|
1231
|
+
terminalSessions: Array.isArray(lanePayload?.terminalSessions) ? lanePayload.terminalSessions : [],
|
|
1232
|
+
summary: lanePayload?.summary || null,
|
|
1233
|
+
});
|
|
1234
|
+
} catch (err) {
|
|
1235
|
+
if (summary) summary.textContent = "error";
|
|
1236
|
+
if (list) list.innerHTML = '<div class="empty">failed to load operator lanes: ' + escHtml(err.message || "unknown") + '</div>';
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
async function fetchStuckWorkItems() {
|
|
1241
|
+
try {
|
|
1242
|
+
const res = await fetch("/api/workitems/stuck");
|
|
1243
|
+
const data = await res.json();
|
|
1244
|
+
const list = document.getElementById("stuck-list");
|
|
1245
|
+
const badge = document.getElementById("stuck-badge");
|
|
1246
|
+
|
|
1247
|
+
const totalStuck = data.stuckWorkItems?.length ?? 0;
|
|
1248
|
+
const totalFailed = data.failedWorkItems?.length ?? 0;
|
|
1249
|
+
const total = totalStuck + totalFailed;
|
|
1250
|
+
|
|
1251
|
+
badge.textContent = total;
|
|
1252
|
+
badge.style.color = total > 0 ? "var(--red)" : "var(--dim)";
|
|
1253
|
+
|
|
1254
|
+
if (total === 0) {
|
|
1255
|
+
list.innerHTML = '<div style="color: var(--green-mid); padding: 4px;">✓ All work items healthy</div>';
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
let html = '';
|
|
1260
|
+
|
|
1261
|
+
if (totalStuck > 0) {
|
|
1262
|
+
html += '<div style="color: var(--amber); font-weight: bold; margin-bottom: 4px; font-size: 9px; text-transform: uppercase;">⏱ Stuck (' + totalStuck + ')</div>';
|
|
1263
|
+
data.stuckWorkItems.forEach(workItem => {
|
|
1264
|
+
const priorityColor = workItem.priority === 1 ? 'var(--red)' : workItem.priority === 2 ? 'var(--amber)' : 'var(--dim)';
|
|
1265
|
+
html += '<div style="margin-bottom: 6px; padding: 4px; border-left: 2px solid ' + priorityColor + '; background: var(--warnBg);">';
|
|
1266
|
+
html += '<div style="color: var(--white); font-size: 10px; margin-bottom: 2px;">' + workItem.identifier + ': ' + workItem.title.substring(0, 40) + (workItem.title.length > 40 ? '...' : '') + '</div>';
|
|
1267
|
+
html += '<div style="color: var(--amber); font-size: 9px;">' + workItem.reason + '</div>';
|
|
1268
|
+
if (workItem.project?.name) {
|
|
1269
|
+
html += '<div style="color: var(--dim); font-size: 9px; margin-top: 2px;">📁 ' + workItem.project.name + '</div>';
|
|
1270
|
+
}
|
|
1271
|
+
html += '</div>';
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if (totalFailed > 0) {
|
|
1276
|
+
if (totalStuck > 0) html += '<div style="height: 8px;"></div>';
|
|
1277
|
+
html += '<div style="color: var(--red); font-weight: bold; margin-bottom: 4px; font-size: 9px; text-transform: uppercase;">✖ Failed (' + totalFailed + ')</div>';
|
|
1278
|
+
data.failedWorkItems.forEach(workItem => {
|
|
1279
|
+
const priorityColor = workItem.priority === 1 ? 'var(--red)' : workItem.priority === 2 ? 'var(--amber)' : 'var(--dim)';
|
|
1280
|
+
html += '<div style="margin-bottom: 6px; padding: 4px; border-left: 2px solid ' + priorityColor + '; background: var(--dangerBg);">';
|
|
1281
|
+
html += '<div style="color: var(--white); font-size: 10px; margin-bottom: 2px;">' + workItem.identifier + ': ' + workItem.title.substring(0, 40) + (workItem.title.length > 40 ? '...' : '') + '</div>';
|
|
1282
|
+
html += '<div style="color: var(--red); font-size: 9px;">' + workItem.reason + '</div>';
|
|
1283
|
+
if (workItem.project?.name) {
|
|
1284
|
+
html += '<div style="color: var(--dim); font-size: 9px; margin-top: 2px;">📁 ' + workItem.project.name + '</div>';
|
|
1285
|
+
}
|
|
1286
|
+
html += '</div>';
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
list.innerHTML = html;
|
|
1291
|
+
} catch (err) {
|
|
1292
|
+
console.error("Failed to fetch stuck work items:", err);
|
|
1293
|
+
document.getElementById("stuck-list").innerHTML = '<div style="color: var(--red);">Error loading</div>';
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
let stuckSidecarOpen = false;
|
|
1298
|
+
function toggleStuckSidecar() {
|
|
1299
|
+
stuckSidecarOpen = !stuckSidecarOpen;
|
|
1300
|
+
const body = document.getElementById("stuck-sidecar-body");
|
|
1301
|
+
const arrow = document.getElementById("stuck-sidecar-arrow");
|
|
1302
|
+
if (body) body.className = "sidecar-body " + (stuckSidecarOpen ? "expanded" : "collapsed");
|
|
1303
|
+
if (arrow) arrow.textContent = stuckSidecarOpen ? "▼" : "▶";
|
|
1304
|
+
if (stuckSidecarOpen) fetchStuckWorkItems();
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function updateBoardWorkItemSummary() {
|
|
1308
|
+
const summary = document.getElementById("work-item-summary");
|
|
1309
|
+
const visible = getVisibleWorkItems();
|
|
1310
|
+
const total = allWorkItems.length;
|
|
1311
|
+
const openCount = visible.filter((item) => !['Done', 'Removed'].includes(item.status)).length;
|
|
1312
|
+
const doneCount = visible.filter((item) => item.status === 'Done').length;
|
|
1313
|
+
const removedCount = visible.filter((item) => item.status === 'Removed').length;
|
|
1314
|
+
const areaCount = new Set(visible.map((item) => item.area).filter(Boolean)).size;
|
|
1315
|
+
if (summary) {
|
|
1316
|
+
summary.textContent = visible.length + " items · " + openCount + " open · " + doneCount + " done";
|
|
1317
|
+
}
|
|
1318
|
+
const statTotal = document.getElementById("stat-total");
|
|
1319
|
+
const statOpen = document.getElementById("stat-open");
|
|
1320
|
+
const statDone = document.getElementById("stat-done");
|
|
1321
|
+
const statRemoved = document.getElementById("stat-removed");
|
|
1322
|
+
const statAreas = document.getElementById("stat-areas");
|
|
1323
|
+
const statVisible = document.getElementById("stat-visible");
|
|
1324
|
+
if (statTotal) statTotal.textContent = String(total);
|
|
1325
|
+
if (statOpen) statOpen.textContent = String(openCount);
|
|
1326
|
+
if (statDone) statDone.textContent = String(doneCount);
|
|
1327
|
+
if (statRemoved) statRemoved.textContent = String(removedCount);
|
|
1328
|
+
if (statAreas) statAreas.textContent = String(areaCount);
|
|
1329
|
+
if (statVisible) statVisible.textContent = String(visible.length);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function onRepoScopeChange(repo) {
|
|
1333
|
+
currentRepoScope = repo;
|
|
1334
|
+
renderBoard();
|
|
1335
|
+
updateBoardWorkItemSummary();
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// ---- Shared REPO scope dropdown population (mirrors corrected dashboard) ----
|
|
1339
|
+
// The shared "REPO [All v]" selector markup lives in sharedShell.ts; here we
|
|
1340
|
+
// populate it from the board's own project fetch so it stays in sync.
|
|
1341
|
+
// Options are keyed by project PATH (canonical, unique) so two repos sharing a
|
|
1342
|
+
// directory basename don't collide; the visible label uses name || basename.
|
|
1343
|
+
let boardProjects = [];
|
|
1344
|
+
function syncRepoScopeDropdown() {
|
|
1345
|
+
const sel = document.getElementById("repo-scope-select");
|
|
1346
|
+
if (!sel) return;
|
|
1347
|
+
// Preserve the user's current selection so a refresh doesn't reset to "All".
|
|
1348
|
+
const previousValue = sel.value;
|
|
1349
|
+
// Canonical set of present project paths (keyed identity).
|
|
1350
|
+
const presentPaths = new Set(boardProjects.map(function(p) { return p.path; }).filter(Boolean));
|
|
1351
|
+
// Existing non-All option values (paths) already in the dropdown.
|
|
1352
|
+
const existing = new Set(
|
|
1353
|
+
Array.from(sel.querySelectorAll("option"))
|
|
1354
|
+
.map(function(o) { return o.value; })
|
|
1355
|
+
.filter(function(v) { return v && v !== "All"; })
|
|
1356
|
+
);
|
|
1357
|
+
// Add any project not yet in the dropdown, keyed by path.
|
|
1358
|
+
for (const p of boardProjects) {
|
|
1359
|
+
const path = p.path;
|
|
1360
|
+
if (!path || existing.has(path)) continue;
|
|
1361
|
+
const label = p.name || path.split("/").pop() || path;
|
|
1362
|
+
const opt = document.createElement("option");
|
|
1363
|
+
opt.value = path;
|
|
1364
|
+
opt.textContent = label;
|
|
1365
|
+
sel.appendChild(opt);
|
|
1366
|
+
existing.add(path);
|
|
1367
|
+
}
|
|
1368
|
+
// Remove options for projects that have been unpinned (keep All + still-present paths).
|
|
1369
|
+
Array.from(sel.querySelectorAll("option")).forEach(function(opt) {
|
|
1370
|
+
if (opt.value && opt.value !== "All" && !presentPaths.has(opt.value)) {
|
|
1371
|
+
sel.removeChild(opt);
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
// Restore previous selection if that path still exists; otherwise leave as-is
|
|
1375
|
+
// (a removed selection naturally falls back to the first remaining option).
|
|
1376
|
+
if (previousValue === "All" || presentPaths.has(previousValue)) {
|
|
1377
|
+
sel.value = previousValue;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
async function fetchBoardProjects() {
|
|
1382
|
+
try {
|
|
1383
|
+
const res = await fetch("/api/projects");
|
|
1384
|
+
if (!res.ok) return;
|
|
1385
|
+
const data = await res.json();
|
|
1386
|
+
boardProjects = Array.isArray(data) ? data : [];
|
|
1387
|
+
syncRepoScopeDropdown();
|
|
1388
|
+
} catch (e) {
|
|
1389
|
+
// non-fatal: dropdown simply keeps whatever options it already has
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Wave 1 PBI/Bug state machine columns (6 primary + Removed toggle)
|
|
1394
|
+
const COLUMNS = [
|
|
1395
|
+
{ status: 'New', label: 'New', color: 'var(--white)' },
|
|
1396
|
+
{ status: 'Approved', label: 'Approved', color: 'var(--cyan)' },
|
|
1397
|
+
{ status: 'Committed', label: 'Committed', color: 'var(--amber)' },
|
|
1398
|
+
{ status: 'In Progress', label: 'In Progress', color: 'var(--amber)' },
|
|
1399
|
+
{ status: 'In Testing', label: 'In Testing', color: 'var(--cyan)' },
|
|
1400
|
+
{ status: 'Done', label: 'Done', color: 'var(--green)' },
|
|
1401
|
+
];
|
|
1402
|
+
const REMOVED_COLUMN = { status: 'Removed', label: 'Removed', color: 'var(--dim)' };
|
|
1403
|
+
|
|
1404
|
+
// Six lifecycle dot labels (Plan / Impl / Review / Tests / Docs / Merged)
|
|
1405
|
+
const LIFECYCLE_PHASES = [
|
|
1406
|
+
'planning',
|
|
1407
|
+
'implementing',
|
|
1408
|
+
'reviewing',
|
|
1409
|
+
'testing',
|
|
1410
|
+
'documenting',
|
|
1411
|
+
'done',
|
|
1412
|
+
];
|
|
1413
|
+
|
|
1414
|
+
let allWorkItems = [];
|
|
1415
|
+
let areas = new Set();
|
|
1416
|
+
let showRemoved = false;
|
|
1417
|
+
|
|
1418
|
+
// Normalize a work item's priority to one of: urgent|high|medium|low|none.
|
|
1419
|
+
// Handles: local label strings, AzDO numeric 1-4 (top-level or under azdoFields),
|
|
1420
|
+
// and AzDO "1-Critical / 2-High / 3-Medium / 4-Low" label strings.
|
|
1421
|
+
function normalizePriority(wi) {
|
|
1422
|
+
if (!wi) return 'none';
|
|
1423
|
+
const numMap = { 1: 'urgent', 2: 'high', 3: 'medium', 4: 'low' };
|
|
1424
|
+
const az = wi.azdoFields || null;
|
|
1425
|
+
// 1) AzDO numeric priority (authoritative when AzDO-sourced)
|
|
1426
|
+
if (az && typeof az.priority === 'number' && numMap[az.priority]) {
|
|
1427
|
+
return numMap[az.priority];
|
|
1428
|
+
}
|
|
1429
|
+
// 2) Top-level value may be a number, numeric string, or a label
|
|
1430
|
+
const raw = wi.priority;
|
|
1431
|
+
if (typeof raw === 'number' && numMap[raw]) return numMap[raw];
|
|
1432
|
+
if (typeof raw === 'string') {
|
|
1433
|
+
const t = raw.trim().toLowerCase();
|
|
1434
|
+
if (!t) return 'none';
|
|
1435
|
+
// pure number string e.g. "2"
|
|
1436
|
+
if (/^[1-4]$/.test(t)) return numMap[Number(t)];
|
|
1437
|
+
// "2-high", "1 - critical" style → take leading digit
|
|
1438
|
+
const leadDigit = t.match(/^([1-4])\\b/);
|
|
1439
|
+
if (leadDigit) return numMap[Number(leadDigit[1])];
|
|
1440
|
+
// already a label
|
|
1441
|
+
if (t === 'urgent' || t === 'critical') return 'urgent';
|
|
1442
|
+
if (t === 'high') return 'high';
|
|
1443
|
+
if (t === 'medium' || t === 'normal') return 'medium';
|
|
1444
|
+
if (t === 'low') return 'low';
|
|
1445
|
+
if (t === 'none') return 'none';
|
|
1446
|
+
}
|
|
1447
|
+
return 'none';
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// REST helpers
|
|
1451
|
+
async function api(method, path, body) {
|
|
1452
|
+
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
|
1453
|
+
if (body !== undefined) opts.body = JSON.stringify(body);
|
|
1454
|
+
const res = await fetch(path, opts);
|
|
1455
|
+
const json = await res.json();
|
|
1456
|
+
if (!json.ok && !res.ok) throw new Error(json.error || 'API error');
|
|
1457
|
+
return json;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// Load work items.
|
|
1461
|
+
// NOTE: area filtering is intentionally client-side (see applyAreaFilter) and is
|
|
1462
|
+
// NOT passed to the server here — changing the area must never trigger a network
|
|
1463
|
+
// round-trip. Only this function (and the background sync) hits the API.
|
|
1464
|
+
async function loadWorkItems() {
|
|
1465
|
+
const json = await api('GET', '/api/factory/workitems');
|
|
1466
|
+
|
|
1467
|
+
const items = json.data || [];
|
|
1468
|
+
|
|
1469
|
+
allWorkItems = items.map(wi => {
|
|
1470
|
+
const bizState = wi.layeredState?.business?.state || wi.status || 'New';
|
|
1471
|
+
const execPhase = wi.layeredState?.execution?.currentPhase || null;
|
|
1472
|
+
const execArtifacts = wi.layeredState?.execution?.artifacts || [];
|
|
1473
|
+
const az = wi.azdoFields || null;
|
|
1474
|
+
// Normalize priority from any source shape to a label (urgent/high/medium/low/none).
|
|
1475
|
+
const normalizedPriority = normalizePriority(wi);
|
|
1476
|
+
return {
|
|
1477
|
+
id: wi.id,
|
|
1478
|
+
// AzDO numeric id (string of digits) when sourced from AzDO; same as id in that case.
|
|
1479
|
+
azdoId: az?.azdoId || null,
|
|
1480
|
+
area: wi.area || '',
|
|
1481
|
+
title: wi.title || '',
|
|
1482
|
+
description: az?.description || wi.description || '',
|
|
1483
|
+
status: bizState,
|
|
1484
|
+
priority: normalizedPriority,
|
|
1485
|
+
source: wi.source || 'local',
|
|
1486
|
+
labels: wi.labels || [],
|
|
1487
|
+
assignee: wi.assignee || null,
|
|
1488
|
+
relevantFiles: wi.relevantFiles || [],
|
|
1489
|
+
dependencies: wi.dependencies || [],
|
|
1490
|
+
childIds: wi.childIds || [],
|
|
1491
|
+
memoryIds: wi.memoryIds || [],
|
|
1492
|
+
acceptanceCriteria: az?.acceptanceCriteria || wi.acceptanceCriteria || '',
|
|
1493
|
+
// Parent chain for breadcrumb
|
|
1494
|
+
parentId: wi.parentId || null,
|
|
1495
|
+
parentTitle: az?.parentTitle || wi.parentTitle || null,
|
|
1496
|
+
grandparentTitle: az?.grandparentTitle || wi.grandparentTitle || null,
|
|
1497
|
+
// Operator Lane / Failure Reason — AzDO custom fields
|
|
1498
|
+
operatorLane: az?.operatorLane || wi.operatorLane || null,
|
|
1499
|
+
failureReason: az?.failureReason || wi.failureReason || null,
|
|
1500
|
+
workItemType: az?.workItemType || null,
|
|
1501
|
+
// Lifecycle booleans from AzDO custom fields override execution-phase derivation.
|
|
1502
|
+
azLifecycle: az?.lifecycle || null,
|
|
1503
|
+
// Execution phase for lifecycle dots (locally-driven items)
|
|
1504
|
+
execPhase,
|
|
1505
|
+
execArtifacts,
|
|
1506
|
+
// Prefer real AzDO change/created dates when the mirror provides them, then
|
|
1507
|
+
// fall back to the payload's own timestamps. (When AzDO is the source these
|
|
1508
|
+
// may equal the sync time; see normalizeCardTimestamp for selection order.)
|
|
1509
|
+
changedAt: az?.changedDate || az?.System_ChangedDate || wi.changedAt || null,
|
|
1510
|
+
createdAt: az?.createdDate || az?.System_CreatedDate || wi.createdAt || null,
|
|
1511
|
+
updatedAt: wi.updatedAt || null,
|
|
1512
|
+
closedAt: wi.closedAt || null,
|
|
1513
|
+
};
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
// Area list refresh
|
|
1517
|
+
areas = new Set();
|
|
1518
|
+
for (const wi of allWorkItems) if (wi.area) areas.add(wi.area);
|
|
1519
|
+
updateAreaFilter();
|
|
1520
|
+
updateBoardWorkItemSummary();
|
|
1521
|
+
renderBoard();
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
function updateAreaFilter() {
|
|
1525
|
+
const sel = document.getElementById('filter-area');
|
|
1526
|
+
// Preserve the active area selection across option rebuilds.
|
|
1527
|
+
const current = currentAreaFilter || sel.value;
|
|
1528
|
+
const opts = ['<option value="">all areas</option>'];
|
|
1529
|
+
for (const a of areas) {
|
|
1530
|
+
opts.push('<option value="' + a + '"' + (a === current ? ' selected' : '') + '>' + a + '</option>');
|
|
1531
|
+
}
|
|
1532
|
+
sel.innerHTML = opts.join('');
|
|
1533
|
+
// If the previously-selected area no longer exists, fall back to "all areas".
|
|
1534
|
+
if (current && !areas.has(current)) {
|
|
1535
|
+
currentAreaFilter = '';
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function toggleShowRemoved() {
|
|
1540
|
+
showRemoved = !showRemoved;
|
|
1541
|
+
const btn = document.getElementById('show-removed-btn');
|
|
1542
|
+
btn.classList.toggle('active', showRemoved);
|
|
1543
|
+
// Update board grid: 7 columns when Removed is visible, 6 otherwise
|
|
1544
|
+
document.getElementById('board').classList.toggle('show-removed', showRemoved);
|
|
1545
|
+
renderBoard();
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// Compute the visible work items by applying ALL client-side filters:
|
|
1549
|
+
// shared repo scope, toolbar area, toolbar priority, and the search box.
|
|
1550
|
+
// Pure function over allWorkItems + the current filter state — no network.
|
|
1551
|
+
function filterWorkItems(items, opts) {
|
|
1552
|
+
const o = opts || {};
|
|
1553
|
+
const repoScope = o.repoScope !== undefined ? o.repoScope : currentRepoScope;
|
|
1554
|
+
const area = o.area !== undefined ? o.area : currentAreaFilter;
|
|
1555
|
+
const priority = o.priority !== undefined ? o.priority : currentPriorityFilter;
|
|
1556
|
+
const search = o.search !== undefined ? o.search : currentSearchQuery();
|
|
1557
|
+
let out = Array.isArray(items) ? items : [];
|
|
1558
|
+
if (repoScope && repoScope !== "All") {
|
|
1559
|
+
const needle = repoScope.replace("enact-", "").toLowerCase();
|
|
1560
|
+
out = out.filter((item) => (item.area || '').toLowerCase().includes(needle));
|
|
1561
|
+
}
|
|
1562
|
+
if (area) {
|
|
1563
|
+
out = out.filter((item) => item.area === area);
|
|
1564
|
+
}
|
|
1565
|
+
if (priority) {
|
|
1566
|
+
out = out.filter((item) => item.priority === priority);
|
|
1567
|
+
}
|
|
1568
|
+
if (search) {
|
|
1569
|
+
const q = search.toLowerCase();
|
|
1570
|
+
out = out.filter((item) =>
|
|
1571
|
+
(item.title || '').toLowerCase().includes(q) ||
|
|
1572
|
+
(item.description || '').toLowerCase().includes(q));
|
|
1573
|
+
}
|
|
1574
|
+
return out;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function currentSearchQuery() {
|
|
1578
|
+
const el = document.getElementById('search-input');
|
|
1579
|
+
return el ? (el.value || '') : '';
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
function getVisibleWorkItems() {
|
|
1583
|
+
return filterWorkItems(allWorkItems, {});
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// Kanban board rendering
|
|
1587
|
+
function renderBoard() {
|
|
1588
|
+
const board = document.getElementById('board');
|
|
1589
|
+
board.innerHTML = '';
|
|
1590
|
+
|
|
1591
|
+
const scopedItems = getVisibleWorkItems();
|
|
1592
|
+
const cols = showRemoved ? [...COLUMNS, REMOVED_COLUMN] : COLUMNS;
|
|
1593
|
+
|
|
1594
|
+
for (const col of cols) {
|
|
1595
|
+
const items = scopedItems.filter(i => i.status === col.status);
|
|
1596
|
+
const colEl = document.createElement('div');
|
|
1597
|
+
colEl.className = 'column' + (col.status === 'Removed' ? ' col-removed' : '');
|
|
1598
|
+
colEl.innerHTML = \`
|
|
1599
|
+
<div class="col-header">
|
|
1600
|
+
<span style="color:\${col.color}">\${col.label}</span>
|
|
1601
|
+
<span class="count">\${items.length}</span>
|
|
1602
|
+
</div>
|
|
1603
|
+
<div class="col-body" data-status="\${col.status}"></div>
|
|
1604
|
+
\`;
|
|
1605
|
+
|
|
1606
|
+
const body = colEl.querySelector('.col-body');
|
|
1607
|
+
for (const wi of items) {
|
|
1608
|
+
body.appendChild(createCard(wi));
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// Drag-and-drop target
|
|
1612
|
+
body.addEventListener('dragover', e => { e.preventDefault(); body.style.background = 'var(--green-dim)'; });
|
|
1613
|
+
body.addEventListener('dragleave', () => { body.style.background = ''; });
|
|
1614
|
+
body.addEventListener('drop', async e => {
|
|
1615
|
+
e.preventDefault();
|
|
1616
|
+
body.style.background = '';
|
|
1617
|
+
const workItemId = e.dataTransfer.getData('text/plain');
|
|
1618
|
+
if (workItemId && col.status) {
|
|
1619
|
+
await api('PATCH', '/api/factory/workitems/' + workItemId, { business: { state: col.status } });
|
|
1620
|
+
loadWorkItems();
|
|
1621
|
+
}
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
board.appendChild(colEl);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// Lifecycle dots: prefer AzDO's six Boolean fields when present (mirror
|
|
1629
|
+
// is authoritative); otherwise derive from local execution phase order.
|
|
1630
|
+
function lifecycleDots(wi) {
|
|
1631
|
+
if (wi.azLifecycle) {
|
|
1632
|
+
const l = wi.azLifecycle;
|
|
1633
|
+
return [
|
|
1634
|
+
!!l.planWritten,
|
|
1635
|
+
!!l.implementationComplete,
|
|
1636
|
+
!!l.reviewPassed,
|
|
1637
|
+
!!l.testsPassing,
|
|
1638
|
+
!!l.docsUpdated,
|
|
1639
|
+
!!l.mergedToDevelop,
|
|
1640
|
+
];
|
|
1641
|
+
}
|
|
1642
|
+
const phase = wi.execPhase;
|
|
1643
|
+
if (!phase) return [false, false, false, false, false, false];
|
|
1644
|
+
const idx = LIFECYCLE_PHASES.indexOf(phase);
|
|
1645
|
+
return LIFECYCLE_PHASES.map((_, i) => phase === 'done' ? true : i < idx);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
function renderLifecycleDots(wi) {
|
|
1649
|
+
const filled = lifecycleDots(wi);
|
|
1650
|
+
const titles = ['Plan written', 'Implementation complete', 'Review passed', 'Tests passing', 'Docs updated', 'Merged to develop'];
|
|
1651
|
+
return '<div class="lifecycle-dots" title="Plan / Impl / Review / Tests / Docs / Merged">' +
|
|
1652
|
+
filled.map((f, i) => '<span class="dot' + (f ? ' filled' : '') + '" title="' + titles[i] + '"></span>').join('') +
|
|
1653
|
+
'</div>';
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
function renderLaneBadge(lane) {
|
|
1657
|
+
if (!lane || lane === 'none') return '';
|
|
1658
|
+
const cls = 'lane-' + lane.toLowerCase();
|
|
1659
|
+
return '<span class="badge-lane ' + cls + '">' + lane + '</span>';
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function renderBreadcrumb(wi) {
|
|
1663
|
+
if (wi.grandparentTitle && wi.parentTitle) {
|
|
1664
|
+
return '<div class="card-breadcrumb has-parent">' + escHtml(wi.parentTitle) + ' → ' + escHtml(wi.grandparentTitle) + '</div>';
|
|
1665
|
+
}
|
|
1666
|
+
if (wi.parentTitle) {
|
|
1667
|
+
return '<div class="card-breadcrumb has-parent">' + escHtml(wi.parentTitle) + '</div>';
|
|
1668
|
+
}
|
|
1669
|
+
// No parent → render nothing so the card doesn't show a stray blank/em-dash first line.
|
|
1670
|
+
return '';
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
function createCard(wi) {
|
|
1674
|
+
const card = document.createElement('div');
|
|
1675
|
+
card.className = 'card';
|
|
1676
|
+
card.draggable = true;
|
|
1677
|
+
card.addEventListener('dragstart', e => { e.dataTransfer.setData('text/plain', wi.id); });
|
|
1678
|
+
card.addEventListener('click', () => openDetail(wi.id));
|
|
1679
|
+
|
|
1680
|
+
const priorityClass = 'p-' + wi.priority;
|
|
1681
|
+
const labels = (wi.labels || []).map(l => '<span class="card-label">' + l + '</span>').join('');
|
|
1682
|
+
const cardTs = normalizeCardTimestamp(wi);
|
|
1683
|
+
const timeAgo = formatTimeAgo(cardTs);
|
|
1684
|
+
const absTime = formatAbsoluteTime(cardTs);
|
|
1685
|
+
const dotsHtml = renderLifecycleDots(wi);
|
|
1686
|
+
const laneBadge = renderLaneBadge(wi.operatorLane);
|
|
1687
|
+
const failBadge = wi.failureReason
|
|
1688
|
+
? '<span class="badge-failure">' + escHtml(wi.failureReason) + '</span>'
|
|
1689
|
+
: '';
|
|
1690
|
+
const breadcrumb = renderBreadcrumb(wi);
|
|
1691
|
+
|
|
1692
|
+
// AzDO-sourced rows: numeric id rendered as #1234; local-created: short nanoid.
|
|
1693
|
+
const idLabel = wi.azdoId ? '#' + wi.azdoId : wi.id.slice(0, 6);
|
|
1694
|
+
const typeBadge = wi.workItemType
|
|
1695
|
+
? '<span class="card-label">' + escHtml(wi.workItemType) + '</span>'
|
|
1696
|
+
: '';
|
|
1697
|
+
|
|
1698
|
+
card.innerHTML = \`
|
|
1699
|
+
\${breadcrumb}
|
|
1700
|
+
<div class="card-title"><span class="card-priority \${priorityClass}"></span>\${escHtml(wi.title)}</div>
|
|
1701
|
+
<div class="card-meta">
|
|
1702
|
+
<span class="card-id">\${idLabel}</span>
|
|
1703
|
+
\${typeBadge}
|
|
1704
|
+
<span>\${wi.area}</span>
|
|
1705
|
+
\${wi.assignee ? '<span>' + wi.assignee + '</span>' : ''}
|
|
1706
|
+
\${timeAgo ? '<span title="' + escHtml(absTime) + '">' + timeAgo + '</span>' : ''}
|
|
1707
|
+
</div>
|
|
1708
|
+
<div class="card-meta" style="margin-top:4px">
|
|
1709
|
+
\${dotsHtml}
|
|
1710
|
+
\${laneBadge}
|
|
1711
|
+
\${failBadge}
|
|
1712
|
+
</div>
|
|
1713
|
+
\${labels ? '<div class="card-meta" style="margin-top:3px">' + labels + '</div>' : ''}
|
|
1714
|
+
\`;
|
|
1715
|
+
return card;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// Work Item detail panel
|
|
1719
|
+
async function openDetail(id) {
|
|
1720
|
+
const [itemJson, eventsJson] = await Promise.all([
|
|
1721
|
+
api('GET', '/api/factory/workitems/' + id),
|
|
1722
|
+
api('GET', '/api/factory/workitems/' + id + '/events?limit=20'),
|
|
1723
|
+
]);
|
|
1724
|
+
|
|
1725
|
+
const wi = itemJson.item;
|
|
1726
|
+
if (!wi) return;
|
|
1727
|
+
const bizState = wi.layeredState?.business?.state || wi.status || 'New';
|
|
1728
|
+
const az = wi.azdoFields || null;
|
|
1729
|
+
const item = {
|
|
1730
|
+
id: wi.id,
|
|
1731
|
+
azdoId: az?.azdoId || null,
|
|
1732
|
+
workItemType: az?.workItemType || null,
|
|
1733
|
+
area: wi.area || '',
|
|
1734
|
+
title: wi.title || '',
|
|
1735
|
+
description: az?.description || wi.description || '',
|
|
1736
|
+
status: az?.state || bizState,
|
|
1737
|
+
priority: wi.priority || 'none',
|
|
1738
|
+
labels: wi.labels || [],
|
|
1739
|
+
assignee: wi.assignee || null,
|
|
1740
|
+
relevantFiles: wi.relevantFiles || [],
|
|
1741
|
+
acceptanceCriteria: az?.acceptanceCriteria || wi.acceptanceCriteria || '',
|
|
1742
|
+
reproSteps: az?.reproSteps || null,
|
|
1743
|
+
systemInfo: az?.systemInfo || null,
|
|
1744
|
+
dependencies: wi.dependencies || [],
|
|
1745
|
+
childIds: wi.childIds || [],
|
|
1746
|
+
memoryIds: wi.memoryIds || [],
|
|
1747
|
+
parentTitle: az?.parentTitle || wi.parentTitle || null,
|
|
1748
|
+
grandparentTitle: az?.grandparentTitle || wi.grandparentTitle || null,
|
|
1749
|
+
operatorLane: az?.operatorLane || wi.operatorLane || null,
|
|
1750
|
+
failureReason: az?.failureReason || wi.failureReason || null,
|
|
1751
|
+
azLifecycle: az?.lifecycle || null,
|
|
1752
|
+
execPhase: wi.layeredState?.execution?.currentPhase || null,
|
|
1753
|
+
execArtifacts: wi.layeredState?.execution?.artifacts || [],
|
|
1754
|
+
createdAt: wi.createdAt,
|
|
1755
|
+
updatedAt: wi.updatedAt,
|
|
1756
|
+
closedAt: wi.closedAt,
|
|
1757
|
+
};
|
|
1758
|
+
const events = eventsJson.events || [];
|
|
1759
|
+
|
|
1760
|
+
const panel = document.getElementById('detail-panel');
|
|
1761
|
+
const content = document.getElementById('detail-content');
|
|
1762
|
+
|
|
1763
|
+
const allCols = [...COLUMNS, REMOVED_COLUMN];
|
|
1764
|
+
const statusOptions = allCols.map(c =>
|
|
1765
|
+
'<option value="' + c.status + '"' + (c.status === item.status ? ' selected' : '') + '>' + c.label + '</option>'
|
|
1766
|
+
).join('');
|
|
1767
|
+
|
|
1768
|
+
const detailIdLabel = item.azdoId ? '#' + item.azdoId : item.id;
|
|
1769
|
+
const typeLabel = item.workItemType ? ' | ' + item.workItemType : '';
|
|
1770
|
+
const laneRow = item.operatorLane
|
|
1771
|
+
? '<div class="detail-section"><h4>OPERATOR LANE</h4><p>' + escHtml(item.operatorLane) + '</p></div>'
|
|
1772
|
+
: '';
|
|
1773
|
+
const failureRow = item.failureReason
|
|
1774
|
+
? '<div class="detail-section"><h4>FAILURE REASON</h4><p style="color:var(--red)">' + escHtml(item.failureReason) + '</p></div>'
|
|
1775
|
+
: '';
|
|
1776
|
+
const lifecycleRow = item.azLifecycle ? renderLifecycleDots(item) : '';
|
|
1777
|
+
const lifecycleSection = item.azLifecycle
|
|
1778
|
+
? '<div class="detail-section"><h4>LIFECYCLE</h4>' + lifecycleRow + '</div>'
|
|
1779
|
+
: '';
|
|
1780
|
+
|
|
1781
|
+
// Acceptance criteria: AzDO may send HTML; local items may be string with checkbox lines or an array.
|
|
1782
|
+
// renderAcceptanceCriteria handles all three formats and ensures each item is on its own line.
|
|
1783
|
+
const acRaw = item.acceptanceCriteria;
|
|
1784
|
+
const acRendered = renderAcceptanceCriteria(acRaw);
|
|
1785
|
+
const acHtml = acRendered
|
|
1786
|
+
? '<div class="detail-section"><h4>ACCEPTANCE CRITERIA</h4>' + acRendered + '</div>'
|
|
1787
|
+
: '';
|
|
1788
|
+
|
|
1789
|
+
// Escape repro steps / system info (may be AzDO-sourced HTML) to prevent XSS; preserve line breaks.
|
|
1790
|
+
const reproRow = item.reproSteps
|
|
1791
|
+
? '<div class="detail-section"><h4>REPRO STEPS</h4><div style="font-size:12px">' + escHtml(String(item.reproSteps)).replace(/\\r?\\n/g, '<br>') + '</div></div>'
|
|
1792
|
+
: '';
|
|
1793
|
+
const sysInfoRow = item.systemInfo
|
|
1794
|
+
? '<div class="detail-section"><h4>SYSTEM INFO</h4><div style="font-size:12px">' + escHtml(String(item.systemInfo)).replace(/\\r?\\n/g, '<br>') + '</div></div>'
|
|
1795
|
+
: '';
|
|
1796
|
+
|
|
1797
|
+
content.innerHTML = \`
|
|
1798
|
+
<div class="detail-title">\${escHtml(item.title)}</div>
|
|
1799
|
+
<div style="color:var(--dim);font-size:10px;margin-bottom:1rem">
|
|
1800
|
+
\${detailIdLabel}\${typeLabel} | \${item.area}
|
|
1801
|
+
</div>
|
|
1802
|
+
|
|
1803
|
+
<div class="detail-section">
|
|
1804
|
+
<h4>STATUS</h4>
|
|
1805
|
+
<select class="filter-select" onchange="changeStatus('\${item.id}', this.value)" style="width:100%">
|
|
1806
|
+
\${statusOptions}
|
|
1807
|
+
</select>
|
|
1808
|
+
</div>
|
|
1809
|
+
|
|
1810
|
+
\${lifecycleSection}
|
|
1811
|
+
\${laneRow}
|
|
1812
|
+
\${failureRow}
|
|
1813
|
+
|
|
1814
|
+
<div class="detail-section">
|
|
1815
|
+
<h4>DESCRIPTION</h4>
|
|
1816
|
+
<div style="font-size:12px;line-height:1.5">\${item.description ? renderMarkdown(item.description) : '<span style="color:var(--dim)">no description</span>'}</div>
|
|
1817
|
+
</div>
|
|
1818
|
+
|
|
1819
|
+
\${acHtml}
|
|
1820
|
+
\${reproRow}
|
|
1821
|
+
\${sysInfoRow}
|
|
1822
|
+
|
|
1823
|
+
\${item.relevantFiles.length ? '<div class="detail-section"><h4>RELEVANT FILES</h4><ul>' + item.relevantFiles.map(f => '<li>' + escHtml(f) + '</li>').join('') + '</ul></div>' : ''}
|
|
1824
|
+
|
|
1825
|
+
\${item.dependencies.length ? '<div class="detail-section"><h4>DEPENDENCIES</h4><p>' + item.dependencies.join(', ') + '</p></div>' : ''}
|
|
1826
|
+
|
|
1827
|
+
<div class="detail-section">
|
|
1828
|
+
<h4>ACTIVITY (\${events.length})</h4>
|
|
1829
|
+
\${events.map(ev => \`
|
|
1830
|
+
<div class="event-item">
|
|
1831
|
+
<span class="ev-type">\${ev.type}</span>
|
|
1832
|
+
\${ev.content ? ': ' + escHtml(ev.content).slice(0, 100) : ''}
|
|
1833
|
+
\${ev.oldValue && ev.newValue ? ': ' + ev.oldValue + ' → ' + ev.newValue : ''}
|
|
1834
|
+
<span style="float:right">\${formatTimeAgo(ev.createdAt)}</span>
|
|
1835
|
+
</div>
|
|
1836
|
+
\`).join('')}
|
|
1837
|
+
</div>
|
|
1838
|
+
|
|
1839
|
+
<div class="detail-section" style="margin-top:1.5rem">
|
|
1840
|
+
<h4>ADD COMMENT</h4>
|
|
1841
|
+
<textarea id="comment-input" style="width:100%;background:var(--bg3);color:var(--white);border:1px solid var(--border);padding:6px;font-family:inherit;font-size:12px;min-height:60px;border-radius:3px" placeholder="comment..."></textarea>
|
|
1842
|
+
<button class="btn btn-primary" style="margin-top:4px" onclick="addComment('\${item.id}')">COMMENT</button>
|
|
1843
|
+
</div>
|
|
1844
|
+
|
|
1845
|
+
<div class="form-actions" style="margin-top:1.5rem;justify-content:flex-start">
|
|
1846
|
+
<button class="btn" style="color:var(--red);border-color:var(--red)" onclick="deleteWorkItem('\${item.id}')">DELETE</button>
|
|
1847
|
+
</div>
|
|
1848
|
+
\`;
|
|
1849
|
+
|
|
1850
|
+
panel.classList.add('active');
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function closeDetail() {
|
|
1854
|
+
document.getElementById('detail-panel').classList.remove('active');
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
async function changeStatus(id, status) {
|
|
1858
|
+
await api('PATCH', '/api/factory/workitems/' + id, { business: { state: status } });
|
|
1859
|
+
loadWorkItems();
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
async function addComment(id) {
|
|
1863
|
+
const input = document.getElementById('comment-input');
|
|
1864
|
+
const content = input.value.trim();
|
|
1865
|
+
if (!content) return;
|
|
1866
|
+
await api('POST', '/api/factory/workitems/' + id + '/comments', { content });
|
|
1867
|
+
input.value = '';
|
|
1868
|
+
openDetail(id);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
async function deleteWorkItem(id) {
|
|
1872
|
+
if (!confirm('Delete this work item?')) return;
|
|
1873
|
+
await api('DELETE', '/api/factory/workitems/' + id);
|
|
1874
|
+
closeDetail();
|
|
1875
|
+
loadWorkItems();
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// Work Item creation
|
|
1879
|
+
function openCreateModal() {
|
|
1880
|
+
const sel = document.getElementById('new-project');
|
|
1881
|
+
sel.innerHTML = [...areas].map(a => '<option value="' + a + '">' + a + '</option>').join('');
|
|
1882
|
+
if (sel.options.length === 0) {
|
|
1883
|
+
sel.innerHTML = '<option value="Enact/Factory">Enact/Factory</option>';
|
|
1884
|
+
}
|
|
1885
|
+
document.getElementById('create-modal').classList.add('active');
|
|
1886
|
+
document.getElementById('new-title').focus();
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
function closeCreateModal() {
|
|
1890
|
+
document.getElementById('create-modal').classList.remove('active');
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
async function createWorkItem() {
|
|
1894
|
+
const title = document.getElementById('new-title').value.trim();
|
|
1895
|
+
if (!title) { alert('Title required'); return; }
|
|
1896
|
+
|
|
1897
|
+
const input = {
|
|
1898
|
+
projectId: document.getElementById('new-project').value || 'Enact/Factory',
|
|
1899
|
+
title,
|
|
1900
|
+
description: document.getElementById('new-desc').value,
|
|
1901
|
+
priority: document.getElementById('new-priority').value,
|
|
1902
|
+
status: document.getElementById('new-status').value,
|
|
1903
|
+
labels: document.getElementById('new-labels').value.split(',').map(s => s.trim()).filter(Boolean),
|
|
1904
|
+
relevantFiles: document.getElementById('new-files').value.split(',').map(s => s.trim()).filter(Boolean),
|
|
1905
|
+
};
|
|
1906
|
+
|
|
1907
|
+
await api('POST', '/api/factory/workitems', input);
|
|
1908
|
+
|
|
1909
|
+
closeCreateModal();
|
|
1910
|
+
document.getElementById('new-title').value = '';
|
|
1911
|
+
document.getElementById('new-desc').value = '';
|
|
1912
|
+
document.getElementById('new-labels').value = '';
|
|
1913
|
+
document.getElementById('new-files').value = '';
|
|
1914
|
+
loadWorkItems();
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// Utilities
|
|
1918
|
+
function escHtml(s) {
|
|
1919
|
+
if (!s) return '';
|
|
1920
|
+
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// Lightweight dependency-free markdown→HTML renderer (no external deps).
|
|
1924
|
+
// Escapes HTML first (XSS-safe), then applies markdown transforms.
|
|
1925
|
+
function renderMarkdown(raw) {
|
|
1926
|
+
if (!raw) return '';
|
|
1927
|
+
// 1. HTML-escape the raw string first
|
|
1928
|
+
let s = escHtml(raw);
|
|
1929
|
+
// 2. Fenced code blocks (use character-class [^] trick to avoid literal backticks in template)
|
|
1930
|
+
const BT = '\`';
|
|
1931
|
+
const BT3 = BT + BT + BT;
|
|
1932
|
+
const fencedRe = new RegExp(BT3 + '([\\\\s\\\\S]*?)' + BT3, 'g');
|
|
1933
|
+
s = s.replace(fencedRe, function(_, code) {
|
|
1934
|
+
return '<pre style="background:var(--bg3);padding:0.5rem;border-radius:3px;overflow-x:auto;font-size:11px"><code>' + code.trim() + '</code></pre>';
|
|
1935
|
+
});
|
|
1936
|
+
// 3. Inline code (single backtick delimiters)
|
|
1937
|
+
const inlineCodeRe = new RegExp(BT + '([^' + BT + '\\\\n]+)' + BT, 'g');
|
|
1938
|
+
s = s.replace(inlineCodeRe, '<code style="background:var(--bg3);padding:1px 4px;border-radius:2px;font-size:11px">$1</code>');
|
|
1939
|
+
// 4. Bold (**text** or __text__)
|
|
1940
|
+
s = s.replace(/\\*\\*([^*\\n]+)\\*\\*/g, '<strong>$1</strong>');
|
|
1941
|
+
s = s.replace(/__([^_\\n]+)__/g, '<strong>$1</strong>');
|
|
1942
|
+
// 5. Italic (*text* or _text_) — single delimiters
|
|
1943
|
+
s = s.replace(/\\*([^*\\n]+)\\*/g, '<em>$1</em>');
|
|
1944
|
+
s = s.replace(/_([^_\\n]+)_/g, '<em>$1</em>');
|
|
1945
|
+
// 6. Links [text](url) — sanitize href: allow only http(s)/protocol-relative/root-relative URLs.
|
|
1946
|
+
s = s.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, function(_, text, url) {
|
|
1947
|
+
const safe = /^(https?:)?\\/\\//i.test(url) || /^\\//.test(url);
|
|
1948
|
+
if (!safe) return text;
|
|
1949
|
+
return '<a href="' + url + '" style="color:var(--cyan)" target="_blank" rel="noopener noreferrer">' + text + '</a>';
|
|
1950
|
+
});
|
|
1951
|
+
// 7. Process line by line for headings, lists, and paragraphs
|
|
1952
|
+
const lines = s.split('\\n');
|
|
1953
|
+
const out = [];
|
|
1954
|
+
let inUl = false;
|
|
1955
|
+
let inOl = false;
|
|
1956
|
+
function closeList() {
|
|
1957
|
+
if (inUl) { out.push('</ul>'); inUl = false; }
|
|
1958
|
+
if (inOl) { out.push('</ol>'); inOl = false; }
|
|
1959
|
+
}
|
|
1960
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1961
|
+
const line = lines[i];
|
|
1962
|
+
// Headings
|
|
1963
|
+
const h3 = line.match(/^#{3,}\\s+(.+)$/);
|
|
1964
|
+
const h2 = line.match(/^##\\s+(.+)$/);
|
|
1965
|
+
const h1 = line.match(/^#\\s+(.+)$/);
|
|
1966
|
+
if (h1 && !h2 && !h3) { closeList(); out.push('<h3 style="color:var(--green);font-size:13px;margin:0.75rem 0 0.25rem">' + h1[1] + '</h3>'); continue; }
|
|
1967
|
+
if (h2 && !h3) { closeList(); out.push('<h4 style="color:var(--cyan);font-size:12px;margin:0.6rem 0 0.2rem">' + h2[1] + '</h4>'); continue; }
|
|
1968
|
+
if (h3) { closeList(); out.push('<h5 style="color:var(--dim);font-size:11px;margin:0.5rem 0 0.15rem">' + h3[1] + '</h5>'); continue; }
|
|
1969
|
+
// Unordered list (-, *, +)
|
|
1970
|
+
const ulMatch = line.match(/^[\\-*+]\\s+(.+)$/);
|
|
1971
|
+
if (ulMatch) {
|
|
1972
|
+
if (inOl) { out.push('</ol>'); inOl = false; }
|
|
1973
|
+
if (!inUl) { out.push('<ul style="padding-left:1.25rem;margin:0.25rem 0">'); inUl = true; }
|
|
1974
|
+
out.push('<li>' + ulMatch[1] + '</li>');
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
// Ordered list (1. 2. etc.)
|
|
1978
|
+
const olMatch = line.match(/^\\d+\\.\\s+(.+)$/);
|
|
1979
|
+
if (olMatch) {
|
|
1980
|
+
if (inUl) { out.push('</ul>'); inUl = false; }
|
|
1981
|
+
if (!inOl) { out.push('<ol style="padding-left:1.25rem;margin:0.25rem 0">'); inOl = true; }
|
|
1982
|
+
out.push('<li>' + olMatch[1] + '</li>');
|
|
1983
|
+
continue;
|
|
1984
|
+
}
|
|
1985
|
+
// Blank line — close lists, emit spacing
|
|
1986
|
+
if (line.trim() === '') {
|
|
1987
|
+
closeList();
|
|
1988
|
+
out.push('<div style="height:0.4rem"></div>');
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
// Regular paragraph line
|
|
1992
|
+
closeList();
|
|
1993
|
+
out.push('<p style="margin:0 0 0.2rem">' + line + '</p>');
|
|
1994
|
+
}
|
|
1995
|
+
closeList();
|
|
1996
|
+
return out.join('\\n');
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// Render acceptance criteria: handles plain-text checkbox lines (- [ ] / - [x])
|
|
2000
|
+
// as well as raw HTML (AzDO) and plain string arrays.
|
|
2001
|
+
function renderAcceptanceCriteria(acRaw) {
|
|
2002
|
+
if (!acRaw) return '';
|
|
2003
|
+
// Array of strings → render as list items
|
|
2004
|
+
if (Array.isArray(acRaw) && acRaw.length) {
|
|
2005
|
+
return '<ul style="padding-left:1.25rem;margin:0">' +
|
|
2006
|
+
acRaw.map(function(c) { return '<li style="margin-bottom:2px">' + escHtml(String(c)) + '</li>'; }).join('') +
|
|
2007
|
+
'</ul>';
|
|
2008
|
+
}
|
|
2009
|
+
if (typeof acRaw !== 'string' || !acRaw.trim()) return '';
|
|
2010
|
+
const trimmed = acRaw.trim();
|
|
2011
|
+
// If the string looks like HTML (AzDO rich text), escape it to neutralize any
|
|
2012
|
+
// embedded scripts/handlers (XSS-safe), preserving line breaks for readability.
|
|
2013
|
+
if (/<[a-zA-Z]/.test(trimmed)) {
|
|
2014
|
+
return '<div style="font-size:12px">' + escHtml(trimmed).replace(/\\r?\\n/g, '<br>') + '</div>';
|
|
2015
|
+
}
|
|
2016
|
+
// Plain text with checkbox markers (- [ ] or - [x]) or plain bullet lines.
|
|
2017
|
+
// Split on newlines and render each as its own list item.
|
|
2018
|
+
const lines = trimmed.split(/\\r?\\n/);
|
|
2019
|
+
const items = lines
|
|
2020
|
+
.map(function(line) { return line.trim(); })
|
|
2021
|
+
.filter(function(line) { return line.length > 0; })
|
|
2022
|
+
.map(function(line) {
|
|
2023
|
+
// Checkbox checked: - [x] or * [x]
|
|
2024
|
+
const checked = line.match(/^[\\-*+]\\s*\\[x\\]\\s*(.+)$/i);
|
|
2025
|
+
if (checked) return '<li style="margin-bottom:3px;list-style:none"><span style="color:var(--green)">☑</span> ' + escHtml(checked[1]) + '</li>';
|
|
2026
|
+
// Checkbox unchecked: - [ ] or * [ ]
|
|
2027
|
+
const unchecked = line.match(/^[\\-*+]\\s*\\[\\s?\\]\\s*(.+)$/);
|
|
2028
|
+
if (unchecked) return '<li style="margin-bottom:3px;list-style:none"><span style="color:var(--dim)">☐</span> ' + escHtml(unchecked[1]) + '</li>';
|
|
2029
|
+
// Plain bullet
|
|
2030
|
+
const bullet = line.match(/^[\\-*+]\\s+(.+)$/);
|
|
2031
|
+
if (bullet) return '<li style="margin-bottom:3px">' + escHtml(bullet[1]) + '</li>';
|
|
2032
|
+
// Numbered
|
|
2033
|
+
const numbered = line.match(/^\\d+\\.\\s+(.+)$/);
|
|
2034
|
+
if (numbered) return '<li style="margin-bottom:3px">' + escHtml(numbered[1]) + '</li>';
|
|
2035
|
+
// Plain line
|
|
2036
|
+
return '<li style="margin-bottom:3px;list-style:none">' + escHtml(line) + '</li>';
|
|
2037
|
+
});
|
|
2038
|
+
if (!items.length) return '';
|
|
2039
|
+
return '<ul style="padding-left:0;margin:0">' + items.join('') + '</ul>';
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
function formatTimeAgo(iso) {
|
|
2043
|
+
if (!iso) return '';
|
|
2044
|
+
const t = new Date(iso).getTime();
|
|
2045
|
+
if (isNaN(t)) return '';
|
|
2046
|
+
const diff = Date.now() - t;
|
|
2047
|
+
const mins = Math.floor(diff / 60000);
|
|
2048
|
+
if (mins < 1) return 'just now';
|
|
2049
|
+
if (mins < 60) return mins + 'm ago';
|
|
2050
|
+
const hrs = Math.floor(mins / 60);
|
|
2051
|
+
if (hrs < 24) return hrs + 'h ago';
|
|
2052
|
+
const days = Math.floor(hrs / 24);
|
|
2053
|
+
return days + 'd ago';
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// Pick the most meaningful real timestamp for a card: most-recent activity first.
|
|
2057
|
+
// Returns an ISO string or '' when none is available.
|
|
2058
|
+
function normalizeCardTimestamp(wi) {
|
|
2059
|
+
if (!wi) return '';
|
|
2060
|
+
const candidates = [wi.changedAt, wi.updatedAt, wi.closedAt, wi.createdAt];
|
|
2061
|
+
for (const c of candidates) {
|
|
2062
|
+
if (c && !isNaN(new Date(c).getTime())) return c;
|
|
2063
|
+
}
|
|
2064
|
+
return '';
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
// Absolute, human-readable timestamp for the card title/tooltip.
|
|
2068
|
+
function formatAbsoluteTime(iso) {
|
|
2069
|
+
if (!iso) return '';
|
|
2070
|
+
const d = new Date(iso);
|
|
2071
|
+
if (isNaN(d.getTime())) return '';
|
|
2072
|
+
try {
|
|
2073
|
+
return d.toLocaleString();
|
|
2074
|
+
} catch (e) {
|
|
2075
|
+
return d.toISOString();
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
let searchTimer;
|
|
2080
|
+
function debounceSearch() {
|
|
2081
|
+
// Search is client-side over already-loaded items; just re-render after debounce.
|
|
2082
|
+
clearTimeout(searchTimer);
|
|
2083
|
+
searchTimer = setTimeout(() => {
|
|
2084
|
+
renderBoard();
|
|
2085
|
+
updateBoardWorkItemSummary();
|
|
2086
|
+
}, 200);
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// Toolbar AREA filter — CLIENT-SIDE only. Must NOT refetch from the server/AzDO.
|
|
2090
|
+
function applyAreaFilter() {
|
|
2091
|
+
const el = document.getElementById('filter-area');
|
|
2092
|
+
currentAreaFilter = el ? (el.value || '') : '';
|
|
2093
|
+
renderBoard();
|
|
2094
|
+
updateBoardWorkItemSummary();
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
// Toolbar PRIORITY filter — CLIENT-SIDE only.
|
|
2098
|
+
function applyPriorityFilter() {
|
|
2099
|
+
const el = document.getElementById('filter-priority');
|
|
2100
|
+
currentPriorityFilter = el ? (el.value || '') : '';
|
|
2101
|
+
renderBoard();
|
|
2102
|
+
updateBoardWorkItemSummary();
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// Back-compat: any remaining callers re-render client-side (no network).
|
|
2106
|
+
function applyFilter() {
|
|
2107
|
+
applyAreaFilter();
|
|
2108
|
+
applyPriorityFilter();
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
async function syncWorkItemsOnce() {
|
|
2112
|
+
try {
|
|
2113
|
+
await fetch('/api/workitems/sync', { method: 'POST' });
|
|
2114
|
+
} catch (err) {
|
|
2115
|
+
console.warn('Work item sync failed:', err);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// Keyboard shortcuts
|
|
2120
|
+
document.addEventListener('keydown', e => {
|
|
2121
|
+
if (e.key === 'Escape') {
|
|
2122
|
+
closeCreateModal();
|
|
2123
|
+
closeDetail();
|
|
2124
|
+
if (isThemePickerOpen()) closeThemePicker();
|
|
2125
|
+
}
|
|
2126
|
+
if (e.key === 'n' && !e.target.closest('input,textarea,select')) {
|
|
2127
|
+
e.preventDefault();
|
|
2128
|
+
openCreateModal();
|
|
2129
|
+
}
|
|
2130
|
+
});
|
|
2131
|
+
|
|
2132
|
+
document.addEventListener('click', (event) => {
|
|
2133
|
+
const target = event.target;
|
|
2134
|
+
// event.target may be a Text or SVG node lacking .closest(); resolve a safe Element.
|
|
2135
|
+
const targetEl = (target && typeof target.closest === 'function')
|
|
2136
|
+
? target
|
|
2137
|
+
: (target && target.parentElement ? target.parentElement : null);
|
|
2138
|
+
// Close theme popover when clicking outside it
|
|
2139
|
+
const popover = document.getElementById('theme-popover');
|
|
2140
|
+
const launcher = document.getElementById('theme-launcher-btn');
|
|
2141
|
+
if (popover && launcher) {
|
|
2142
|
+
if (!popover.contains(target) && !launcher.contains(target)) {
|
|
2143
|
+
closeThemePicker();
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
// Close detail panel when clicking outside it.
|
|
2147
|
+
// Guard: don't close when the click is INSIDE the panel,
|
|
2148
|
+
// or when the click is on a card (which triggers openDetail to open a new one).
|
|
2149
|
+
const detailPanel = document.getElementById('detail-panel');
|
|
2150
|
+
if (detailPanel && detailPanel.classList.contains('active')) {
|
|
2151
|
+
const onCard = targetEl && typeof targetEl.closest === 'function' && targetEl.closest('.card');
|
|
2152
|
+
if (!detailPanel.contains(target) && !onCard) {
|
|
2153
|
+
closeDetail();
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
|
|
2158
|
+
bootAppTheme();
|
|
2159
|
+
initializeTerminal();
|
|
2160
|
+
pollHealth();
|
|
2161
|
+
fetchLanes();
|
|
2162
|
+
fetchStuckWorkItems();
|
|
2163
|
+
fetchBoardProjects();
|
|
2164
|
+
fetchExecStats();
|
|
2165
|
+
|
|
2166
|
+
window.addEventListener('storage', (event) => {
|
|
2167
|
+
if (event.key === THEME_STORAGE) {
|
|
2168
|
+
bootAppTheme();
|
|
2169
|
+
}
|
|
2170
|
+
});
|
|
2171
|
+
|
|
2172
|
+
// Initial load
|
|
2173
|
+
syncWorkItemsOnce().finally(loadWorkItems);
|
|
2174
|
+
setInterval(pollHealth, 10000);
|
|
2175
|
+
setInterval(fetchLanes, 30000);
|
|
2176
|
+
setInterval(fetchStuckWorkItems, 60000);
|
|
2177
|
+
setInterval(renderSyncAgo, 1000);
|
|
2178
|
+
setInterval(fetchExecStats, 60000);
|
|
2179
|
+
// 30-second auto-refresh
|
|
2180
|
+
setInterval(async () => {
|
|
2181
|
+
await syncWorkItemsOnce();
|
|
2182
|
+
await loadWorkItems();
|
|
2183
|
+
}, 30000);
|
|
2184
|
+
connectBoardSSE(true);
|
|
2185
|
+
</script>
|
|
2186
|
+
</body>
|
|
2187
|
+
</html>`;
|
|
2188
|
+
}
|
|
2189
|
+
export const WORK_ITEM_BOARD_HTML = createWorkItemBoardHtml();
|
|
2190
|
+
// Legacy alias kept for any existing imports
|
|
2191
|
+
export const ISSUE_BOARD_HTML = WORK_ITEM_BOARD_HTML;
|
|
2192
|
+
//# sourceMappingURL=workItemBoardHtml.js.map
|