12 KiB
Feedmill Recounter — Web UI Redesign
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Redesign the feedmill_recounter web UI from a basic dark theme to a clean, modern, user-friendly interface with live preview of processing, using a Flat Design system with teal primary (#0D9488), orange accent (#EA580C), Plus Jakarta Sans font, and dark-mode-ready tokens.
Current State: 4 templates (base.html, index.html, status.html, jobs.html), 1 CSS file (34 lines), no JS file. App.py serves on port 9000.
Tech Stack: HTML, CSS, vanilla JS (no new Python dependencies), Flask templates
Global Constraints
- Python >= 3.10 (uses
X | Yunion syntax) - No new Python dependencies for UI redesign (Flask + OpenCV already installed)
- Web UI port 9000 via
WEB_PORTenv variable - All model weights in
models/directory;.enginefiles are gitignored - Class filtering by name string (
"sack","box","truck"), not numeric ID - CSS uses Flat Design with teal primary (#0D9488), orange accent (#EA580C)
- Font: Plus Jakarta Sans (Google Fonts CDN)
- Dark-mode-ready CSS custom properties (light mode default, easy dark swap)
- Accessible: skip-link, focus rings, semantic HTML, ARIA where needed
- Responsive: mobile-first, works on 320px to 1920px+
- No JS frameworks — vanilla JS only
- Live preview: pipeline emits JPEG frame every Nth frame → Job.latest_frame → served as base64 in API → JS polls and displays
File Structure (changes only)
feedmill_recounter/
├── static/
│ ├── style.css # REWRITE — full Flat Design system
│ └── app.js # CREATE — JS for drag-drop, polling, preview
├── templates/
│ ├── base.html # REWRITE — semantic HTML, skip-link, font, tokens
│ ├── index.html # REWRITE — drag-drop upload, model cards
│ ├── status.html # REWRITE — progress bar, live preview, result cards
│ └── jobs.html # REWRITE — card grid with badges + thumbnails
├── src/
│ ├── preview.py # CREATE — extract_thumbnail, probe_video, extract_sample_frames
│ ├── pipeline.py # MODIFY — emit frame JPEG every Nth frame to job.latest_frame
│ └── job.py # MODIFY — add latest_frame: bytes | None field
└── app.py # MODIFY — add /preview/thumb, /api/jobs/<id>/samples, /api/jobs/<id>/frame routes
Task 1: Design System Foundation
Files:
- Rewrite:
feedmill_recounter/static/style.css - Rewrite:
feedmill_recounter/templates/base.html
Requirements:
-
style.css— complete rewrite with:- CSS custom properties (design tokens) for colors, spacing, typography, radii, shadows
- Teal primary:
--color-primary: #0D9488(with 50-900 scale shades) - Orange accent:
--color-accent: #EA580C(with 50-900 scale shades) - Neutrals:
--color-neutral-50through--color-neutral-900 - Status colors: success (#16A34A), warning (#F59E0B), error (#DC2626), info (#0EA5E9)
- Typography:
--font-sans: 'Plus Jakarta Sans', system-ui, sans-serif - Spacing scale:
--space-1(4px) through--space-16(64px) - Border radius:
--radius-sm(6px),--radius-md(8px),--radius-lg(12px),--radius-xl(16px) - Shadows:
--shadow-sm,--shadow-md,--shadow-lg - Transitions:
--transition-fast(150ms),--transition-normal(250ms) - Base reset/normalize
.containermax-width 1200px centered- Skip-link (visually hidden, visible on focus)
- Focus ring: 2px solid primary, offset 2px
- Button styles: primary (teal), secondary (outline), accent (orange), ghost
- Card component:
.cardwith padding, border-radius, shadow, border - Badge component:
.badgewith status variants - Form elements: inputs, selects, labels with consistent styling
- Responsive breakpoints: 640px, 768px, 1024px, 1280px
-
base.html— rewrite with:<!DOCTYPE html>,<html lang="en"><meta charset="UTF-8">,<meta name="viewport" content="width=device-width, initial-scale=1.0">- Google Fonts link for Plus Jakarta Sans (weights 400, 500, 600, 700)
- Skip-link:
<a href="#main" class="skip-link">Skip to content</a> - Semantic
<header>with<nav>containing Upload and Jobs links <main id="main">with{% block content %}{% endblock %}<footer>with project credit- Link to
style.css {% block head %}{% endblock %}for page-specific head content (scripts, meta)
Commit: feat(ui): design system foundation — CSS tokens, base template
Task 2: Upload Page Redesign
Files:
- Rewrite:
feedmill_recounter/templates/index.html - Create:
feedmill_recounter/static/app.js(drag-drop + video preview logic)
Requirements:
-
index.html— rewrite with:- Page heading: "Upload & Analyze"
- Drag-and-drop zone:
.upload-zonewith dashed border, icon, text "Drag video here or click to browse" - Hidden file input triggered by zone click
- Video preview:
<video>element showing selected file (controls, muted) - File info display: name, size, duration (if probed)
- Model selection section: grid of
.model-cardelements - Each model card: checkbox, model name, known classes as badges, class-filter dropdown
- "Select All" / "Deselect All" toggle for models
- Submit button: "Start Analysis" (primary, disabled until video + model selected)
- Loading state on submit: button text changes to "Starting..."
-
app.js— create with:initUploadZone(): click-to-browse, drag-drop handlers, file validationinitVideoPreview(): FileReader →<video>src, display file infoinitModelCards(): select-all toggle, enable/disable submit based on selectionsinitFormSubmit(): disable button on submit, show loading state- Progressive enhancement: works without JS (basic file input still functions)
Commit: feat(ui): upload page — drag-drop, video preview, model cards
Task 3: Preview Backend
Files:
- Create:
feedmill_recounter/src/preview.py - Modify:
feedmill_recounter/app.py— add preview routes
Requirements:
-
preview.py— create with:probe_video(video_path: str) -> dict: returns {width, height, fps, duration, frame_count} using cv2extract_thumbnail(video_path: str, output_path: str, time_sec: float = 1.0) -> str: extracts a single frame as JPEG, returns pathextract_sample_frames(video_path: str, output_dir: str, count: int = 6) -> list[str]: extracts evenly-spaced frames as JPEGs, returns paths
-
app.py— add routes:GET /preview/thumb/<job_id>— serves thumbnail JPEG for a job's video (extract on first request, cache in job output_dir)GET /api/jobs/<job_id>/samples— returns JSON array of sample frame paths (extract if not cached)GET /api/jobs/<job_id>/frame— returns current latest_frame as JPEG (for live preview during processing)
Commit: feat(api): video preview endpoints — thumbnail, sample frames, live frame
Task 4: Status Page Redesign
Files:
- Rewrite:
feedmill_recounter/templates/status.html - Modify:
feedmill_recounter/static/app.js— add polling + live preview logic
Requirements:
-
status.html— rewrite with:- Job header: job ID, status badge (color-coded), created time
- Progress section: animated progress bar with percentage label, current model name
- Live preview section:
<img>element that polls/api/jobs/<id>/frameevery 2s, shows annotated frame during processing - Video thumbnail: extracted from original video
- Results section: grid of result cards (not table)
- Each result card: model name, loading/unloading/net counts as big numbers, batch count, duration, download button
- Error display: alert-style error message if job failed
- Auto-refresh: JS polls
/api/jobs/<id>every 2s, updates progress bar + status badge + live preview + results
-
app.js— add:initStatusPage(): starts polling loopupdateProgressBar(progress): animates widthupdateLivePreview(jobId): fetches/api/jobs/<id>/frame, updates<img>srcupdateResults(results): renders result cards- Polling stops when status is COMPLETED, FAILED, or CANCELLED
Commit: feat(ui): status page — progress bar, live preview, result cards, polling
Task 5: Live Annotated Frame Emission
Files:
- Modify:
feedmill_recounter/src/job.py— addlatest_frame: bytes | Nonefield to Job - Modify:
feedmill_recounter/src/pipeline.py— emit JPEG frame every 10th frame to callback
Requirements:
-
job.py— add toJobdataclass:latest_frame: bytes | None = Nonefield- Thread-safe update: worker sets
job.latest_frame = jpeg_bytesunderself._lock
-
pipeline.py— modifyrun_pipeline():- Accept optional
frame_callback: Callable[[bytes], None] | None = Noneparameter - Every 10th frame (frame_idx % 10 == 0), encode frame as JPEG bytes via
cv2.imencode('.jpg', viz) - Call
frame_callback(jpeg_bytes)if provided - In
job.py_run_job(), pass callback that setsjob.latest_frame
- Accept optional
Commit: feat(pipeline): emit live annotated frames for preview
Task 6: Jobs Page Redesign
Files:
- Rewrite:
feedmill_recounter/templates/jobs.html
Requirements:
jobs.html— rewrite with:- Page heading: "Processing Jobs"
- Empty state: illustration + "No jobs yet" + "Upload a video" CTA
- Card grid layout (responsive: 1 col mobile, 2 col tablet, 3 col desktop)
- Each job card:
- Thumbnail (extracted from video, or placeholder)
- Status badge (top-right corner)
- Job ID (truncated, with copy button)
- Video filename
- Model count + progress bar (thin)
- Created time (relative: "2 minutes ago")
- "View Details" link
- Sort: newest first (already default from job_queue.list_jobs)
Commit: feat(ui): jobs page — card grid with badges and thumbnails
Task 7: Accessibility, Responsive, Polish
Files:
- Modify:
feedmill_recounter/static/style.css— add responsive utilities, a11y enhancements - Modify:
feedmill_recounter/static/app.js— add keyboard handlers, ARIA updates - Modify:
feedmill_recounter/templates/*.html— add ARIA attributes where needed
Requirements:
-
CSS additions:
@media (prefers-reduced-motion: reduce)— disable animations@media (prefers-color-scheme: dark)— dark mode token overrides- Print styles:
@media print— hide nav, buttons, show content .sr-onlyutility for screen-reader-only text- Focus-visible styles for keyboard navigation
-
JS additions:
- Keyboard: Enter/Space on upload zone triggers file input
- ARIA:
aria-live="polite"on progress region,aria-busyduring processing - Announce status changes to screen readers
-
HTML fixes:
- All images have
alttext - All interactive elements have accessible names
- Form inputs have associated labels
- Status changes announced via
aria-live
- All images have
Commit: fix(ui): accessibility, responsive, polish pass
Task 8: Final Verification
Files: None (verification only)
Requirements:
- Run existing tests:
cd /home/jetson/feedmill_semarang_project/feedmill_recounter && python -m pytest tests/ -v - Verify Flask app starts:
python -c "from app import app; print('OK')" - Verify templates render: manual check or curl localhost:9000
- Verify CSS loads: check static/style.css served correctly
- Verify JS loads: check static/app.js served correctly
- Verify all routes work: /, /jobs, /status/, /preview/thumb/, /api/jobs, /api/jobs/, /api/jobs//frame
Commit: none (verification only)