163 lines
6.8 KiB
HTML
163 lines
6.8 KiB
HTML
{{define "content"}}
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem;">
|
|
<div class="card" style="text-align: center; padding: 1.5rem;">
|
|
<h3 style="margin-top: 0; color: var(--text-secondary); font-size: 0.875rem; text-transform: uppercase;">Files Encoded</h3>
|
|
<div style="font-size: 2rem; font-weight: 700; color: var(--accent);">{{.FilesEncoded}}</div>
|
|
</div>
|
|
<div class="card" style="text-align: center; padding: 1.5rem;">
|
|
<h3 style="margin-top: 0; color: var(--text-secondary); font-size: 0.875rem; text-transform: uppercase;">Space Saved</h3>
|
|
<div style="font-size: 2rem; font-weight: 700; color: var(--accent);">{{.SavedSpace}}</div>
|
|
</div>
|
|
<div class="card" style="text-align: center; padding: 1.5rem;">
|
|
<h3 style="margin-top: 0; color: var(--text-secondary); font-size: 0.875rem; text-transform: uppercase;">In Queue</h3>
|
|
<div style="font-size: 2rem; font-weight: 700; color: var(--accent);">{{.QueueLength}}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Active Job Queue</h2>
|
|
<div class="table-container">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>File</th>
|
|
<th>Type</th>
|
|
<th>Priority</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="queue-body">
|
|
<!-- Populated by JS -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1rem;">
|
|
<div>
|
|
<label for="limit-select" style="color: var(--text-secondary); font-size: 0.875rem; margin-right: 0.5rem;">Rows per page:</label>
|
|
<select id="limit-select" onchange="changeLimit()" style="background: var(--surface); color: var(--text-primary); border: 1px solid var(--border); border-radius: 4px; padding: 0.25rem 0.5rem;">
|
|
<option value="10">10</option>
|
|
<option value="50">50</option>
|
|
<option value="100">100</option>
|
|
</select>
|
|
</div>
|
|
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
|
<button id="prev-page" class="btn btn-sm btn-outline" onclick="changePage(-1)">Previous</button>
|
|
<span id="page-info" style="color: var(--text-secondary); font-size: 0.875rem;">Page 1 of 1</span>
|
|
<button id="next-page" class="btn btn-sm btn-outline" onclick="changePage(1)">Next</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const queueBody = document.getElementById('queue-body');
|
|
let currentPage = 1;
|
|
let currentLimit = 10;
|
|
|
|
async function loadQueue() {
|
|
const res = await fetch(`/api/queue?page=${currentPage}&limit=${currentLimit}`);
|
|
const data = await res.json();
|
|
|
|
renderQueue(data.jobs || []);
|
|
|
|
document.getElementById('page-info').innerText = `Page ${data.page} of ${data.totalPages}`;
|
|
document.getElementById('prev-page').disabled = data.page <= 1;
|
|
document.getElementById('next-page').disabled = data.page >= data.totalPages;
|
|
|
|
currentPage = data.page;
|
|
}
|
|
|
|
function changeLimit() {
|
|
currentLimit = document.getElementById('limit-select').value;
|
|
currentPage = 1;
|
|
loadQueue();
|
|
}
|
|
|
|
function changePage(delta) {
|
|
currentPage += delta;
|
|
loadQueue();
|
|
}
|
|
|
|
function renderQueue(jobs) {
|
|
queueBody.innerHTML = jobs.map(job => {
|
|
let statusBadge = `<span class="badge ${job.status}">${job.status}</span>`;
|
|
|
|
let progressHtml = '';
|
|
if (job.status === 'processing') {
|
|
progressHtml = `
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" id="progress-${job.id}" style="width: 0%"></div>
|
|
</div>
|
|
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.25rem;" id="progress-text-${job.id}">0%</div>
|
|
`;
|
|
}
|
|
|
|
let actions = '';
|
|
if (job.status === 'pending') {
|
|
actions = `
|
|
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
|
<button class="btn btn-sm btn-outline" onclick="bumpJob(${job.id})">Bump</button>
|
|
<button class="btn btn-sm btn-danger" onclick="cancelJob(${job.id})">Cancel</button>
|
|
</div>
|
|
`;
|
|
} else if (job.status === 'processing') {
|
|
actions = `
|
|
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
|
<button class="btn btn-sm btn-danger" onclick="cancelJob(${job.id})">Cancel</button>
|
|
</div>
|
|
`;
|
|
} else if (job.status === 'failed') {
|
|
// Retry not fully implemented in API yet, but could re-enqueue
|
|
actions = `<span style="color: var(--error); font-size: 0.75rem;">${job.error_message}</span>`;
|
|
}
|
|
|
|
return `
|
|
<tr id="job-${job.id}">
|
|
<td>${job.id}</td>
|
|
<td style="word-break: break-all;">
|
|
${job.file_path.split('/').pop()}
|
|
${progressHtml}
|
|
</td>
|
|
<td><span class="badge" style="background: var(--surface-hover); color: var(--text-primary); border-color: var(--border);">${job.media_type}</span></td>
|
|
<td>${job.priority}</td>
|
|
<td>${statusBadge}</td>
|
|
<td>${actions}</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
async function bumpJob(id) {
|
|
await fetch('/api/jobs/bump/' + id, { method: 'POST' });
|
|
loadQueue();
|
|
}
|
|
|
|
async function cancelJob(id) {
|
|
if (!confirm("Cancel job?")) return;
|
|
await fetch('/api/jobs/cancel/' + id, { method: 'POST' });
|
|
loadQueue();
|
|
}
|
|
|
|
// SSE listeners
|
|
window.sse.addEventListener('queue_updated', () => loadQueue());
|
|
window.sse.addEventListener('job_started', () => loadQueue());
|
|
window.sse.addEventListener('job_completed', () => loadQueue());
|
|
window.sse.addEventListener('job_failed', () => loadQueue());
|
|
window.sse.addEventListener('job_added', () => loadQueue());
|
|
|
|
window.sse.addEventListener('progress', (e) => {
|
|
const data = JSON.parse(e.data);
|
|
const fill = document.getElementById(`progress-${data.id}`);
|
|
const text = document.getElementById(`progress-text-${data.id}`);
|
|
if (fill && text) {
|
|
fill.style.width = `${data.progress}%`;
|
|
text.innerText = `${data.progress}%`;
|
|
}
|
|
});
|
|
|
|
// Initial load
|
|
loadQueue();
|
|
</script>
|
|
{{end}}
|