// ========================================== // ISSUE MANAGER VIEW // Manages issues, solutions, and execution queue // ========================================== // ========== Issue State ========== var issueData = { issues: [], queue: { queue: [], conflicts: [], execution_groups: [], grouped_items: {} }, selectedIssue: null, selectedSolution: null, selectedSolutionIssueId: null, statusFilter: 'all', searchQuery: '', viewMode: 'issues' // 'issues' | 'queue' }; var issueLoading = false; var issueDragState = { dragging: null, groupId: null }; // ========== Main Render Function ========== async function renderIssueManager() { const container = document.getElementById('mainContent'); if (!container) return; // Hide stats grid and search hideStatsAndCarousel(); // Show loading state container.innerHTML = '
' + '
' + '

' + t('common.loading') + '

' + '
'; // Load data await Promise.all([loadIssueData(), loadQueueData()]); // Render the main view renderIssueView(); } // ========== Data Loading ========== async function loadIssueData() { issueLoading = true; try { const response = await fetch('/api/issues?path=' + encodeURIComponent(projectPath)); if (!response.ok) throw new Error('Failed to load issues'); const data = await response.json(); issueData.issues = data.issues || []; updateIssueBadge(); } catch (err) { console.error('Failed to load issues:', err); issueData.issues = []; } finally { issueLoading = false; } } async function loadQueueData() { try { const response = await fetch('/api/queue?path=' + encodeURIComponent(projectPath)); if (!response.ok) throw new Error('Failed to load queue'); issueData.queue = await response.json(); } catch (err) { console.error('Failed to load queue:', err); issueData.queue = { queue: [], conflicts: [], execution_groups: [], grouped_items: {} }; } } async function loadIssueDetail(issueId) { try { const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath)); if (!response.ok) throw new Error('Failed to load issue detail'); return await response.json(); } catch (err) { console.error('Failed to load issue detail:', err); return null; } } function updateIssueBadge() { const badge = document.getElementById('badgeIssues'); if (badge) { badge.textContent = issueData.issues.length; } } // ========== Main View Render ========== function renderIssueView() { const container = document.getElementById('mainContent'); if (!container) return; const issues = issueData.issues || []; // Apply both status and search filters let filteredIssues = issueData.statusFilter === 'all' ? issues : issues.filter(i => i.status === issueData.statusFilter); if (issueData.searchQuery) { const query = issueData.searchQuery.toLowerCase(); filteredIssues = filteredIssues.filter(i => i.id.toLowerCase().includes(query) || (i.title && i.title.toLowerCase().includes(query)) || (i.context && i.context.toLowerCase().includes(query)) ); } container.innerHTML = `

${t('issues.title') || 'Issue Manager'}

${t('issues.description') || 'Manage issues, solutions, and execution queue'}

${issueData.viewMode === 'issues' ? renderIssueListSection(filteredIssues) : renderQueueSection()}
`; lucide.createIcons(); // Initialize drag-drop if in queue view if (issueData.viewMode === 'queue') { initQueueDragDrop(); } } function switchIssueView(mode) { issueData.viewMode = mode; renderIssueView(); } // ========== Issue List Section ========== function renderIssueListSection(issues) { const statuses = ['all', 'registered', 'planning', 'planned', 'queued', 'executing', 'completed', 'failed']; const totalIssues = issueData.issues?.length || 0; return `
${t('issues.filterStatus') || 'Status'}: ${statuses.map(status => ` `).join('')}
${t('issues.showing') || 'Showing'} ${issues.length} ${t('issues.of') || 'of'} ${totalIssues} ${t('issues.issues') || 'issues'}
${issues.length === 0 ? `

${t('issues.noIssues') || 'No issues found'}

${issueData.searchQuery || issueData.statusFilter !== 'all' ? (t('issues.tryDifferentFilter') || 'Try adjusting your search or filters') : (t('issues.createHint') || 'Click "Create" to add your first issue')}

${!issueData.searchQuery && issueData.statusFilter === 'all' ? ` ` : ''}
` : issues.map(issue => renderIssueCard(issue)).join('')}
`; } function renderIssueCard(issue) { const statusColors = { registered: 'registered', planning: 'planning', planned: 'planned', queued: 'queued', executing: 'executing', completed: 'completed', failed: 'failed' }; return `
${issue.id} ${issue.status || 'unknown'}
${renderPriorityStars(issue.priority || 3)}

