otb-cloud secure encrypted backups
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

{% 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 %}