fix(ui): jobs page — semantic cards, remove onclick, flat progress bar

This commit is contained in:
jetson committed 2026-09-17 16:19:51 +07:00
1 parent b1170f42a9
commit 20d523469d
2 files changed
+60 -13

No files matched your search

@@ -0,0 +1,40 @@
## Task 6: Jobs Page Redesign — Report
### Changes Made
**`templates/jobs.html`** — Complete rewrite from table layout to responsive card grid.
- Page heading changed from "All Jobs" to "Processing Jobs"
- Card grid layout with responsive columns: 1 col (mobile), 2 col (640px+), 3 col (1024px+)
- Each job card contains:
- Thumbnail image from `/preview/thumb/<job_id>` with SVG placeholder fallback on error
- Status badge (top-right corner) using `.badge-*` classes: success (COMPLETED), info (RUNNING), warning (PENDING), error (FAILED), neutral (CANCELLED)
- Truncated job ID (12 chars + ellipsis) with copy-to-clipboard button (green checkmark feedback)
- Video filename extracted from `job.video_path`
- Thin 4px progress bar with model count and percentage
- Relative timestamp (e.g. "2 minutes ago") with auto-update every 60s
- "View Details" link to `/status/<job_id>`
- Empty state with film-strip SVG icon, "No jobs yet" heading, and "Upload a video" CTA button
- Scoped `<style>` block with design system tokens (no new CSS in `style.css`)
- Inline `<script>` for relative time formatting and clipboard copy functionality
### Verification
- Template loads without Jinja2 errors
- Flask `/jobs` endpoint returns 200 OK
- Empty state renders correctly (no jobs by default)
- All required elements present in rendered HTML: page header, grid, card structure, thumbnails, badges, copy button, progress bars, relative times, empty state, CTA button
### Commit
`b1170f4` — `feat(ui): jobs page — card grid with badges and thumbnails`
---
### Review Fixes (3 findings)
1. **Non-semantic card container** — Changed `<div class="card job-card">` → `<article class="card job-card">` (and closing tag) for better screen reader navigation.
2. **`onclick` on copy button** — Removed inline `onclick="copyJobId(this, '{{ job.job_id }}')"` attribute; added `data-job-id="{{ job.job_id }}"` for identification; moved copy logic to `addEventListener('click', ...)` in the `<script>` block using `document.querySelectorAll('.copy-btn')`.
3. **Gradient on progress bar** — Replaced `linear-gradient(90deg, var(--color-primary-500), var(--color-primary-400))` with solid `var(--color-primary-500)` to match flat design system.
+20 -13
View File
@@ -190,7 +190,7 @@
.job-card-progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--color-primary-500), var(--color-primary-400));
background: var(--color-primary-500);
border-radius: 2px;
transition: width 0.4s ease;
}
@@ -227,7 +227,7 @@
{% if jobs %}
<div class="jobs-grid">
{% for job in jobs %}
<div class="card job-card">
<article class="card job-card">
<div class="job-card-thumb">
<img src="/preview/thumb/{{ job.job_id }}" alt="Thumbnail for job {{ job.job_id }}" loading="lazy"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
@@ -248,7 +248,7 @@
<div class="job-card-body">
<div class="job-card-header">
<span class="job-card-id" title="{{ job.job_id }}">{{ job.job_id[:12] }}{% if job.job_id|length > 12 %}&hellip;{% endif %}</span>
<button class="copy-btn" onclick="copyJobId(this, '{{ job.job_id }}')" title="Copy Job ID" aria-label="Copy Job ID">
<button class="copy-btn" data-job-id="{{ job.job_id }}" title="Copy Job ID" aria-label="Copy Job ID">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
</button>
</div>
@@ -267,7 +267,7 @@
<span class="job-card-time" data-timestamp="{{ job.created_at }}"></span>
<a href="/status/{{ job.job_id }}" class="job-card-link">View Details</a>
</div>
</div>
</article>
{% endfor %}
</div>
{% else %}
@@ -321,15 +321,22 @@
setInterval(updateTimes, 60000);
})();
function copyJobId(btn, id) {
navigator.clipboard.writeText(id).then(function() {
btn.classList.add('copied');
btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
setTimeout(function() {
btn.classList.remove('copied');
btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
}, 1500);
var copySvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
var checkSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
document.querySelectorAll('.copy-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var id = this.getAttribute('data-job-id');
var el = this;
navigator.clipboard.writeText(id).then(function() {
el.classList.add('copied');
el.innerHTML = checkSvg;
setTimeout(function() {
el.classList.remove('copied');
el.innerHTML = copySvg;
}, 1500);
});
});
}
});
</script>
{% endblock %}