${issue.title || issue.id}

${issue.task_count || 0} ${t('issues.tasks') || 'tasks'} ${issue.solution_count || 0} ${t('issues.solutions') || 'solutions'} ${issue.bound_solution_id ? ` ${t('issues.boundSolution') || 'Bound'} ` : ''}
`; } function renderPriorityStars(priority) { const maxStars = 5; let stars = ''; for (let i = 1; i <= maxStars; i++) { stars += ``; } return stars; } function filterIssuesByStatus(status) { issueData.statusFilter = status; renderIssueView(); } // ========== Queue Section ========== function renderQueueSection() { const queue = issueData.queue; const queueItems = queue.queue || []; const metadata = queue._metadata || {}; // Check if queue is empty if (queueItems.length === 0) { return `

${t('issues.queueEmpty') || 'Queue is empty'}

${t('issues.queueEmptyHint') || 'Generate execution queue from bound solutions'}

`; } // Group items by execution_group or treat all as single group const groups = queue.execution_groups || []; let groupedItems = queue.grouped_items || {}; // If no execution_groups, create a default grouping from queue items if (groups.length === 0 && queueItems.length > 0) { const groupMap = {}; queueItems.forEach(item => { const groupId = item.execution_group || 'default'; if (!groupMap[groupId]) { groupMap[groupId] = []; } groupMap[groupId].push(item); }); // Create synthetic groups const syntheticGroups = Object.keys(groupMap).map(groupId => ({ id: groupId, type: 'sequential', task_count: groupMap[groupId].length })); return `
${t('issues.queueId') || 'Queue ID'} ${queue.id || 'N/A'}
${t('issues.status') || 'Status'} ${queue.status || 'unknown'}
${t('issues.issues') || 'Issues'} ${(queue.issue_ids || []).join(', ') || 'N/A'}
${metadata.total_tasks || queueItems.length} ${t('issues.totalTasks') || 'Total'}
${metadata.pending_count || queueItems.filter(i => i.status === 'pending').length} ${t('issues.pending') || 'Pending'}
${metadata.executing_count || queueItems.filter(i => i.status === 'executing').length} ${t('issues.executing') || 'Executing'}
${metadata.completed_count || queueItems.filter(i => i.status === 'completed').length} ${t('issues.completed') || 'Completed'}
${metadata.failed_count || queueItems.filter(i => i.status === 'failed').length} ${t('issues.failed') || 'Failed'}
${syntheticGroups.map(group => renderQueueGroup(group, groupMap[group.id] || [])).join('')}
${queue.conflicts && queue.conflicts.length > 0 ? renderConflictsSection(queue.conflicts) : ''} `; } return `
${groups.length} ${t('issues.executionGroups') || 'groups'} · ${queueItems.length} ${t('issues.totalItems') || 'items'}

${t('issues.reorderHint') || 'Drag items within a group to reorder'}

${groups.map(group => renderQueueGroup(group, groupedItems[group.id] || [])).join('')}
${queue.conflicts && queue.conflicts.length > 0 ? renderConflictsSection(queue.conflicts) : ''} `; } function renderQueueGroup(group, items) { const isParallel = group.type === 'parallel'; return `
${group.id} (${isParallel ? t('issues.parallelGroup') || 'Parallel' : t('issues.sequentialGroup') || 'Sequential'})
${group.task_count} tasks
${items.map((item, idx) => renderQueueItem(item, idx, items.length)).join('')}
`; } function renderQueueItem(item, index, total) { const statusColors = { pending: '', ready: 'ready', executing: 'executing', completed: 'completed', failed: 'failed', blocked: 'blocked' }; return `
${item.queue_id} ${item.issue_id} ${item.task_id} ${item.depends_on && item.depends_on.length > 0 ? ` ` : ''}
`; } function renderConflictsSection(conflicts) { return `

Conflicts (${conflicts.length})

