mirror of
https://github.com/P-Aimon-Pen/ROFMAD
synced 2026-06-21 13:45:02 +00:00
528 lines
20 KiB
HTML
Executable File
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 %} |