mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat(issue-manager): Add queue history modal for viewing and switching queues
- Add GET /api/queue/history endpoint to fetch all queues from index - Add GET /api/queue/:id endpoint to fetch specific queue details - Add POST /api/queue/switch endpoint to switch active queue - Add History button in queue toolbar - Add queue history modal with list view and detail view - Add switch functionality to change active queue - Add CSS styles for queue history components 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -236,6 +236,82 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
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) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,6 +423,10 @@ function renderQueueSection() {
|
||||
<button class="btn-secondary" onclick="refreshQueue()" title="${t('issues.refreshQueue') || 'Refresh'}">
|
||||
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button class="btn-secondary" onclick="showQueueHistoryModal()" title="${t('issues.queueHistory') || 'Queue History'}">
|
||||
<i data-lucide="history" class="w-4 h-4"></i>
|
||||
<span>${t('issues.history') || 'History'}</span>
|
||||
</button>
|
||||
<button class="btn-secondary" onclick="createExecutionQueue()" title="${t('issues.regenerateQueue') || 'Regenerate Queue'}">
|
||||
<i data-lucide="rotate-cw" class="w-4 h-4"></i>
|
||||
<span>${t('issues.regenerate') || 'Regenerate'}</span>
|
||||
@@ -1529,6 +1533,240 @@ function hideQueueCommandModal() {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 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 = `
|
||||
<div class="issue-modal-backdrop" onclick="hideQueueHistoryModal()"></div>
|
||||
<div class="issue-modal-content" style="max-width: 700px; max-height: 80vh;">
|
||||
<div class="issue-modal-header">
|
||||
<h3><i data-lucide="history" class="w-5 h-5 inline mr-2"></i>${t('issues.queueHistory') || 'Queue History'}</h3>
|
||||
<button class="btn-icon" onclick="hideQueueHistoryModal()">
|
||||
<i data-lucide="x" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="issue-modal-body" style="overflow-y: auto; max-height: calc(80vh - 120px);">
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<i data-lucide="loader-2" class="w-6 h-6 animate-spin"></i>
|
||||
<span class="ml-2">${t('common.loading') || 'Loading...'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
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
|
||||
? `<div class="text-center py-8 text-muted-foreground">
|
||||
<i data-lucide="inbox" class="w-12 h-12 mx-auto mb-2 opacity-50"></i>
|
||||
<p>${t('issues.noQueueHistory') || 'No queue history found'}</p>
|
||||
</div>`
|
||||
: `<div class="queue-history-list">
|
||||
${queues.map(q => `
|
||||
<div class="queue-history-item ${q.id === activeQueueId ? 'active' : ''}" onclick="viewQueueDetail('${q.id}')">
|
||||
<div class="queue-history-header">
|
||||
<span class="queue-history-id font-mono">${q.id}</span>
|
||||
${q.id === activeQueueId ? '<span class="queue-active-badge">Active</span>' : ''}
|
||||
<span class="queue-history-status ${q.status || ''}">${q.status || 'unknown'}</span>
|
||||
</div>
|
||||
<div class="queue-history-meta">
|
||||
<span class="text-xs text-muted-foreground">
|
||||
<i data-lucide="layers" class="w-3 h-3 inline"></i>
|
||||
${q.issue_ids?.length || 0} issues
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
<i data-lucide="check-circle" class="w-3 h-3 inline"></i>
|
||||
${q.completed_tasks || 0}/${q.total_tasks || 0} tasks
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
<i data-lucide="calendar" class="w-3 h-3 inline"></i>
|
||||
${q.created_at ? new Date(q.created_at).toLocaleDateString() : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="queue-history-actions">
|
||||
${q.id !== activeQueueId ? `
|
||||
<button class="btn-sm btn-primary" onclick="event.stopPropagation(); switchToQueue('${q.id}')">
|
||||
<i data-lucide="arrow-right-circle" class="w-3 h-3"></i>
|
||||
${t('issues.switchTo') || 'Switch'}
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="btn-sm btn-secondary" onclick="event.stopPropagation(); viewQueueDetail('${q.id}')">
|
||||
<i data-lucide="eye" class="w-3 h-3"></i>
|
||||
${t('issues.view') || 'View'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>`;
|
||||
|
||||
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 = `
|
||||
<div class="text-center py-8 text-red-500">
|
||||
<i data-lucide="alert-circle" class="w-8 h-8 mx-auto mb-2"></i>
|
||||
<p>${t('errors.loadFailed') || 'Failed to load queue history'}</p>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<i data-lucide="loader-2" class="w-6 h-6 animate-spin"></i>
|
||||
<span class="ml-2">${t('common.loading') || 'Loading...'}</span>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="queue-detail-view">
|
||||
<div class="queue-detail-header mb-4">
|
||||
<button class="btn-sm btn-secondary" onclick="showQueueHistoryModal()">
|
||||
<i data-lucide="arrow-left" class="w-3 h-3"></i>
|
||||
${t('common.back') || 'Back'}
|
||||
</button>
|
||||
<h4 class="text-lg font-semibold ml-4">${queue.id || queueId}</h4>
|
||||
</div>
|
||||
|
||||
<div class="queue-detail-stats mb-4">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">${tasks.length}</span>
|
||||
<span class="stat-label">${t('issues.totalTasks') || 'Total'}</span>
|
||||
</div>
|
||||
<div class="stat-item completed">
|
||||
<span class="stat-value">${tasks.filter(t => t.status === 'completed').length}</span>
|
||||
<span class="stat-label">${t('issues.completed') || 'Completed'}</span>
|
||||
</div>
|
||||
<div class="stat-item pending">
|
||||
<span class="stat-value">${tasks.filter(t => t.status === 'pending').length}</span>
|
||||
<span class="stat-label">${t('issues.pending') || 'Pending'}</span>
|
||||
</div>
|
||||
<div class="stat-item failed">
|
||||
<span class="stat-value">${tasks.filter(t => t.status === 'failed').length}</span>
|
||||
<span class="stat-label">${t('issues.failed') || 'Failed'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="queue-detail-groups">
|
||||
${Object.entries(grouped).map(([groupId, items]) => `
|
||||
<div class="queue-group-section">
|
||||
<div class="queue-group-header">
|
||||
<i data-lucide="folder" class="w-4 h-4"></i>
|
||||
<span>${groupId}</span>
|
||||
<span class="text-xs text-muted-foreground">(${items.length} tasks)</span>
|
||||
</div>
|
||||
<div class="queue-group-items">
|
||||
${items.map(item => `
|
||||
<div class="queue-detail-item ${item.status || ''}">
|
||||
<span class="item-id font-mono text-xs">${item.item_id || item.task_id}</span>
|
||||
<span class="item-issue text-xs">${item.issue_id}</span>
|
||||
<span class="item-status ${item.status || ''}">${item.status || 'unknown'}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div class="text-center py-8">
|
||||
<button class="btn-sm btn-secondary mb-4" onclick="showQueueHistoryModal()">
|
||||
<i data-lucide="arrow-left" class="w-3 h-3"></i>
|
||||
${t('common.back') || 'Back'}
|
||||
</button>
|
||||
<div class="text-red-500">
|
||||
<i data-lucide="alert-circle" class="w-8 h-8 mx-auto mb-2"></i>
|
||||
<p>${t('errors.loadFailed') || 'Failed to load queue detail'}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
function copyCommand(command) {
|
||||
navigator.clipboard.writeText(command).then(() => {
|
||||
showNotification(t('common.copied') || 'Copied to clipboard', 'success');
|
||||
|
||||
Reference in New Issue
Block a user