${conflicts.map(c => `
${c.file} ${c.tasks.join(' → ')} ${c.resolved ? 'Resolved' : 'Pending'}
`).join('')}
`; } // ========== Drag-Drop for Queue ========== function initQueueDragDrop() { const items = document.querySelectorAll('.queue-item[draggable="true"]'); items.forEach(item => { item.addEventListener('dragstart', handleIssueDragStart); item.addEventListener('dragend', handleIssueDragEnd); item.addEventListener('dragover', handleIssueDragOver); item.addEventListener('drop', handleIssueDrop); }); } function handleIssueDragStart(e) { const item = e.target.closest('.queue-item'); if (!item) return; issueDragState.dragging = item.dataset.queueId; issueDragState.groupId = item.dataset.groupId; item.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', item.dataset.queueId); } function handleIssueDragEnd(e) { const item = e.target.closest('.queue-item'); if (item) { item.classList.remove('dragging'); } issueDragState.dragging = null; issueDragState.groupId = null; // Remove all placeholders document.querySelectorAll('.queue-drop-placeholder').forEach(p => p.remove()); } function handleIssueDragOver(e) { e.preventDefault(); const target = e.target.closest('.queue-item'); if (!target || target.dataset.queueId === issueDragState.dragging) return; // Only allow drag within same group if (target.dataset.groupId !== issueDragState.groupId) { e.dataTransfer.dropEffect = 'none'; return; } e.dataTransfer.dropEffect = 'move'; } function handleIssueDrop(e) { e.preventDefault(); const target = e.target.closest('.queue-item'); if (!target || !issueDragState.dragging) return; // Only allow drop within same group if (target.dataset.groupId !== issueDragState.groupId) return; const container = target.closest('.queue-items'); if (!container) return; // Get new order const items = Array.from(container.querySelectorAll('.queue-item')); const draggedItem = items.find(i => i.dataset.queueId === issueDragState.dragging); const targetIndex = items.indexOf(target); const draggedIndex = items.indexOf(draggedItem); if (draggedIndex === targetIndex) return; // Reorder in DOM if (draggedIndex < targetIndex) { target.after(draggedItem); } else { target.before(draggedItem); } // Get new order and save const newOrder = Array.from(container.querySelectorAll('.queue-item')).map(i => i.dataset.queueId); saveQueueOrder(issueDragState.groupId, newOrder); } async function saveQueueOrder(groupId, newOrder) { try { const response = await fetch('/api/queue/reorder?path=' + encodeURIComponent(projectPath), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ groupId, newOrder }) }); if (!response.ok) { throw new Error('Failed to save queue order'); } const result = await response.json(); if (result.error) { showNotification(result.error, 'error'); } else { showNotification('Queue reordered', 'success'); // Reload queue data await loadQueueData(); } } catch (err) { console.error('Failed to save queue order:', err); showNotification('Failed to save queue order', 'error'); // Reload to restore original order await loadQueueData(); renderIssueView(); } } // ========== Detail Panel ========== async function openIssueDetail(issueId) { const panel = document.getElementById('issueDetailPanel'); if (!panel) return; panel.innerHTML = '
'; panel.classList.remove('hidden'); lucide.createIcons(); const detail = await loadIssueDetail(issueId); if (!detail) { panel.innerHTML = '
Failed to load issue
'; return; } issueData.selectedIssue = detail; renderIssueDetailPanel(detail); } function renderIssueDetailPanel(issue) { const panel = document.getElementById('issueDetailPanel'); if (!panel) return; const boundSolution = issue.solutions?.find(s => s.is_bound); panel.innerHTML = `

${issue.id}

