changes to mobile styling in modal view, complete reword of backend worker system
This commit is contained in:
@@ -206,7 +206,140 @@
|
||||
</div>
|
||||
</div><!-- end settings-grid -->
|
||||
|
||||
<!-- Import Queue Status (Full Width) -->
|
||||
<div class="settings-container" style="max-width: 1200px; margin: 2rem auto;">
|
||||
<div class="settings-section">
|
||||
<h2>Import Queue Status</h2>
|
||||
<p class="settings-description">Monitor and control the Celery-based import queue system.</p>
|
||||
|
||||
<div id="queueStatusSection">
|
||||
<div class="stats-grid" style="grid-template-columns: repeat(6, 1fr);">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="queuePending">-</span>
|
||||
<span class="stat-label">Pending</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="queueQueued">-</span>
|
||||
<span class="stat-label">Queued</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="queueProcessing">-</span>
|
||||
<span class="stat-label">Processing</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="queueComplete">-</span>
|
||||
<span class="stat-label">Complete</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="queueSkipped">-</span>
|
||||
<span class="stat-label">Skipped</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="queueFailed">-</span>
|
||||
<span class="stat-label">Failed</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="activeBatchInfo" class="import-stats" style="margin-top: 1rem; display: none;">
|
||||
<h3>Active Batch</h3>
|
||||
<p>Processing <strong id="batchDir">-</strong></p>
|
||||
<div class="batch-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="batchProgressFill" style="width: 0%"></div>
|
||||
</div>
|
||||
<span id="batchProgressText">0 / 0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="queue-actions" style="margin-top: 1rem; display: flex; gap: 0.75rem; flex-wrap: wrap;">
|
||||
<button id="triggerImportBtn" class="btn primary-btn">Trigger Import Scan</button>
|
||||
<button id="retryFailedBtn" class="btn warning-btn">Retry Failed Tasks</button>
|
||||
<button id="clearCompletedBtn" class="btn secondary-btn">Clear Completed</button>
|
||||
<button id="regenMissingThumbsBtn" class="btn secondary-btn">Regenerate Missing Thumbnails</button>
|
||||
<a href="/flower" target="_blank" class="btn secondary-btn" style="text-decoration: none;">Open Flower Dashboard</a>
|
||||
</div>
|
||||
|
||||
<div id="celeryWorkerInfo" style="margin-top: 1rem;">
|
||||
<p style="color: var(--text-muted); font-size: 0.85rem;">
|
||||
Workers: <strong id="workerCount">-</strong> |
|
||||
Active Tasks: <strong id="activeTasks">-</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Tasks Table -->
|
||||
<div id="recentTasksSection" style="margin-top: 1.5rem;">
|
||||
<h3>Recent Tasks</h3>
|
||||
<div class="tasks-table-wrapper" style="max-height: 300px; overflow-y: auto;">
|
||||
<table class="tasks-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>File</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Error</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="recentTasksBody">
|
||||
<tr><td colspan="5" style="text-align: center; color: var(--text-muted);">Loading...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tasks-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.tasks-table th,
|
||||
.tasks-table td {
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.tasks-table th {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: var(--text-dim);
|
||||
font-weight: 500;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
.tasks-table td {
|
||||
color: var(--text);
|
||||
}
|
||||
.tasks-table .status-pending { color: #9ca3af; }
|
||||
.tasks-table .status-queued { color: #60a5fa; }
|
||||
.tasks-table .status-processing { color: #fbbf24; }
|
||||
.tasks-table .status-complete { color: #34d399; }
|
||||
.tasks-table .status-skipped { color: #a78bfa; }
|
||||
.tasks-table .status-failed { color: #f87171; }
|
||||
.batch-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--btn-primary);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.queue-actions .btn {
|
||||
min-width: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.settings-description {
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 1rem;
|
||||
@@ -363,6 +496,177 @@
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// ========================================
|
||||
// Celery Queue Status
|
||||
// ========================================
|
||||
async function loadQueueStatus() {
|
||||
try {
|
||||
const r = await fetch('/api/import/status');
|
||||
const data = await r.json();
|
||||
|
||||
if (data.ok) {
|
||||
// Update status counts
|
||||
document.getElementById('queuePending').textContent = data.status_counts.pending || 0;
|
||||
document.getElementById('queueQueued').textContent = data.status_counts.queued || 0;
|
||||
document.getElementById('queueProcessing').textContent = data.status_counts.processing || 0;
|
||||
document.getElementById('queueComplete').textContent = data.status_counts.complete || 0;
|
||||
document.getElementById('queueSkipped').textContent = data.status_counts.skipped || 0;
|
||||
document.getElementById('queueFailed').textContent = data.status_counts.failed || 0;
|
||||
|
||||
// Update active batch info
|
||||
const batchEl = document.getElementById('activeBatchInfo');
|
||||
if (data.active_batch) {
|
||||
batchEl.style.display = 'block';
|
||||
document.getElementById('batchDir').textContent = data.active_batch.source_directory;
|
||||
const processed = data.active_batch.processed_files || 0;
|
||||
const total = data.active_batch.total_files || 1;
|
||||
const pct = Math.round((processed / total) * 100);
|
||||
document.getElementById('batchProgressFill').style.width = pct + '%';
|
||||
document.getElementById('batchProgressText').textContent = `${processed} / ${total}`;
|
||||
} else {
|
||||
batchEl.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update recent tasks table
|
||||
const tbody = document.getElementById('recentTasksBody');
|
||||
if (data.recent_tasks && data.recent_tasks.length > 0) {
|
||||
tbody.innerHTML = data.recent_tasks.map(t => `
|
||||
<tr>
|
||||
<td title="${t.source_path || ''}">${t.source_path || '-'}</td>
|
||||
<td>${t.task_type || '-'}</td>
|
||||
<td class="status-${t.status}">${t.status || '-'}</td>
|
||||
<td title="${t.error_message || ''}">${t.error_message ? t.error_message.substring(0, 30) + '...' : '-'}</td>
|
||||
<td>${t.created_at ? new Date(t.created_at).toLocaleString() : '-'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} else {
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; color: var(--text-muted);">No recent tasks</td></tr>';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load queue status:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCeleryStatus() {
|
||||
try {
|
||||
const r = await fetch('/api/celery/status');
|
||||
const data = await r.json();
|
||||
|
||||
if (data.ok) {
|
||||
document.getElementById('workerCount').textContent = data.worker_count || 0;
|
||||
document.getElementById('activeTasks').textContent = data.active_tasks || 0;
|
||||
} else {
|
||||
document.getElementById('workerCount').textContent = '?';
|
||||
document.getElementById('activeTasks').textContent = '?';
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('workerCount').textContent = 'offline';
|
||||
document.getElementById('activeTasks').textContent = '-';
|
||||
}
|
||||
}
|
||||
|
||||
// Load queue status on page load and refresh periodically
|
||||
loadQueueStatus();
|
||||
loadCeleryStatus();
|
||||
setInterval(loadQueueStatus, 5000); // Refresh every 5 seconds
|
||||
setInterval(loadCeleryStatus, 10000); // Refresh every 10 seconds
|
||||
|
||||
// Trigger Import Scan
|
||||
document.getElementById('triggerImportBtn').addEventListener('click', async () => {
|
||||
const btn = document.getElementById('triggerImportBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Starting...';
|
||||
|
||||
try {
|
||||
const r = await fetch('/api/import/trigger', { method: 'POST' });
|
||||
const data = await r.json();
|
||||
|
||||
if (data.ok) {
|
||||
alert('Import scan started! Task ID: ' + data.task_id);
|
||||
loadQueueStatus();
|
||||
} else {
|
||||
alert('Failed to start import: ' + (data.error || 'Unknown error'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Failed to start import: ' + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Trigger Import Scan';
|
||||
}
|
||||
});
|
||||
|
||||
// Retry Failed Tasks
|
||||
document.getElementById('retryFailedBtn').addEventListener('click', async () => {
|
||||
const btn = document.getElementById('retryFailedBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Retrying...';
|
||||
|
||||
try {
|
||||
const r = await fetch('/api/import/retry-failed', { method: 'POST' });
|
||||
const data = await r.json();
|
||||
|
||||
if (data.ok) {
|
||||
alert(`Retried ${data.retried} failed task(s).`);
|
||||
loadQueueStatus();
|
||||
} else {
|
||||
alert('Failed to retry: ' + (data.error || 'Unknown error'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Failed to retry: ' + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Retry Failed Tasks';
|
||||
}
|
||||
});
|
||||
|
||||
// Clear Completed Tasks
|
||||
document.getElementById('clearCompletedBtn').addEventListener('click', async () => {
|
||||
const btn = document.getElementById('clearCompletedBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Clearing...';
|
||||
|
||||
try {
|
||||
const r = await fetch('/api/import/clear-completed', { method: 'POST' });
|
||||
const data = await r.json();
|
||||
|
||||
if (data.ok) {
|
||||
alert(`Cleared ${data.deleted} completed/skipped task(s).`);
|
||||
loadQueueStatus();
|
||||
} else {
|
||||
alert('Failed to clear: ' + (data.error || 'Unknown error'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Failed to clear: ' + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Clear Completed';
|
||||
}
|
||||
});
|
||||
|
||||
// Regenerate Missing Thumbnails
|
||||
document.getElementById('regenMissingThumbsBtn').addEventListener('click', async () => {
|
||||
const btn = document.getElementById('regenMissingThumbsBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Starting...';
|
||||
|
||||
try {
|
||||
const r = await fetch('/api/thumbnails/regenerate-missing', { method: 'POST' });
|
||||
const data = await r.json();
|
||||
|
||||
if (data.ok) {
|
||||
alert('Missing thumbnail generation started! Task ID: ' + data.task_id);
|
||||
} else {
|
||||
alert('Failed to start: ' + (data.error || 'Unknown error'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Failed to start: ' + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Regenerate Missing Thumbnails';
|
||||
}
|
||||
});
|
||||
|
||||
// Load import settings
|
||||
fetch('/api/import-settings')
|
||||
.then(r => r.json())
|
||||
|
||||
Reference in New Issue
Block a user