diff --git a/ccw/src/core/routes/issue-routes.ts b/ccw/src/core/routes/issue-routes.ts index 1be89304..c14f63ad 100644 --- a/ccw/src/core/routes/issue-routes.ts +++ b/ccw/src/core/routes/issue-routes.ts @@ -236,6 +236,82 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise { return true; } + // GET /api/queue/history - Get queue history (all queues from index) + if (pathname === '/api/queue/history' && req.method === 'GET') { + const queuesDir = join(issuesDir, 'queues'); + const indexPath = join(queuesDir, 'index.json'); + + if (!existsSync(indexPath)) { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ queues: [], active_queue_id: null })); + return true; + } + + try { + const index = JSON.parse(readFileSync(indexPath, 'utf8')); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(index)); + } catch { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ queues: [], active_queue_id: null })); + } + return true; + } + + // GET /api/queue/:id - Get specific queue by ID + const queueDetailMatch = pathname.match(/^\/api\/queue\/([^/]+)$/); + if (queueDetailMatch && req.method === 'GET' && queueDetailMatch[1] !== 'history' && queueDetailMatch[1] !== 'reorder') { + const queueId = queueDetailMatch[1]; + const queuesDir = join(issuesDir, 'queues'); + const queueFilePath = join(queuesDir, `${queueId}.json`); + + if (!existsSync(queueFilePath)) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: `Queue ${queueId} not found` })); + return true; + } + + try { + const queue = JSON.parse(readFileSync(queueFilePath, 'utf8')); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(groupQueueByExecutionGroup(queue))); + } catch { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Failed to read queue' })); + } + return true; + } + + // POST /api/queue/switch - Switch active queue + if (pathname === '/api/queue/switch' && req.method === 'POST') { + handlePostRequest(req, res, async (body: any) => { + const { queueId } = body; + if (!queueId) return { error: 'queueId required' }; + + const queuesDir = join(issuesDir, 'queues'); + const indexPath = join(queuesDir, 'index.json'); + const queueFilePath = join(queuesDir, `${queueId}.json`); + + if (!existsSync(queueFilePath)) { + return { error: `Queue ${queueId} not found` }; + } + + try { + const index = existsSync(indexPath) + ? JSON.parse(readFileSync(indexPath, 'utf8')) + : { active_queue_id: null, queues: [] }; + + index.active_queue_id = queueId; + writeFileSync(indexPath, JSON.stringify(index, null, 2)); + + return { success: true, active_queue_id: queueId }; + } catch (err) { + return { error: 'Failed to switch queue' }; + } + }); + return true; + } + // POST /api/queue/reorder - Reorder queue items if (pathname === '/api/queue/reorder' && req.method === 'POST') { handlePostRequest(req, res, async (body: any) => { diff --git a/ccw/src/templates/dashboard-css/32-issue-manager.css b/ccw/src/templates/dashboard-css/32-issue-manager.css index a47de4a1..b59c180a 100644 --- a/ccw/src/templates/dashboard-css/32-issue-manager.css +++ b/ccw/src/templates/dashboard-css/32-issue-manager.css @@ -2542,3 +2542,260 @@ min-width: 80px; } } + +/* ========================================== + QUEUE HISTORY MODAL + ========================================== */ + +.queue-history-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.queue-history-item { + padding: 1rem; + background: hsl(var(--muted) / 0.3); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.15s ease; +} + +.queue-history-item:hover { + background: hsl(var(--muted) / 0.5); + border-color: hsl(var(--primary) / 0.3); +} + +.queue-history-item.active { + border-color: hsl(var(--primary)); + background: hsl(var(--primary) / 0.1); +} + +.queue-history-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.5rem; +} + +.queue-history-id { + font-size: 0.875rem; + font-weight: 600; + color: hsl(var(--foreground)); +} + +.queue-active-badge { + padding: 0.125rem 0.5rem; + font-size: 0.625rem; + font-weight: 600; + text-transform: uppercase; + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + border-radius: 9999px; +} + +.queue-history-status { + padding: 0.125rem 0.5rem; + font-size: 0.75rem; + border-radius: 0.25rem; + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); +} + +.queue-history-status.active { + background: hsl(142 76% 36% / 0.2); + color: hsl(142 76% 36%); +} + +.queue-history-status.completed { + background: hsl(142 76% 36% / 0.2); + color: hsl(142 76% 36%); +} + +.queue-history-status.archived { + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); +} + +.queue-history-meta { + display: flex; + flex-wrap: wrap; + gap: 1rem; + margin-bottom: 0.75rem; +} + +.queue-history-actions { + display: flex; + gap: 0.5rem; + justify-content: flex-end; +} + +/* Queue Detail View */ +.queue-detail-view { + padding: 0.5rem 0; +} + +.queue-detail-header { + display: flex; + align-items: center; + padding-bottom: 1rem; + border-bottom: 1px solid hsl(var(--border)); +} + +.queue-detail-stats { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0.75rem; +} + +.queue-detail-stats .stat-item { + text-align: center; + padding: 0.75rem; + background: hsl(var(--muted) / 0.3); + border-radius: 0.5rem; +} + +.queue-detail-stats .stat-value { + display: block; + font-size: 1.5rem; + font-weight: 700; + color: hsl(var(--foreground)); +} + +.queue-detail-stats .stat-label { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); +} + +.queue-detail-stats .stat-item.completed .stat-value { + color: hsl(142 76% 36%); +} + +.queue-detail-stats .stat-item.pending .stat-value { + color: hsl(48 96% 53%); +} + +.queue-detail-stats .stat-item.failed .stat-value { + color: hsl(0 84% 60%); +} + +.queue-detail-groups { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.queue-group-section { + background: hsl(var(--muted) / 0.2); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + overflow: hidden; +} + +.queue-group-header { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + background: hsl(var(--muted) / 0.3); + border-bottom: 1px solid hsl(var(--border)); + font-weight: 500; +} + +.queue-group-items { + padding: 0.5rem; +} + +.queue-detail-item { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.5rem 0.75rem; + border-radius: 0.25rem; +} + +.queue-detail-item:hover { + background: hsl(var(--muted) / 0.3); +} + +.queue-detail-item .item-id { + min-width: 120px; + color: hsl(var(--muted-foreground)); +} + +.queue-detail-item .item-issue { + min-width: 80px; + color: hsl(var(--primary)); +} + +.queue-detail-item .item-status { + margin-left: auto; + padding: 0.125rem 0.5rem; + font-size: 0.75rem; + border-radius: 0.25rem; + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); +} + +.queue-detail-item .item-status.completed { + background: hsl(142 76% 36% / 0.2); + color: hsl(142 76% 36%); +} + +.queue-detail-item .item-status.pending { + background: hsl(48 96% 53% / 0.2); + color: hsl(48 96% 53%); +} + +.queue-detail-item .item-status.executing { + background: hsl(217 91% 60% / 0.2); + color: hsl(217 91% 60%); +} + +.queue-detail-item .item-status.failed { + background: hsl(0 84% 60% / 0.2); + color: hsl(0 84% 60%); +} + +/* Small Buttons */ +.btn-sm { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + border-radius: 0.25rem; + border: none; + cursor: pointer; + transition: all 0.15s ease; +} + +.btn-sm.btn-primary { + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); +} + +.btn-sm.btn-primary:hover { + opacity: 0.9; +} + +.btn-sm.btn-secondary { + background: hsl(var(--muted)); + color: hsl(var(--foreground)); +} + +.btn-sm.btn-secondary:hover { + background: hsl(var(--muted) / 0.8); +} + +@media (max-width: 640px) { + .queue-detail-stats { + grid-template-columns: repeat(2, 1fr); + } + + .queue-history-meta { + flex-direction: column; + gap: 0.25rem; + } +} diff --git a/ccw/src/templates/dashboard-js/views/issue-manager.js b/ccw/src/templates/dashboard-js/views/issue-manager.js index ef5be1e5..c2c2bbe1 100644 --- a/ccw/src/templates/dashboard-js/views/issue-manager.js +++ b/ccw/src/templates/dashboard-js/views/issue-manager.js @@ -423,6 +423,10 @@ function renderQueueSection() { + + +
+
+ + ${t('common.loading') || 'Loading...'} +
+
+ + `; + modal.classList.remove('hidden'); + lucide.createIcons(); + + // Fetch queue history + try { + const response = await fetch(`/api/queue/history?path=${encodeURIComponent(projectPath)}`); + const data = await response.json(); + + const queues = data.queues || []; + const activeQueueId = data.active_queue_id; + + // Render queue list + const queueListHtml = queues.length === 0 + ? `
+ +