${issue.status || 'unknown'}
${issue.title || issue.id}
${issue.context || 'No context'}
${(issue.solutions || []).length > 0 ? (issue.solutions || []).map(sol => `
${sol.id} ${sol.is_bound ? '' + (t('issues.bound') || 'Bound') + '' : ''} ${sol.tasks?.length || 0} ${t('issues.tasks') || 'tasks'}
`).join('') : '

' + (t('issues.noSolutions') || 'No solutions') + '

'}
${(issue.tasks || []).length > 0 ? (issue.tasks || []).map(task => `
${task.id}

${task.title || task.description || ''}

`).join('') : '

' + (t('issues.noTasks') || 'No tasks') + '

'}
`; lucide.createIcons(); } function closeIssueDetail() { const panel = document.getElementById('issueDetailPanel'); if (panel) { panel.classList.add('hidden'); } issueData.selectedIssue = null; } function toggleSolutionExpand(solId) { const el = document.getElementById('solution-' + solId); if (el) { el.classList.toggle('hidden'); } } // ========== Solution Detail Modal ========== function openSolutionDetail(issueId, solutionId) { const issue = issueData.selectedIssue || issueData.issues.find(i => i.id === issueId); if (!issue) return; const solution = issue.solutions?.find(s => s.id === solutionId); if (!solution) return; issueData.selectedSolution = solution; issueData.selectedSolutionIssueId = issueId; const modal = document.getElementById('solutionDetailModal'); if (modal) { modal.classList.remove('hidden'); renderSolutionDetail(solution); lucide.createIcons(); } } function closeSolutionDetail() { const modal = document.getElementById('solutionDetailModal'); if (modal) { modal.classList.add('hidden'); } issueData.selectedSolution = null; issueData.selectedSolutionIssueId = null; } function renderSolutionDetail(solution) { const idEl = document.getElementById('solutionDetailId'); const bodyEl = document.getElementById('solutionDetailBody'); const bindBtn = document.getElementById('solutionBindBtn'); if (idEl) { idEl.textContent = solution.id; } // Update bind button state if (bindBtn) { if (solution.is_bound) { bindBtn.innerHTML = `${t('issues.unbind') || 'Unbind'}`; bindBtn.classList.remove('btn-secondary'); bindBtn.classList.add('btn-primary'); } else { bindBtn.innerHTML = `${t('issues.bind') || 'Bind'}`; bindBtn.classList.remove('btn-primary'); bindBtn.classList.add('btn-secondary'); } } if (!bodyEl) return; const tasks = solution.tasks || []; bodyEl.innerHTML = `
${tasks.length} ${t('issues.totalTasks') || 'Total Tasks'}
${solution.is_bound ? '✓' : '—'} ${t('issues.bindStatus') || 'Bind Status'}
${solution.created_at ? new Date(solution.created_at).toLocaleDateString() : '—'} ${t('issues.createdAt') || 'Created'}

${t('issues.taskList') || 'Task List'}

${tasks.length === 0 ? `

${t('issues.noTasks') || 'No tasks in this solution'}

` : tasks.map((task, index) => renderSolutionTask(task, index)).join('')}
`; lucide.createIcons(); } function renderSolutionTask(task, index) { const actionClass = (task.action || 'unknown').toLowerCase(); const modPoints = task.modification_points || []; // Support both old and new field names const implSteps = task.implementation || task.implementation_steps || []; const acceptance = task.acceptance || task.acceptance_criteria || []; const testInfo = task.test || {}; const regression = task.regression || []; const commitInfo = task.commit || {}; const dependsOn = task.depends_on || task.dependencies || []; // Handle acceptance as object or array const acceptanceCriteria = Array.isArray(acceptance) ? acceptance : (acceptance.criteria || []); const acceptanceVerification = acceptance.verification || []; return `
#${index + 1} ${task.id || ''} ${task.action || 'Unknown'}
${task.title || task.description || 'No title'}
`; } function toggleTaskExpand(index) { const details = document.getElementById('taskDetails' + index); const icon = document.getElementById('taskExpandIcon' + index); if (details) { details.classList.toggle('hidden'); } if (icon) { icon.style.transform = details?.classList.contains('hidden') ? '' : 'rotate(180deg)'; } } function toggleSolutionJson() { const content = document.getElementById('solutionJsonContent'); if (content) { content.classList.toggle('hidden'); } } async function toggleSolutionBind() { const solution = issueData.selectedSolution; const issueId = issueData.selectedSolutionIssueId; if (!solution || !issueId) return; const action = solution.is_bound ? 'unbind' : 'bind'; try { const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath), { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ bound_solution_id: action === 'bind' ? solution.id : null }) }); if (!response.ok) throw new Error('Failed to ' + action); showNotification(action === 'bind' ? (t('issues.solutionBound') || 'Solution bound') : (t('issues.solutionUnbound') || 'Solution unbound'), 'success'); // Refresh data await loadIssueData(); const detail = await loadIssueDetail(issueId); if (detail) { issueData.selectedIssue = detail; // Update solution reference const updatedSolution = detail.solutions?.find(s => s.id === solution.id); if (updatedSolution) { issueData.selectedSolution = updatedSolution; renderSolutionDetail(updatedSolution); } renderIssueDetailPanel(detail); } } catch (err) { console.error('Failed to ' + action + ' solution:', err); showNotification('Failed to ' + action + ' solution', 'error'); } } // Helper: escape HTML function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function openQueueItemDetail(queueId) { const item = issueData.queue.queue?.find(q => q.queue_id === queueId); if (item) { openIssueDetail(item.issue_id); } } // ========== Edit Functions ========== function startEditField(issueId, field, currentValue) { const container = document.getElementById('issueTitle'); if (!container) return; container.innerHTML = `
`; lucide.createIcons(); document.getElementById('editField')?.focus(); } function startEditContext(issueId) { const container = document.getElementById('issueContext'); const currentValue = issueData.selectedIssue?.context || ''; if (!container) return; container.innerHTML = `
`; lucide.createIcons(); document.getElementById('editContext')?.focus(); } async function saveFieldEdit(issueId, field) { const input = document.getElementById('editField'); if (!input) return; const value = input.value.trim(); if (!value) return; try { const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath), { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ [field]: value }) }); if (!response.ok) throw new Error('Failed to update'); showNotification('Updated ' + field, 'success'); // Refresh data await loadIssueData(); const detail = await loadIssueDetail(issueId); if (detail) { issueData.selectedIssue = detail; renderIssueDetailPanel(detail); } } catch (err) { showNotification('Failed to update', 'error'); cancelEdit(); } } async function saveContextEdit(issueId) { const textarea = document.getElementById('editContext'); if (!textarea) return; const value = textarea.value; try { const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath), { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ context: value }) }); if (!response.ok) throw new Error('Failed to update'); showNotification('Context updated', 'success'); // Refresh detail const detail = await loadIssueDetail(issueId); if (detail) { issueData.selectedIssue = detail; renderIssueDetailPanel(detail); } } catch (err) { showNotification('Failed to update context', 'error'); cancelEdit(); } } function cancelEdit() { if (issueData.selectedIssue) { renderIssueDetailPanel(issueData.selectedIssue); } } async function updateTaskStatus(issueId, taskId, status) { try { const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '/tasks/' + encodeURIComponent(taskId) + '?path=' + encodeURIComponent(projectPath), { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); if (!response.ok) throw new Error('Failed to update task'); showNotification('Task status updated', 'success'); } catch (err) { showNotification('Failed to update task status', 'error'); } } // ========== Search Functions ========== function handleIssueSearch(value) { issueData.searchQuery = value; renderIssueView(); } function clearIssueSearch() { issueData.searchQuery = ''; renderIssueView(); } // ========== Create Issue Modal ========== function generateIssueId() { // Generate unique ID: ISSUE-YYYYMMDD-XXX format const now = new Date(); const dateStr = now.getFullYear().toString() + String(now.getMonth() + 1).padStart(2, '0') + String(now.getDate()).padStart(2, '0'); // Find existing IDs with same date prefix const prefix = 'ISSUE-' + dateStr + '-'; const existingIds = (issueData.issues || []) .map(i => i.id) .filter(id => id.startsWith(prefix)); // Get next sequence number let maxSeq = 0; existingIds.forEach(id => { const seqStr = id.replace(prefix, ''); const seq = parseInt(seqStr, 10); if (!isNaN(seq) && seq > maxSeq) { maxSeq = seq; } }); return prefix + String(maxSeq + 1).padStart(3, '0'); } function showCreateIssueModal() { const modal = document.getElementById('createIssueModal'); if (modal) { modal.classList.remove('hidden'); // Auto-generate issue ID const idInput = document.getElementById('newIssueId'); if (idInput) { idInput.value = generateIssueId(); } lucide.createIcons(); // Focus on title input instead of ID setTimeout(() => { document.getElementById('newIssueTitle')?.focus(); }, 100); } } function regenerateIssueId() { const idInput = document.getElementById('newIssueId'); if (idInput) { idInput.value = generateIssueId(); } } function hideCreateIssueModal() { const modal = document.getElementById('createIssueModal'); if (modal) { modal.classList.add('hidden'); // Clear form const idInput = document.getElementById('newIssueId'); const titleInput = document.getElementById('newIssueTitle'); const contextInput = document.getElementById('newIssueContext'); const prioritySelect = document.getElementById('newIssuePriority'); if (idInput) idInput.value = ''; if (titleInput) titleInput.value = ''; if (contextInput) contextInput.value = ''; if (prioritySelect) prioritySelect.value = '3'; } } async function createIssue() { const idInput = document.getElementById('newIssueId'); const titleInput = document.getElementById('newIssueTitle'); const contextInput = document.getElementById('newIssueContext'); const prioritySelect = document.getElementById('newIssuePriority'); const issueId = idInput?.value?.trim(); const title = titleInput?.value?.trim(); const context = contextInput?.value?.trim(); const priority = parseInt(prioritySelect?.value || '3'); if (!issueId) { showNotification(t('issues.idRequired') || 'Issue ID is required', 'error'); idInput?.focus(); return; } if (!title) { showNotification(t('issues.titleRequired') || 'Title is required', 'error'); titleInput?.focus(); return; } try { const response = await fetch('/api/issues?path=' + encodeURIComponent(projectPath), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: issueId, title: title, context: context, priority: priority, source: 'dashboard' }) }); const result = await response.json(); if (!response.ok || result.error) { showNotification(result.error || 'Failed to create issue', 'error'); return; } showNotification(t('issues.created') || 'Issue created successfully', 'success'); hideCreateIssueModal(); // Reload data and refresh view await loadIssueData(); renderIssueView(); } catch (err) { console.error('Failed to create issue:', err); showNotification('Failed to create issue', 'error'); } } // ========== Delete Issue ========== async function deleteIssue(issueId) { if (!confirm(t('issues.confirmDelete') || 'Are you sure you want to delete this issue?')) { return; } try { const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath), { method: 'DELETE' }); if (!response.ok) throw new Error('Failed to delete'); showNotification(t('issues.deleted') || 'Issue deleted', 'success'); closeIssueDetail(); // Reload data and refresh view await loadIssueData(); renderIssueView(); } catch (err) { showNotification('Failed to delete issue', 'error'); } } // ========== Queue Operations ========== async function refreshQueue() { try { await loadQueueData(); renderIssueView(); showNotification(t('issues.queueRefreshed') || 'Queue refreshed', 'success'); } catch (err) { showNotification('Failed to refresh queue', 'error'); } } function createExecutionQueue() { showQueueCommandModal(); } function showQueueCommandModal() { // Create modal if not exists let modal = document.getElementById('queueCommandModal'); if (!modal) { modal = document.createElement('div'); modal.id = 'queueCommandModal'; modal.className = 'issue-modal'; document.body.appendChild(modal); } const command = 'claude /issue:queue'; const altCommand = 'ccw issue queue'; modal.innerHTML = `

${t('issues.createQueue') || 'Create Execution Queue'}

${t('issues.queueCommandHint') || 'Run one of the following commands in your terminal to generate the execution queue from bound solutions:'}

${command}
${altCommand}

${t('issues.queueCommandInfo') || 'After running the command, click "Refresh" to see the updated queue.'}

`; modal.classList.remove('hidden'); lucide.createIcons(); } function hideQueueCommandModal() { const modal = document.getElementById('queueCommandModal'); if (modal) { modal.classList.add('hidden'); } } function copyCommand(command) { navigator.clipboard.writeText(command).then(() => { showNotification(t('common.copied') || 'Copied to clipboard', 'success'); }).catch(err => { console.error('Failed to copy:', err); // Fallback: select text const textArea = document.createElement('textarea'); textArea.value = command; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); showNotification(t('common.copied') || 'Copied to clipboard', 'success'); }); }