Files
2026-06-18 15:03:00 +02:00

528 lines
20 KiB
HTML
Executable File

{% extends "base.html" %}
{% block extra_head %}
<style>
/* Queue Page */
.queue-stats {
color: var(--text-muted);
font-size: 0.82rem;
font-weight: 500;
font-family: 'JetBrains Mono', monospace;
}
.queue-stats span { color: var(--gb-blue-b); font-weight: 700; }
.no-queued-tasks,
.no-active-tasks {
background-color: var(--gb-bg);
border: 1px solid var(--gb-bg1);
padding: 2rem;
text-align: center;
margin-bottom: 1.5rem;
}
.no-queued-tasks h3, .no-active-tasks h3 { color: var(--text-secondary); margin-bottom: 0.5rem; font-size: 0.9rem; }
.no-queued-tasks p, .no-active-tasks p { color: var(--text-muted); margin: 0; font-size: 0.82rem; }
.task-actions { white-space: nowrap; display: flex; gap: 0.35rem; }
/* Override base table hover — no translateX */
.tasks-table tbody tr:hover { background-color: var(--gb-bg1); }
.tasks-table-container { border: 1px solid var(--gb-bg1); overflow: hidden; }
/* Concurrency reused from dashboard styles in styles.html */
.concurrency-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 0.75rem;
}
.concurrency-card {
background-color: var(--gb-bg);
padding: 1rem;
border-left: 2px solid var(--gb-bg3);
}
.concurrency-card h4 {
color: var(--text-muted);
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 0.5rem;
}
.usage-fraction { color: var(--gb-blue-b); font-weight: 700; font-size: 0.95rem; font-family: 'JetBrains Mono', monospace; }
.usage-bar { background-color: var(--gb-bg1); height: 8px; margin: 0.35rem 0; border: 1px solid var(--gb-bg2); }
.usage-fill { background-color: var(--gb-blue); height: 100%; }
.updating { opacity: 0.6; pointer-events: none; }
@media (max-width: 768px) {
.task-actions { flex-direction: column; }
}
</style>
{% endblock %}
{% block title %}Task Queue{% endblock %}
{% block content %}
<div class="queue-section">
<div class="section-header">
<h1>Task Queue Management</h1>
<div class="queue-stats">
<span><span id="active-count">{{ active_tasks.len() }}</span> active | <span id="queued-count">{{ queued_tasks.len() }}</span> queued</span>
</div>
</div>
<!-- Concurrency Status -->
<div class="section-card">
<div class="section-header">
<h2>Semaphore Status</h2>
</div>
<div class="section-content">
<div class="concurrency-grid">
{% for group in concurrency_stats.display_groups %}
<div class="concurrency-card">
<h4>{{ group.name }}</h4>
<div class="concurrency-info">
<div class="concurrency-usage">
<span class="usage-fraction">{{ group.in_use }}/{{ group.total_limit }}</span>
<div class="usage-bar">
<div class="usage-fill" style="width: {% if group.total_limit > 0 %}{{ (group.in_use * 100 / group.total_limit) }}{% else %}0{% endif %}%"></div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- Active Tasks -->
{% if active_tasks.len() > 0 %}
<div class="section-card" id="active-tasks-section">
<div class="section-header">
<h2>Active Tasks (<span id="active-tasks-count">{{ active_tasks.len() }}</span>)</h2>
</div>
<div class="section-content">
<div class="tasks-table-container">
<table class="tasks-table" id="active-tasks-table">
<thead>
<tr>
<th>Task</th>
<th>Target</th>
<th>Duration</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for task in active_tasks %}
<tr>
<td class="task-name">{{ task.name }}</td>
<td class="task-target">{{ task.target }}</td>
<td class="task-duration">{{ task.duration }}</td>
<td class="task-actions">
<a href="/task/{{ task.id }}" class="btn btn-detail btn-small">Detail</a>
<button class="btn btn-danger btn-small" onclick="killTask('{{ task.id }}', '{{ task.name }}')">Kill</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="no-active-tasks" id="no-active-tasks">
<h3>No Active Tasks</h3>
<p>No tasks are currently running. Tasks will appear here when system is executing work.</p>
</div>
{% endif %}
<!-- Queued Tasks -->
{% if queued_tasks.len() > 0 %}
<div class="section-card" id="queued-tasks-section">
<div class="section-header">
<h2>Queued Tasks (<span id="queued-tasks-count">{{ queued_tasks.len() }}</span>)</h2>
</div>
<div class="section-content">
<div class="tasks-table-container">
<table class="tasks-table" id="queued-tasks-table">
<thead>
<tr>
<th>Task</th>
<th>Target</th>
<th>Queued Duration</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for task in queued_tasks %}
<tr>
<td class="task-name">{{ task.name }}</td>
<td class="task-target">{{ task.target }}</td>
<td class="task-duration">{{ task.duration }}</td>
<td class="task-actions">
<a href="/task/{{ task.id }}" class="btn btn-detail btn-small">Detail</a>
<button class="btn btn-danger btn-small" onclick="killTask('{{ task.id }}', '{{ task.name }}')">Kill</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="no-queued-tasks" id="no-queued-tasks">
<h3>No Queued Tasks</h3>
<p>All tasks are either running or completed. Queue will show here when system is under load.</p>
</div>
{% endif %}
</div>
<script>
// Kill task function
async function killTask(taskId, taskName) {
if (!confirm(`Kill task "${taskName}"?`)) {
return;
}
try {
const response = await fetch(`/api/kill-task/${taskId}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
if (window.ROFMAD?.showNotification) {
window.ROFMAD.showNotification(data.message, 'success');
}
await queueRefresh.refresh();
} else {
alert('Failed to kill task: ' + (data.message || 'Unknown error'));
}
} catch (error) {
console.error('Error killing task:', error);
alert('Failed to kill task: ' + error.message);
}
}
// Queue refresh functionality
const queueRefresh = {
interval: null,
start: () => {
queueRefresh.interval = setInterval(async () => {
if (document.hidden || window.location.pathname !== '/queue') return;
try {
const [taskResponse, concurrencyResponse, activeTasksResponse, queuedTasksResponse] = await Promise.all([
fetch('/api/task-status'),
fetch('/api/concurrency-status'),
fetch('/api/active-tasks'),
fetch('/api/queued-tasks') // FIX: Use correct endpoint
]);
if (taskResponse.ok) {
const taskData = await taskResponse.json();
queueRefresh.updateTaskCounters(taskData);
}
if (activeTasksResponse.ok) {
const activeData = await activeTasksResponse.json();
queueRefresh.updateActiveTasksTable(activeData.tasks || []);
}
if (queuedTasksResponse.ok) {
const queuedData = await queuedTasksResponse.json();
queueRefresh.updateQueuedTasksTable(queuedData.tasks || []); // FIX: Use correct property
}
if (concurrencyResponse.ok) {
const concurrencyData = await concurrencyResponse.json();
queueRefresh.updateConcurrencyDisplay(concurrencyData);
}
} catch (error) {
console.error('Queue update failed:', error);
}
}, 2000);
},
stop: () => {
if (queueRefresh.interval) {
clearInterval(queueRefresh.interval);
queueRefresh.interval = null;
}
},
refresh: async () => {
try {
const [taskRes, activeRes, queuedRes, concurrencyRes] = await Promise.all([
fetch('/api/task-status'),
fetch('/api/active-tasks'),
fetch('/api/queued-tasks'),
fetch('/api/concurrency-status'),
]);
if (taskRes.ok) queueRefresh.updateTaskCounters(await taskRes.json());
if (activeRes.ok) queueRefresh.updateActiveTasksTable((await activeRes.json()).tasks || []);
if (queuedRes.ok) queueRefresh.updateQueuedTasksTable((await queuedRes.json()).tasks || []);
if (concurrencyRes.ok) queueRefresh.updateConcurrencyDisplay(await concurrencyRes.json());
} catch (e) {
console.error('Queue refresh failed:', e);
}
},
updateTaskCounters: (data) => {
// Update main queue stats
const activeCountEl = document.getElementById('active-count');
const queuedCountEl = document.getElementById('queued-count');
if (activeCountEl) activeCountEl.textContent = data.active_count;
if (queuedCountEl) queuedCountEl.textContent = data.queued_count;
// Update section headers
const activeTasksCountEl = document.getElementById('active-tasks-count');
const queuedTasksCountEl = document.getElementById('queued-tasks-count');
if (activeTasksCountEl) activeTasksCountEl.textContent = data.active_count;
if (queuedTasksCountEl) queuedTasksCountEl.textContent = data.queued_count;
},
updateActiveTasksTable: (tasks) => {
const activeSection = document.getElementById('active-tasks-section');
const noActiveSection = document.getElementById('no-active-tasks');
if (tasks.length === 0) {
// Show empty state
if (activeSection) activeSection.style.display = 'none';
if (noActiveSection) {
noActiveSection.style.display = 'block';
} else {
// Create empty state if it doesn't exist
const emptyState = document.createElement('div');
emptyState.className = 'no-active-tasks';
emptyState.id = 'no-active-tasks';
emptyState.innerHTML = `
<h3>No Active Tasks</h3>
<p>No tasks are currently running. Tasks will appear here when system is executing work.</p>
`;
activeSection.parentNode.insertBefore(emptyState, activeSection);
activeSection.style.display = 'none';
}
} else {
// Show tasks table
if (noActiveSection) noActiveSection.style.display = 'none';
if (activeSection) {
activeSection.style.display = 'block';
const tbody = activeSection.querySelector('tbody');
if (tbody) {
tbody.innerHTML = tasks.map(task => `
<tr>
<td class="task-name">${task.name}</td>
<td class="task-target">${task.target}</td>
<td class="task-duration">${task.duration}</td>
<td class="task-actions">
<a href="/task/${task.id}" class="btn btn-secondary btn-small">Detail</a>
<button class="btn btn-danger btn-small" onclick="killTask('${task.id}', '${task.name}')">Kill</button>
</td>
</tr>
`).join('');
}
}
}
},
updateQueuedTasksTable: (tasks) => {
const queuedSection = document.getElementById('queued-tasks-section');
const noQueuedSection = document.getElementById('no-queued-tasks');
if (tasks.length === 0) {
// Show empty state
if (queuedSection) queuedSection.style.display = 'none';
if (noQueuedSection) {
noQueuedSection.style.display = 'block';
} else {
// Create empty state if it doesn't exist
const emptyState = document.createElement('div');
emptyState.className = 'no-queued-tasks';
emptyState.id = 'no-queued-tasks';
emptyState.innerHTML = `
<h3>No Queued Tasks</h3>
<p>All tasks are either running or completed. Queue will show here when system is under load.</p>
`;
queuedSection.parentNode.insertBefore(emptyState, queuedSection.nextSibling);
queuedSection.style.display = 'none';
}
} else {
// Show tasks table
if (noQueuedSection) noQueuedSection.style.display = 'none';
if (queuedSection) {
queuedSection.style.display = 'block';
const tbody = queuedSection.querySelector('tbody');
if (tbody) {
tbody.innerHTML = tasks.map(task => `
<tr>
<td class="task-name">${task.name}</td>
<td class="task-target">${task.target}</td>
<td class="task-duration">${task.duration}</td>
<td class="task-actions">
<a href="/task/${task.id}" class="btn btn-secondary btn-small">Detail</a>
<button class="btn btn-danger btn-small" onclick="killTask('${task.id}', '${task.name}')">Kill</button>
</td>
</tr>
`).join('');
}
}
}
},
updateConcurrencyDisplay: (data) => {
// Update each concurrency card
(data.display_groups || []).forEach(group => {
const cards = document.querySelectorAll('.concurrency-card');
cards.forEach(card => {
const h4 = card.querySelector('h4');
if (h4 && h4.textContent.trim() === group.name) {
const fraction = card.querySelector('.usage-fraction');
const fill = card.querySelector('.usage-fill');
if (fraction) fraction.textContent = `${group.in_use}/${group.total_limit}`;
if (fill) {
const percentage = group.total_limit > 0 ? (group.in_use * 100 / group.total_limit) : 0;
fill.style.width = `${percentage}%`;
}
}
});
});
}
};
// Enhanced initialization
document.addEventListener('DOMContentLoaded', () => {
// Start real-time updates
queueRefresh.start();
// Table row hover is handled by CSS
// Enhanced concurrency card animations
const concurrencyCards = document.querySelectorAll('.concurrency-card');
concurrencyCards.forEach(card => {
// Card hover is handled by CSS
});
// Add loading states for kill buttons
document.addEventListener('click', function(e) {
if (e.target.classList.contains('btn-danger')) {
const button = e.target;
const originalText = button.textContent;
// Visual feedback on click
button.style.background = '#b91c1c';
button.style.transform = 'scale(0.95)';
setTimeout(() => {
if (button.parentNode) {
button.style.background = '';
button.style.transform = '';
}
}, 200);
}
});
// Add keyboard shortcuts
document.addEventListener('keydown', function(e) {
if (e.altKey) {
switch(e.key) {
case 'r':
e.preventDefault();
location.reload();
break;
case 'q':
e.preventDefault();
window.location.href = '/queue';
break;
case 'd':
e.preventDefault();
window.location.href = '/';
break;
}
}
});
// Add tooltips for task durations
document.querySelectorAll('.task-duration').forEach(duration => {
duration.addEventListener('mouseenter', function() {
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.style.cssText = `
position: absolute;
background: var(--gb-bg1);
color: var(--gb-fg);
padding: 0.35rem 0.6rem;
border-radius: 0;
font-size: 0.75rem;
font-family: 'JetBrains Mono', monospace;
z-index: 1000;
pointer-events: none;
border: 1px solid var(--gb-bg2);
`;
const text = this.textContent.trim();
tooltip.textContent = `Running for: ${text}`;
document.body.appendChild(tooltip);
const rect = this.getBoundingClientRect();
tooltip.style.left = (rect.left + rect.width / 2 - tooltip.offsetWidth / 2) + 'px';
tooltip.style.top = (rect.top - tooltip.offsetHeight - 5) + 'px';
this.addEventListener('mouseleave', () => {
if (tooltip.parentNode) {
tooltip.parentNode.removeChild(tooltip);
}
}, { once: true });
});
});
// Add pulse animation to active task counts
const activeCountElements = document.querySelectorAll('#active-count, #active-tasks-count');
activeCountElements.forEach(el => {
if (parseInt(el.textContent) > 0) {
el.style.animation = 'pulse 2s ease-in-out infinite';
}
});
// Add CSS animations
const style = document.createElement('style');
style.textContent = `
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.btn-danger {
transition: all 0.2s ease;
}
.btn-danger:active {
transform: scale(0.95);
}
.tooltip {
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
`;
document.head.appendChild(style);
});
// Stop refresh when page unloads
window.addEventListener('beforeunload', () => {
queueRefresh.stop();
});
// Make killTask globally available
window.killTask = killTask;
</script>
{% endblock %}