// ==========================================
// ISSUE MANAGER VIEW
// Manages issues, solutions, and execution queue
// ==========================================
// ========== Issue State ==========
var issueData = {
issues: [],
historyIssues: [], // Archived/completed issues from history
queue: { tasks: [], solutions: [], conflicts: [], execution_groups: [], grouped_items: {} },
selectedIssue: null,
selectedSolution: null,
selectedSolutionIssueId: null,
statusFilter: 'all',
searchQuery: '',
viewMode: 'issues', // 'issues' | 'queue'
// Search suggestions state
searchSuggestions: [],
showSuggestions: false,
selectedSuggestion: -1
};
var issueLoading = false;
var issueDragState = {
dragging: null,
groupId: null
};
// Multi-queue state
var queueData = {
queues: [], // All queue index entries
activeQueueId: null, // Currently active queue
expandedQueueId: null // Queue showing execution groups
};
// ========== 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(), loadAllQueues()]);
// 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 loadIssueHistory() {
try {
const response = await fetch('/api/issues/history?path=' + encodeURIComponent(projectPath));
if (!response.ok) throw new Error('Failed to load issue history');
const data = await response.json();
issueData.historyIssues = data.issues || [];
} catch (err) {
console.error('Failed to load issue history:', err);
issueData.historyIssues = [];
}
}
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 = { tasks: [], solutions: [], conflicts: [], execution_groups: [], grouped_items: {} };
}
}
async function loadAllQueues() {
try {
const response = await fetch('/api/queue/history?path=' + encodeURIComponent(projectPath));
if (!response.ok) throw new Error('Failed to load queue history');
const data = await response.json();
queueData.queues = data.queues || [];
queueData.activeQueueId = data.active_queue_id;
} catch (err) {
console.error('Failed to load all queues:', err);
queueData.queues = [];
queueData.activeQueueId = null;
}
}
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 || [];
const historyIssues = issueData.historyIssues || [];
// Apply both status and search filters
let filteredIssues;
if (issueData.statusFilter === 'all') {
filteredIssues = issues;
} else if (issueData.statusFilter === 'completed') {
// For 'completed' filter, include both current completed issues and archived history issues
const currentCompleted = issues.filter(i => i.status === 'completed');
// Mark history issues as archived for visual distinction
const archivedIssues = historyIssues.map(i => ({ ...i, _isArchived: true }));
filteredIssues = [...currentCompleted, ...archivedIssues];
} else {
filteredIssues = issues.filter(i => i.status === issueData.statusFilter);
}
if (issueData.searchQuery) {
const query = issueData.searchQuery.toLowerCase();
filteredIssues = filteredIssues.filter(i => {
// Basic field search
const basicMatch =
i.id.toLowerCase().includes(query) ||
(i.title && i.title.toLowerCase().includes(query)) ||
(i.context && i.context.toLowerCase().includes(query));
if (basicMatch) return true;
// Search in solutions
if (i.solutions && i.solutions.length > 0) {
return i.solutions.some(sol =>
(sol.description && sol.description.toLowerCase().includes(query)) ||
(sol.approach && sol.approach.toLowerCase().includes(query))
);
}
return false;
});
}
container.innerHTML = `
${issueData.viewMode === 'issues' ? renderIssueListSection(filteredIssues) : renderQueueSection()}
${t('issues.issueTitle') || 'Title'}
${t('issues.issueContext') || 'Context'} (${t('common.optional') || 'optional'})
${t('issues.issuePriority') || 'Priority'}
1 - ${t('issues.priorityLowest') || 'Lowest'}
2 - ${t('issues.priorityLow') || 'Low'}
3 - ${t('issues.priorityMedium') || 'Medium'}
4 - ${t('issues.priorityHigh') || 'High'}
5 - ${t('issues.priorityCritical') || 'Critical'}
`;
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.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' ? `
${t('issues.createFirst') || 'Create First Issue'}
` : ''}
` : 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'
};
const isArchived = issue._isArchived;
return `
${highlightMatch(issue.id, issueData.searchQuery)}
${issue.status || 'unknown'}
${isArchived ? '' + (t('issues.archived') || 'Archived') + ' ' : ''}
${renderPriorityStars(issue.priority || 3)}
${highlightMatch(issue.title || issue.id, issueData.searchQuery)}
`;
}
function renderPriorityStars(priority) {
const maxStars = 5;
let stars = '';
for (let i = 1; i <= maxStars; i++) {
stars += ` `;
}
return stars;
}
async function filterIssuesByStatus(status) {
issueData.statusFilter = status;
// Load history data when filtering by 'completed' status
if (status === 'completed' && issueData.historyIssues.length === 0) {
await loadIssueHistory();
}
renderIssueView();
}
// ========== Queue Section ==========
function renderQueueSection() {
const queues = queueData.queues || [];
const activeQueueId = queueData.activeQueueId;
const expandedQueueId = queueData.expandedQueueId;
// If a queue is expanded, show loading then load detail
if (expandedQueueId) {
// Show loading state first, then load async
setTimeout(() => loadAndRenderExpandedQueue(expandedQueueId), 0);
return `
${t('common.loading') || 'Loading...'}
`;
}
// Show multi-queue cards view
return `
${queues.length === 0 ? `
${t('issues.noQueues') || 'No queues found'}
${t('issues.queueEmptyHint') || 'Generate execution queue from bound solutions'}
${t('issues.createQueue') || 'Create Queue'}
` : `
${queues.map(q => renderQueueCard(q, q.id === activeQueueId)).join('')}
`}
`;
}
function renderQueueCard(queue, isActive) {
const itemCount = queue.total_solutions || queue.total_tasks || 0;
const completedCount = queue.completed_solutions || queue.completed_tasks || 0;
const progressPercent = itemCount > 0 ? Math.round((completedCount / itemCount) * 100) : 0;
const issueCount = queue.issue_ids?.length || 0;
const statusClass = queue.status === 'merged' ? 'merged' : queue.status || '';
const safeQueueId = escapeHtml(queue.id || '');
return `
${completedCount}/${itemCount} ${queue.total_solutions ? 'solutions' : 'tasks'}
${progressPercent}%
${issueCount} issues
${queue.created_at ? new Date(queue.created_at).toLocaleDateString() : 'N/A'}
${!isActive && queue.status !== 'merged' ? `
` : ''}
${queue.status !== 'merged' ? `
` : ''}
`;
}
function toggleQueueExpand(queueId) {
if (queueData.expandedQueueId === queueId) {
queueData.expandedQueueId = null;
} else {
queueData.expandedQueueId = queueId;
}
renderIssueView();
}
async function activateQueue(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.queueActivated') || 'Queue activated: ' + queueId, 'success');
await Promise.all([loadQueueData(), loadAllQueues()]);
renderIssueView();
} else {
showNotification(result.error || 'Failed to activate queue', 'error');
}
} catch (err) {
console.error('Failed to activate queue:', err);
showNotification('Failed to activate queue', 'error');
}
}
async function deactivateQueue(queueId) {
try {
const response = await fetch('/api/queue/deactivate?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.queueDeactivated') || 'Queue deactivated', 'success');
queueData.activeQueueId = null;
await Promise.all([loadQueueData(), loadAllQueues()]);
renderIssueView();
} else {
showNotification(result.error || 'Failed to deactivate queue', 'error');
}
} catch (err) {
console.error('Failed to deactivate queue:', err);
showNotification('Failed to deactivate queue', 'error');
}
}
async function renderExpandedQueueView(queueId) {
const safeQueueId = escapeHtml(queueId || '');
// Fetch queue detail
let queue;
try {
const response = await fetch('/api/queue/' + encodeURIComponent(queueId) + '?path=' + encodeURIComponent(projectPath));
queue = await response.json();
if (queue.error) throw new Error(queue.error);
} catch (err) {
return `
Back
Failed to load queue: ${escapeHtml(err.message)}
`;
}
const queueItems = queue.solutions || queue.tasks || [];
const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0);
const metadata = queue._metadata || {};
const isActive = queueId === queueData.activeQueueId;
// Group items by execution_group
const groupMap = {};
queueItems.forEach(item => {
const groupId = item.execution_group || 'default';
if (!groupMap[groupId]) groupMap[groupId] = [];
groupMap[groupId].push(item);
});
const groups = queue.execution_groups || Object.keys(groupMap).map(groupId => ({
id: groupId,
type: groupId.startsWith('P') ? 'parallel' : 'sequential',
solution_count: groupMap[groupId]?.length || 0
}));
const groupedItems = queue.grouped_items || groupMap;
return `
${isSolutionLevel ? (metadata.total_solutions || queueItems.length) : (metadata.total_tasks || queueItems.length)}
${isSolutionLevel ? 'Solutions' : 'Tasks'}
${metadata.pending_count || queueItems.filter(i => i.status === 'pending').length}
Pending
${metadata.executing_count || queueItems.filter(i => i.status === 'executing').length}
Executing
${isSolutionLevel ? (metadata.completed_solutions || 0) : (metadata.completed_tasks || queueItems.filter(i => i.status === 'completed').length)}
Completed
${metadata.failed_count || queueItems.filter(i => i.status === 'failed').length}
Failed
${t('issues.reorderHint') || 'Drag items within a group to reorder. Click item to view details.'}
${groups.map(group => renderQueueGroupWithDelete(group, groupedItems[group.id] || groupMap[group.id] || [], queueId)).join('')}
${queue.conflicts && queue.conflicts.length > 0 ? renderConflictsSection(queue.conflicts) : ''}
`;
}
// Async loader for expanded queue view - renders into DOM container
async function loadAndRenderExpandedQueue(queueId) {
const wrapper = document.getElementById('queueExpandedWrapper');
if (!wrapper) return;
try {
const html = await renderExpandedQueueView(queueId);
wrapper.innerHTML = html;
// Re-init icons and drag-drop after DOM update
if (window.lucide) {
window.lucide.createIcons();
}
// Initialize drag-drop for queue items
initQueueDragDrop();
} catch (err) {
console.error('Failed to load expanded queue:', err);
wrapper.innerHTML = `
Failed to load queue: ${escapeHtml(err.message || 'Unknown error')}
Back
`;
if (window.lucide) {
window.lucide.createIcons();
}
}
}
function renderQueueGroupWithDelete(group, items, queueId) {
const isParallel = group.type === 'parallel';
const itemCount = group.solution_count || group.task_count || items.length;
const itemLabel = group.solution_count ? 'solutions' : 'tasks';
return `
${items.map((item, idx) => renderQueueItemWithDelete(item, idx, items.length, queueId)).join('')}
`;
}
function renderQueueItemWithDelete(item, index, total, queueId) {
const statusColors = {
pending: '',
ready: 'ready',
executing: 'executing',
completed: 'completed',
failed: 'failed',
blocked: 'blocked'
};
const isSolutionItem = item.task_count !== undefined;
const safeItemId = escapeHtml(item.item_id || '');
const safeIssueId = escapeHtml(item.issue_id || '');
const safeQueueId = escapeHtml(queueId || '');
const safeSolutionId = escapeHtml(item.solution_id || '');
const safeTaskId = escapeHtml(item.task_id || '-');
const safeFilesTouched = item.files_touched ? escapeHtml(item.files_touched.join(', ')) : '';
const safeDependsOn = item.depends_on ? escapeHtml(item.depends_on.join(', ')) : '';
return `
${safeItemId}
${safeIssueId}
${isSolutionItem ? `
${item.task_count} tasks
${item.files_touched && item.files_touched.length > 0 ? `
${item.files_touched.length}
` : ''}
` : `
${safeTaskId}
`}
${item.depends_on && item.depends_on.length > 0 ? `
` : ''}
`;
}
async function deleteQueueItem(queueId, itemId) {
if (!confirm('Delete this item from queue?')) return;
try {
const response = await fetch('/api/queue/' + queueId + '/item/' + encodeURIComponent(itemId) + '?path=' + encodeURIComponent(projectPath), {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showNotification('Item deleted from queue', 'success');
await Promise.all([loadQueueData(), loadAllQueues()]);
renderIssueView();
} else {
showNotification(result.error || 'Failed to delete item', 'error');
}
} catch (err) {
console.error('Failed to delete queue item:', err);
showNotification('Failed to delete item', 'error');
}
}
async function refreshExpandedQueue(queueId) {
await Promise.all([loadQueueData(), loadAllQueues()]);
renderIssueView();
}
// ========== Queue Merge Modal ==========
function showMergeQueueModal(sourceQueueId) {
let modal = document.getElementById('mergeQueueModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'mergeQueueModal';
modal.className = 'issue-modal';
document.body.appendChild(modal);
}
const otherQueues = queueData.queues.filter(q =>
q.id !== sourceQueueId && q.status !== 'merged'
);
const safeSourceId = escapeHtml(sourceQueueId || '');
modal.innerHTML = `
Merge ${safeSourceId} into another queue:
${otherQueues.length === 0 ? `
No other queues available for merging
` : `
Target Queue
${otherQueues.map(q => `
${escapeHtml(q.id)} (${q.total_solutions || q.total_tasks || 0} items)
`).join('')}
Items from source queue will be appended to target queue. Source queue will be marked as "merged".
`}
`;
modal.classList.remove('hidden');
lucide.createIcons();
}
function hideMergeQueueModal() {
const modal = document.getElementById('mergeQueueModal');
if (modal) {
modal.classList.add('hidden');
}
}
async function executeQueueMerge(sourceQueueId) {
const targetQueueId = document.getElementById('targetQueueSelect')?.value;
if (!targetQueueId) return;
try {
const response = await fetch('/api/queue/merge?path=' + encodeURIComponent(projectPath), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sourceQueueId, targetQueueId })
});
const result = await response.json();
if (result.success) {
showNotification('Merged ' + result.mergedItemCount + ' items into ' + targetQueueId, 'success');
hideMergeQueueModal();
queueData.expandedQueueId = null;
await Promise.all([loadQueueData(), loadAllQueues()]);
renderIssueView();
} else {
showNotification(result.error || 'Failed to merge queues', 'error');
}
} catch (err) {
console.error('Failed to merge queues:', err);
showNotification('Failed to merge queues', 'error');
}
}
// ========== Legacy Queue Render (for backward compatibility) ==========
function renderLegacyQueueSection() {
const queue = issueData.queue;
const queueItems = queue.solutions || queue.tasks || [];
const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0);
const metadata = queue._metadata || {};
if (queueItems.length === 0) {
return ``;
}
const groups = queue.execution_groups || [];
let groupedItems = queue.grouped_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);
});
const syntheticGroups = Object.keys(groupMap).map(groupId => ({
id: groupId,
type: 'sequential',
task_count: groupMap[groupId].length
}));
return `
${isSolutionLevel ? (metadata.total_solutions || queueItems.length) : (metadata.total_tasks || queueItems.length)}
${isSolutionLevel ? (t('issues.totalSolutions') || 'Solutions') : (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 `
${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';
// Support both solution-level (solution_count) and task-level (task_count)
const itemCount = group.solution_count || group.task_count || items.length;
const itemLabel = group.solution_count ? 'solutions' : 'tasks';
return `
${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'
};
// Check if this is a solution-level item (has task_count) or task-level (has task_id)
const isSolutionItem = item.task_count !== undefined;
return `
${item.item_id}
${item.issue_id}
${isSolutionItem ? `
${item.task_count} ${t('issues.tasks') || 'tasks'}
${item.files_touched && item.files_touched.length > 0 ? `
${item.files_touched.length}
` : ''}
` : `
${item.task_id || '-'}
`}
${item.depends_on && item.depends_on.length > 0 ? `
` : ''}
`;
}
function renderConflictsSection(conflicts) {
return `
Conflicts (${conflicts.length})
${conflicts.map(c => `
${c.file}
${(c.solutions || c.tasks || []).join(' → ')}
${c.rationale ? `
` : ''}
${c.resolved || c.resolution ? '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.itemId;
issueDragState.groupId = item.dataset.groupId;
item.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', item.dataset.itemId);
}
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.itemId === 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.itemId === 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.itemId);
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, isArchived = false) {
const panel = document.getElementById('issueDetailPanel');
if (!panel) return;
panel.innerHTML = '
';
panel.classList.remove('hidden');
lucide.createIcons();
let detail;
if (isArchived) {
// For archived issues, get detail from historyIssues (already loaded)
const historyIssue = issueData.historyIssues.find(i => i.id === issueId);
if (historyIssue) {
// Mark as archived and provide minimal detail structure
detail = {
...historyIssue,
_isArchived: true,
solutions: historyIssue.solutions || [],
tasks: historyIssue.tasks || []
};
}
} else {
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 = `
Title
${issue.title || issue.id}
Context
${issue.context || 'No context'}
${t('issues.solutions') || 'Solutions'} (${issue.solutions?.length || 0})
${(issue.solutions || []).length > 0 ? (issue.solutions || []).map(sol => `
`).join('') : '
' + (t('issues.noSolutions') || 'No solutions') + '
'}
${t('issues.tasks') || 'Tasks'} (${issue.tasks?.length || 0})
${(issue.tasks || []).length > 0 ? (issue.tasks || []).map(task => `
${task.id}
${['pending', 'ready', 'executing', 'completed', 'failed', 'blocked', 'paused', 'skipped'].map(s =>
`${s} `
).join('')}
${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('')}
${t('issues.viewJson') || 'View Raw JSON'}
${escapeHtml(JSON.stringify(solution, null, 2))}
`;
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 `
${task.title || task.description || 'No title'}
${task.scope ? `
${t('issues.scope') || 'Scope'}:
${task.scope}
` : ''}
${implSteps.length > 0 ? `
1
${t('issues.implementation') || 'Implementation'}
${implSteps.map(step => `${typeof step === 'string' ? step : step.description || JSON.stringify(step)} `).join('')}
` : ''}
${modPoints.length > 0 ? `
${t('issues.modificationPoints') || 'Modification Points'}
${modPoints.map(mp => `
${mp.file || mp}
${mp.target ? `→ ${mp.target} ` : ''}
${mp.change ? `${mp.change} ` : ''}
`).join('')}
` : ''}
${(testInfo.unit?.length > 0 || testInfo.commands?.length > 0) ? `
2
${t('issues.test') || 'Test'}
${testInfo.coverage_target ? `(${testInfo.coverage_target}% coverage) ` : ''}
${testInfo.unit?.length > 0 ? `
${t('issues.unitTests') || 'Unit Tests'}:
${testInfo.unit.map(t => `${t} `).join('')}
` : ''}
${testInfo.integration?.length > 0 ? `
${t('issues.integrationTests') || 'Integration'}:
${testInfo.integration.map(t => `${t} `).join('')}
` : ''}
${testInfo.commands?.length > 0 ? `
${t('issues.commands') || 'Commands'}:
${testInfo.commands.map(cmd => `${cmd}`).join('')}
` : ''}
` : ''}
${regression.length > 0 ? `
3
${t('issues.regression') || 'Regression'}
${regression.map(cmd => `${cmd}`).join('')}
` : ''}
${acceptanceCriteria.length > 0 ? `
4
${t('issues.acceptance') || 'Acceptance'}
${t('issues.criteria') || 'Criteria'}:
${acceptanceCriteria.map(ac => `${typeof ac === 'string' ? ac : ac.description || JSON.stringify(ac)} `).join('')}
${acceptanceVerification.length > 0 ? `
${t('issues.verification') || 'Verification'}:
${acceptanceVerification.map(v => `${v}`).join('')}
` : ''}
` : ''}
${commitInfo.type ? `
5
${t('issues.commit') || 'Commit'}
${commitInfo.type}
(${commitInfo.scope || 'core'})
${commitInfo.breaking ? 'BREAKING ' : ''}
${commitInfo.message_template ? `
${commitInfo.message_template}
` : ''}
` : ''}
${dependsOn.length > 0 ? `
${t('issues.dependencies') || 'Dependencies'}
${dependsOn.map(dep => `${dep} `).join('')}
` : ''}
`;
}
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;
}
// Helper: escape regex special characters
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Helper: highlight matching text in search results
function highlightMatch(text, query) {
if (!text || !query) return escapeHtml(text || '');
const escaped = escapeHtml(text);
const escapedQuery = escapeRegex(escapeHtml(query));
const regex = new RegExp(`(${escapedQuery})`, 'gi');
return escaped.replace(regex, '$1 ');
}
function openQueueItemDetail(itemId) {
// Support both solution-level and task-level queues
const items = issueData.queue.solutions || issueData.queue.tasks || [];
const item = items.find(q => q.item_id === itemId);
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 ==========
var searchDebounceTimer = null;
function handleIssueSearch(value) {
issueData.searchQuery = value;
// Update suggestions immediately (no debounce for dropdown)
updateSearchSuggestions(value);
issueData.showSuggestions = value.length > 0;
issueData.selectedSuggestion = -1;
updateSuggestionsDropdown();
// Clear previous timer
if (searchDebounceTimer) {
clearTimeout(searchDebounceTimer);
}
// 300ms debounce for full re-render to prevent freeze on rapid input
searchDebounceTimer = setTimeout(() => {
renderIssueView();
// Restore input focus and cursor position
const input = document.getElementById('issueSearchInput');
if (input) {
input.focus();
input.setSelectionRange(value.length, value.length);
}
}, 300);
}
function clearIssueSearch() {
if (searchDebounceTimer) {
clearTimeout(searchDebounceTimer);
}
issueData.searchQuery = '';
issueData.showSuggestions = false;
issueData.searchSuggestions = [];
issueData.selectedSuggestion = -1;
renderIssueView();
}
// Update search suggestions based on query
function updateSearchSuggestions(query) {
if (!query || query.length < 1) {
issueData.searchSuggestions = [];
return;
}
const q = query.toLowerCase();
const allIssues = [...issueData.issues, ...issueData.historyIssues];
// Find matching issues (max 6)
issueData.searchSuggestions = allIssues
.filter(issue => {
const idMatch = issue.id.toLowerCase().includes(q);
const titleMatch = issue.title && issue.title.toLowerCase().includes(q);
const contextMatch = issue.context && issue.context.toLowerCase().includes(q);
const solutionMatch = issue.solutions && issue.solutions.some(sol =>
(sol.description && sol.description.toLowerCase().includes(q)) ||
(sol.approach && sol.approach.toLowerCase().includes(q))
);
return idMatch || titleMatch || contextMatch || solutionMatch;
})
.slice(0, 6);
}
// Render search suggestions dropdown
function renderSearchSuggestions() {
if (!issueData.searchSuggestions || issueData.searchSuggestions.length === 0) {
return '';
}
return issueData.searchSuggestions.map((issue, index) => `
${highlightMatch(issue.id, issueData.searchQuery)}
${highlightMatch(issue.title || issue.id, issueData.searchQuery)}
`).join('');
}
// Show search suggestions
function showSearchSuggestions() {
if (issueData.searchQuery) {
updateSearchSuggestions(issueData.searchQuery);
issueData.showSuggestions = true;
updateSuggestionsDropdown();
}
}
// Hide search suggestions
function hideSearchSuggestions() {
issueData.showSuggestions = false;
issueData.selectedSuggestion = -1;
const dropdown = document.getElementById('searchSuggestions');
if (dropdown) {
dropdown.classList.remove('show');
}
}
// Update suggestions dropdown without full re-render
function updateSuggestionsDropdown() {
const dropdown = document.getElementById('searchSuggestions');
if (dropdown) {
dropdown.innerHTML = renderSearchSuggestions();
if (issueData.showSuggestions && issueData.searchSuggestions.length > 0) {
dropdown.classList.add('show');
} else {
dropdown.classList.remove('show');
}
}
}
// Select a suggestion
function selectSuggestion(index) {
const issue = issueData.searchSuggestions[index];
if (issue) {
hideSearchSuggestions();
openIssueDetail(issue.id, issue._isArchived);
}
}
// Handle keyboard navigation in search
function handleSearchKeydown(event) {
const suggestions = issueData.searchSuggestions || [];
if (!issueData.showSuggestions || suggestions.length === 0) {
// If Enter and no suggestions, just search
if (event.key === 'Enter') {
hideSearchSuggestions();
}
return;
}
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
issueData.selectedSuggestion = Math.min(
issueData.selectedSuggestion + 1,
suggestions.length - 1
);
updateSuggestionsDropdown();
break;
case 'ArrowUp':
event.preventDefault();
issueData.selectedSuggestion = Math.max(issueData.selectedSuggestion - 1, -1);
updateSuggestionsDropdown();
break;
case 'Enter':
event.preventDefault();
if (issueData.selectedSuggestion >= 0) {
selectSuggestion(issueData.selectedSuggestion);
} else {
hideSearchSuggestions();
}
break;
case 'Escape':
hideSearchSuggestions();
break;
}
}
// Close suggestions when clicking outside
document.addEventListener('click', function(event) {
const searchContainer = document.querySelector('.issue-search');
if (searchContainer && !searchContainer.contains(event.target)) {
hideSearchSuggestions();
}
});
// ========== 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.queueCommandHint') || 'Run one of the following commands in your terminal to generate the execution queue from bound solutions:'}
Claude Code CLI
${command}
CCW CLI (${t('issues.alternative') || 'Alternative'})
${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');
}
}
// ========== Queue History Modal ==========
async function showQueueHistoryModal() {
// Create modal if not exists
let modal = document.getElementById('queueHistoryModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'queueHistoryModal';
modal.className = 'issue-modal';
document.body.appendChild(modal);
}
// Show loading state
modal.innerHTML = `
`;
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
? ``
: `
${queues.map(q => `
${q.issue_ids?.length || 0} issues
${q.completed_solutions || q.completed_tasks || 0}/${q.total_solutions || q.total_tasks || 0} ${q.total_solutions ? 'solutions' : 'tasks'}
${q.created_at ? new Date(q.created_at).toLocaleDateString() : 'N/A'}
${q.id !== activeQueueId ? `
Switch
` : ''}
View
`).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 = `
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);
}
// Support both solution-level and task-level queues
const items = queue.solutions || queue.queue || queue.tasks || [];
const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0);
const metadata = queue._metadata || {};
// Group by execution_group
const grouped = {};
items.forEach(item => {
const group = item.execution_group || 'ungrouped';
if (!grouped[group]) grouped[group] = [];
grouped[group].push(item);
});
const itemLabel = isSolutionLevel ? 'solutions' : 'tasks';
const detailHtml = `
${items.length}
${isSolutionLevel ? 'Solutions' : 'Total'}
${items.filter(t => t.status === 'completed').length}
Completed
${items.filter(t => t.status === 'pending').length}
Pending
${items.filter(t => t.status === 'failed').length}
Failed
${Object.entries(grouped).map(([groupId, groupItems]) => `
${groupItems.map(item => `
${item.item_id || item.queue_id || item.task_id || 'N/A'}
${isSolutionLevel ? (item.task_count + ' tasks') : (item.title || item.action || 'Untitled')}
${item.issue_id || ''}
${isSolutionLevel && item.files_touched ? `${item.files_touched.length} files ` : ''}
${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 = `
Back
Failed to load queue detail
`;
lucide.createIcons();
}
}
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');
});
}