Files
goencode/web/templates/dashboard.html
T

200 lines
8.2 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;
function formatElapsed(ms) {
const totalSecs = Math.floor(ms / 1000);
if (totalSecs < 0) return '0s';
const hours = Math.floor(totalSecs / 3600);
const minutes = Math.floor((totalSecs % 3600) / 60);
const seconds = totalSecs % 60;
let parts = [];
if (hours > 0) {
parts.push(`${hours}h`);
}
if (minutes > 0 || hours > 0) {
parts.push(`${minutes}m`);
}
parts.push(`${seconds}s`);
return parts.join(' ');
}
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') {
const elapsedMs = Date.now() - new Date(job.updated_at).getTime();
const formatted = formatElapsed(elapsedMs);
progressHtml = `
<div class="progress-bar">
<div class="progress-fill" id="progress-${job.id}" style="width: 0%"></div>
</div>
<div style="display: flex; justify-content: space-between; font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.25rem;">
<span id="progress-text-${job.id}">0%</span>
<span id="progress-elapsed-${job.id}" class="progress-elapsed" data-start-time="${job.updated_at}">Elapsed: ${formatted}</span>
</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}%`;
}
});
// Update elapsed times periodically
setInterval(() => {
document.querySelectorAll('.progress-elapsed').forEach(el => {
const startTimeStr = el.getAttribute('data-start-time');
if (startTimeStr) {
const startTime = new Date(startTimeStr).getTime();
const elapsedMs = Date.now() - startTime;
el.innerText = `Elapsed: ${formatElapsed(elapsedMs)}`;
}
});
}, 1000);
// Initial load
loadQueue();
</script>
{{end}}