Files
feedmill-recounter/docs/superpowers/plans/2026-09-17-web-ui-redesign.md
T

261 lines
12 KiB
Markdown

# 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 | Y` union syntax)
- No new Python dependencies for UI redesign (Flask + OpenCV already installed)
- Web UI port 9000 via `WEB_PORT` env variable
- All model weights in `models/` directory; `.engine` files 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:**
1. `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-50` through `--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
- `.container` max-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: `.card` with padding, border-radius, shadow, border
- Badge component: `.badge` with status variants
- Form elements: inputs, selects, labels with consistent styling
- Responsive breakpoints: 640px, 768px, 1024px, 1280px
2. `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:**
1. `index.html` — rewrite with:
- Page heading: "Upload & Analyze"
- Drag-and-drop zone: `.upload-zone` with 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-card` elements
- 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..."
2. `app.js` — create with:
- `initUploadZone()`: click-to-browse, drag-drop handlers, file validation
- `initVideoPreview()`: FileReader → `<video>` src, display file info
- `initModelCards()`: select-all toggle, enable/disable submit based on selections
- `initFormSubmit()`: 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:**
1. `preview.py` — create with:
- `probe_video(video_path: str) -> dict`: returns {width, height, fps, duration, frame_count} using cv2
- `extract_thumbnail(video_path: str, output_path: str, time_sec: float = 1.0) -> str`: extracts a single frame as JPEG, returns path
- `extract_sample_frames(video_path: str, output_dir: str, count: int = 6) -> list[str]`: extracts evenly-spaced frames as JPEGs, returns paths
2. `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:**
1. `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>/frame` every 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
2. `app.js` — add:
- `initStatusPage()`: starts polling loop
- `updateProgressBar(progress)`: animates width
- `updateLivePreview(jobId)`: fetches `/api/jobs/<id>/frame`, updates `<img>` src
- `updateResults(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` — add `latest_frame: bytes | None` field to Job
- Modify: `feedmill_recounter/src/pipeline.py` — emit JPEG frame every 10th frame to callback
**Requirements:**
1. `job.py` — add to `Job` dataclass:
- `latest_frame: bytes | None = None` field
- Thread-safe update: worker sets `job.latest_frame = jpeg_bytes` under `self._lock`
2. `pipeline.py` — modify `run_pipeline()`:
- Accept optional `frame_callback: Callable[[bytes], None] | None = None` parameter
- 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 sets `job.latest_frame`
**Commit:** `feat(pipeline): emit live annotated frames for preview`
---
### Task 6: Jobs Page Redesign
**Files:**
- Rewrite: `feedmill_recounter/templates/jobs.html`
**Requirements:**
1. `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:**
1. 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-only` utility for screen-reader-only text
- Focus-visible styles for keyboard navigation
2. JS additions:
- Keyboard: Enter/Space on upload zone triggers file input
- ARIA: `aria-live="polite"` on progress region, `aria-busy` during processing
- Announce status changes to screen readers
3. HTML fixes:
- All images have `alt` text
- All interactive elements have accessible names
- Form inputs have associated labels
- Status changes announced via `aria-live`
**Commit:** `fix(ui): accessibility, responsive, polish pass`
---
### Task 8: Final Verification
**Files:** None (verification only)
**Requirements:**
1. Run existing tests: `cd /home/jetson/feedmill_semarang_project/feedmill_recounter && python -m pytest tests/ -v`
2. Verify Flask app starts: `python -c "from app import app; print('OK')"`
3. Verify templates render: manual check or curl localhost:9000
4. Verify CSS loads: check static/style.css served correctly
5. Verify JS loads: check static/app.js served correctly
6. Verify all routes work: /, /jobs, /status/<id>, /preview/thumb/<id>, /api/jobs, /api/jobs/<id>, /api/jobs/<id>/frame
**Commit:** none (verification only)