${t('issues.noQueueHistory') || 'No queue history found'}

+
` + : `
+ ${queues.map(q => ` +
+
+ ${q.id} + ${q.id === activeQueueId ? 'Active' : ''} + ${q.status || 'unknown'} +
+
+ + + ${q.issue_ids?.length || 0} issues + + + + ${q.completed_tasks || 0}/${q.total_tasks || 0} tasks + + + + ${q.created_at ? new Date(q.created_at).toLocaleDateString() : 'N/A'} + +
+
+ ${q.id !== activeQueueId ? ` + + ` : ''} + +
+
+ `).join('')} +
`; + + modal.querySelector('.issue-modal-body').innerHTML = queueListHtml; + lucide.createIcons(); + + } catch (err) { + console.error('Failed to load queue history:', err); + modal.querySelector('.issue-modal-body').innerHTML = ` +
+ +

${t('errors.loadFailed') || 'Failed to load queue history'}

+
+ `; + lucide.createIcons(); + } +} + +function hideQueueHistoryModal() { + const modal = document.getElementById('queueHistoryModal'); + if (modal) { + modal.classList.add('hidden'); + } +} + +async function switchToQueue(queueId) { + try { + const response = await fetch(`/api/queue/switch?path=${encodeURIComponent(projectPath)}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ queueId }) + }); + + const result = await response.json(); + if (result.success) { + showNotification(t('issues.queueSwitched') || 'Switched to queue: ' + queueId, 'success'); + hideQueueHistoryModal(); + await loadQueueData(); + renderIssueView(); + } else { + showNotification(result.error || 'Failed to switch queue', 'error'); + } + } catch (err) { + console.error('Failed to switch queue:', err); + showNotification('Failed to switch queue', 'error'); + } +} + +async function viewQueueDetail(queueId) { + const modal = document.getElementById('queueHistoryModal'); + if (!modal) return; + + // Show loading + modal.querySelector('.issue-modal-body').innerHTML = ` +
+ + ${t('common.loading') || 'Loading...'} +
+ `; + lucide.createIcons(); + + try { + const response = await fetch(`/api/queue/${queueId}?path=${encodeURIComponent(projectPath)}`); + const queue = await response.json(); + + if (queue.error) { + throw new Error(queue.error); + } + + const tasks = queue.tasks || []; + const metadata = queue._metadata || {}; + + // Group by execution_group + const grouped = {}; + tasks.forEach(task => { + const group = task.execution_group || 'ungrouped'; + if (!grouped[group]) grouped[group] = []; + grouped[group].push(task); + }); + + const detailHtml = ` +
+
+ +

${queue.id || queueId}

+
+ +
+
+ ${tasks.length} + ${t('issues.totalTasks') || 'Total'} +
+
+ ${tasks.filter(t => t.status === 'completed').length} + ${t('issues.completed') || 'Completed'} +
+
+ ${tasks.filter(t => t.status === 'pending').length} + ${t('issues.pending') || 'Pending'} +
+
+ ${tasks.filter(t => t.status === 'failed').length} + ${t('issues.failed') || 'Failed'} +
+
+ +
+ ${Object.entries(grouped).map(([groupId, items]) => ` +
+
+ + ${groupId} + (${items.length} tasks) +
+
+ ${items.map(item => ` +
+ ${item.item_id || item.task_id} + ${item.issue_id} + ${item.status || 'unknown'} +
+ `).join('')} +
+
+ `).join('')} +
+
+ `; + + modal.querySelector('.issue-modal-body').innerHTML = detailHtml; + lucide.createIcons(); + + } catch (err) { + console.error('Failed to load queue detail:', err); + modal.querySelector('.issue-modal-body').innerHTML = ` +
+ +
+ +

${t('errors.loadFailed') || 'Failed to load queue detail'}

+
+
+ `; + lucide.createIcons(); + } +} + function copyCommand(command) { navigator.clipboard.writeText(command).then(() => { showNotification(t('common.copied') || 'Copied to clipboard', 'success');