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

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 | 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/, /preview/thumb/, /api/jobs, /api/jobs/, /api/jobs//frame

Commit: none (verification only)