You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
134 lines
6.4 KiB
134 lines
6.4 KiB
{% extends "portal_base.html" %} |
|
|
|
{% block title %}Global Video Jobs - OTB Cloud{% endblock %} |
|
|
|
{% block portal_content %} |
|
<style> |
|
.jobs-wrap { max-width: 1200px; margin: 0 auto; } |
|
.job-list { display:flex; flex-direction:column; gap:14px; margin-top:14px; } |
|
.job-card { background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.08); border-radius:14px; padding:14px; } |
|
.job-head { display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:8px; } |
|
.job-file { font-weight:700; word-break:break-word; } |
|
.job-head-right { display:flex; align-items:center; gap:8px; flex-wrap:wrap; } |
|
.job-badge { display:inline-block; padding:4px 10px; border-radius:999px; font-size:0.85rem; font-weight:700; border:1px solid rgba(255,255,255,0.14); } |
|
.job-badge.queued { background:rgba(245,158,11,0.18); } |
|
.job-badge.processing { background:rgba(59,130,246,0.18); } |
|
.job-badge.complete { background:rgba(34,197,94,0.18); } |
|
.job-badge.failed { background:rgba(239,68,68,0.18); } |
|
.job-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:10px; margin-top:8px; } |
|
.job-meta { background:rgba(255,255,255,0.03); border-radius:10px; padding:10px; } |
|
.job-label { font-size:0.8rem; opacity:0.75; margin-bottom:4px; } |
|
.job-value { word-break:break-word; } |
|
.job-path, .job-error { margin-top:10px; padding:10px; border-radius:10px; background:rgba(255,255,255,0.03); word-break:break-word; } |
|
.job-error { border:1px solid rgba(239,68,68,0.22); } |
|
.job-empty { padding:16px; border-radius:12px; background:rgba(255,255,255,0.04); opacity:0.85; } |
|
.filter-note { opacity:0.8; margin-top:6px; } |
|
</style> |
|
|
|
<div class="jobs-wrap"> |
|
<div class="portal-page-header"> |
|
<div> |
|
<h1 class="portal-page-title">Global Video Jobs</h1> |
|
<p class="portal-page-subtitle">All video processing jobs across your devices.</p> |
|
<div class="filter-note">Newest first. Use this as a cross-device overview.</div> |
|
</div> |
|
<div class="portal-toolbar" style="display:flex;gap:10px;flex-wrap:wrap;"> |
|
<a class="portal-btn" href="/dashboard">Back to Dashboard</a> |
|
<a class="portal-btn" href="/health">Health</a> |
|
</div> |
|
</div> |
|
|
|
{% if jobs and jobs|length > 0 %} |
|
<div class="job-list"> |
|
{% for j in jobs %} |
|
<div class="job-card"> |
|
<div class="job-head"> |
|
<div class="job-file">{{ j.filename }}</div> |
|
<div class="job-head-right"> |
|
{% if j.output_relative_path and j.status == 'complete' %} |
|
<a class="portal-btn" href="/video-output/{{ j.id }}/view" target="_blank">View</a> |
|
<button class="portal-btn" type="button" onclick="sendToLTS({{ j.id }})">Send to LTS</button> |
|
<a class="portal-btn" href="/video-output/{{ j.id }}/download">Download Output</a> |
|
{% endif %} |
|
{% if j.status in ['failed', 'complete'] %} |
|
<button class="portal-btn" type="button" onclick="deleteJob({{ j.id }})">Delete</button> |
|
{% endif %} |
|
<span class="job-badge {{ j.status|lower }}">{{ j.status|upper }}</span> |
|
</div> |
|
</div> |
|
|
|
<div class="job-grid"> |
|
<div class="job-meta"><div class="job-label">Job ID</div><div class="job-value">{{ j.id }}</div></div> |
|
<div class="job-meta"><div class="job-label">Device</div><div class="job-value">{{ j.device_name }} ({{ j.device_id }})</div></div> |
|
<div class="job-meta"><div class="job-label">Profile</div><div class="job-value">{{ j.profile }}</div></div> |
|
<div class="job-meta"><div class="job-label">Rotation Override</div><div class="job-value">{{ j.rotation_override or 'auto' }}</div></div> |
|
<div class="job-meta"><div class="job-label">Processor</div><div class="job-value">{{ j.assigned_processor or 'pending' }}</div></div> |
|
<div class="job-meta"><div class="job-label">Progress</div><div class="job-value">{{ j.progress_percent }}%</div></div> |
|
<div class="job-meta"><div class="job-label">Created</div><div class="job-value">{{ j.created_at }}</div></div> |
|
<div class="job-meta"><div class="job-label">Started</div><div class="job-value">{{ j.started_at }}</div></div> |
|
<div class="job-meta"><div class="job-label">Completed</div><div class="job-value">{{ j.completed_at }}</div></div> |
|
<div class="job-meta"><div class="job-label">Original Size</div><div class="job-value" data-bytes="{{ j.original_size or '' }}"></div></div> |
|
<div class="job-meta"><div class="job-label">Processed Size</div><div class="job-value" data-bytes="{{ j.processed_size or '' }}"></div></div> |
|
<div class="job-meta"><div class="job-label">GPU Time</div><div class="job-value">{{ j.gpu_seconds }}s</div></div> |
|
<div class="job-meta"><div class="job-label">Source File ID</div><div class="job-value">{{ j.source_file_id or '' }}</div></div> |
|
</div> |
|
|
|
{% if j.output_relative_path %} |
|
<div class="job-path"> |
|
<div class="job-label">Output</div> |
|
<div class="job-value">{{ j.output_relative_path }}</div> |
|
</div> |
|
{% endif %} |
|
|
|
{% if j.error_message %} |
|
<div class="job-error"> |
|
<div class="job-label">Error</div> |
|
<div class="job-value">{{ j.error_message }}</div> |
|
</div> |
|
{% endif %} |
|
</div> |
|
{% endfor %} |
|
</div> |
|
{% else %} |
|
<div class="job-empty">No video jobs yet.</div> |
|
{% endif %} |
|
</div> |
|
|
|
<script> |
|
function fmtBytes(v){ |
|
if(v === null || v === undefined || v === "") return ""; |
|
const n = Number(v); |
|
if(!Number.isFinite(n)) return String(v); |
|
if(n < 1024) return `${n} B`; |
|
if(n < 1024*1024) return `${(n/1024).toFixed(1)} KB`; |
|
if(n < 1024*1024*1024) return `${(n/1024/1024).toFixed(2)} MB`; |
|
return `${(n/1024/1024/1024).toFixed(2)} GB`; |
|
} |
|
|
|
document.querySelectorAll("[data-bytes]").forEach(el => { |
|
el.textContent = fmtBytes(el.getAttribute("data-bytes")); |
|
}); |
|
|
|
async function deleteJob(jobId){ |
|
if(!confirm("Delete this job from the list?")) return; |
|
const r = await fetch(`/api/video/jobs/${jobId}/delete`, { method: "POST" }); |
|
if(r.ok){ |
|
window.location.reload(); |
|
} else { |
|
const text = await r.text(); |
|
alert(text || "Delete failed"); |
|
} |
|
} |
|
|
|
async function sendToLTS(jobId){ |
|
if(!confirm("Send this output to LTS storage?")) return; |
|
const r = await fetch(`/video-output/${jobId}/send-to-lts`, { method: "POST" }); |
|
if(r.ok){ |
|
window.location.reload(); |
|
} else { |
|
const text = await r.text(); |
|
alert(text || "Send to LTS failed"); |
|
} |
|
} |
|
</script> |
|
{% endblock %}
|
|
|