GSD: S03 complete — Frontend Core Vue 3 SPA

S03 delivered: Vue 3 + TypeScript + Vite + Pinia SPA with SSE integration,
URL submission, live format extraction, real-time progress, download queue,
responsive layout with mobile bottom tabs. 21 frontend + 122 backend tests.
Ready for S04: Admin, Auth + Supporting Features.
This commit is contained in:
xpltd 2026-03-18 18:55:02 -05:00
parent ea41640c17
commit 9d71d48c50
5 changed files with 362 additions and 1 deletions

View file

@ -65,7 +65,7 @@ This milestone is complete only when all are true:
- [x] **S02: SSE Transport + Session System** `risk:high` `depends:[S01]`
> After this: Open two browser tabs → each gets its own SSE stream scoped to their session cookie. Live progress events flow from yt-dlp worker threads through SSEBroker to the correct session's EventSource. Refresh a tab → SSE replays current state. Health endpoint responds. Proven via real SSE connections and session isolation test.
- [ ] **S03: Frontend Core** `risk:medium` `depends:[S02]`
- [x] **S03: Frontend Core** `risk:medium` `depends:[S02]`
> After this: Full Vue 3 SPA in the browser: paste URL, pick format from live extraction, watch progress bar fill, see completed files in queue. Playlists show as collapsible parent/child rows. Mobile layout (375px) uses bottom tabs, card list, ≥44px targets. Desktop uses sidebar + table. Proven by loading the SPA and completing a download flow.
- [ ] **S04: Admin, Auth + Supporting Features** `risk:medium` `depends:[S02]`

View file

@ -0,0 +1,107 @@
# S03: Frontend Core
**Goal:** Ship a functional Vue 3 SPA that lets a user paste a URL, pick format/quality from live extraction, submit a download, watch real-time SSE progress, and manage a download queue — with a responsive layout that works on both desktop (≥768px) and mobile (375px).
**Demo:** Open the browser → paste a YouTube URL → format picker populates → pick 720p → submit → progress bar fills via SSE → status changes to completed. Open a second tab → submit a different URL → both tabs show only their own session's downloads. Resize to 375px → layout shifts to mobile card view with bottom tabs.
## Must-Haves
- Vue 3 + TypeScript + Vite + Pinia project scaffolded and building cleanly
- API client with TypeScript types matching backend Job, ProgressEvent, FormatInfo models
- SSE composable managing EventSource lifecycle with reconnect and store dispatch
- Downloads Pinia store: reactive jobs map, SSE-driven updates, CRUD actions
- Config Pinia store: loads public config on app init
- URL input component with format picker populated from `GET /api/formats?url=`
- Download queue component with progress bars, status badges, speed/ETA, cancel buttons
- Responsive layout: desktop (header + main content area) and mobile (bottom tabs + card list)
- 44px minimum touch targets on mobile
- `npm run build` produces zero errors
- `vue-tsc --noEmit` passes with zero type errors
- Vitest tests for stores and SSE composable
## Proof Level
- This slice proves: integration (frontend SPA consuming real backend SSE stream, session cookie isolation across tabs)
- Real runtime required: yes (SSE streaming, format extraction, cookie handling)
- Human/UAT required: yes (visual layout verification at desktop + mobile breakpoints)
## Verification
- `cd frontend && npm run build` — zero errors, dist/ produced
- `cd frontend && npx vue-tsc --noEmit` — zero type errors
- `cd frontend && npx vitest run` — all store and composable tests pass
- Browser verification: open SPA against running backend, complete a download flow with live progress
- Browser verification: 375px viewport shows mobile layout with bottom tabs and card list
- Session isolation: two browser tabs with different cookies see different job lists
## Observability / Diagnostics
- Runtime signals: console.log for SSE connect/disconnect/reconnect events during development; downloads store exposes `connectionStatus` ref (connected/disconnected/reconnecting)
- Inspection surfaces: Vue devtools shows Pinia store state (jobs, config); browser Network tab shows SSE stream; browser Application tab shows mrip_session cookie
- Failure visibility: SSE composable logs reconnect attempts with count; failed API calls surface error messages in the UI (toast or inline)
- Redaction constraints: none (session UUIDs are opaque, no secrets in frontend)
## Integration Closure
- Upstream surfaces consumed: `GET/POST/DELETE /api/downloads`, `GET /api/formats?url=`, `GET /api/events` (SSE), `GET /api/config/public`, `GET /api/health`, session cookie from SessionMiddleware
- New wiring introduced in this slice: Vite dev proxy to backend, Vue app mounting, Pinia store initialization, SSE EventSource connection
- What remains before the milestone is truly usable end-to-end: S04 (admin panel), S05 (theme system with CSS variable contract), S06 (Docker + CI/CD)
## Tasks
- [x] **T01: Scaffold Vue 3 + Vite + TypeScript + Pinia project** `est:30m`
- Why: Foundation for all frontend work. Must build cleanly before any components can be written.
- Files: `frontend/package.json`, `frontend/vite.config.ts`, `frontend/tsconfig.json`, `frontend/tsconfig.node.json`, `frontend/src/main.ts`, `frontend/src/App.vue`, `frontend/index.html`
- Do: Create Vue 3 + TS project with Vite. Install pinia and vue-router (for future S04 use). Configure vite.config.ts with proxy: `/api``http://localhost:8000`. Set up minimal App.vue with Pinia. Add vitest config. Add a minimal dark CSS baseline using custom properties (--color-bg, --color-text, --color-accent, --color-surface) that S05 will expand. No Tailwind. Include a `src/api/types.ts` with TypeScript interfaces matching backend models (Job, JobStatus, ProgressEvent, FormatInfo, PublicConfig).
- Verify: `cd frontend && npm run build` succeeds, `npx vue-tsc --noEmit` passes
- Done when: `npm run dev` serves the app at localhost:5173, build produces dist/, type-check passes, vitest runs (0 tests is fine)
- [x] **T02: API client, Pinia stores, and SSE composable** `est:1h`
- Why: The data layer that every component depends on. SSE is the highest-risk integration point — if events don't flow from backend to store, nothing works.
- Files: `frontend/src/api/client.ts`, `frontend/src/stores/downloads.ts`, `frontend/src/stores/config.ts`, `frontend/src/composables/useSSE.ts`, `frontend/src/tests/stores/downloads.test.ts`, `frontend/src/tests/composables/useSSE.test.ts`
- Do: Build fetch-based API client (`api/client.ts`) with GET/POST/DELETE helpers, base URL from import.meta.env or proxy. Build downloads store: `jobs` as reactive Map<string, Job>, actions for `fetchJobs()`, `submitDownload(url, formatId?, quality?)`, `cancelDownload(id)`, internal `_handleInit(jobs)`, `_handleJobUpdate(event)`, `_handleJobRemoved(jobId)`. Build config store: `config` ref, `loadConfig()` action calling GET /api/config/public. Build `useSSE()` composable: creates EventSource to /api/events, parses SSE events, dispatches to downloads store, handles reconnect with exponential backoff (1s, 2s, 4s, max 30s), exposes `connectionStatus` ref. Write vitest tests: downloads store CRUD operations (mock fetch), SSE composable event parsing and store dispatch (mock EventSource).
- Verify: `cd frontend && npx vitest run` — store and composable tests pass
- Done when: Downloads store reactively updates from SSE events, config store loads public config, SSE composable reconnects on disconnect, all tests pass
- [x] **T03: URL input + format picker components** `est:45m`
- Why: The primary user interaction — pasting a URL and selecting quality. Format extraction is async (3-10s) and needs loading UX.
- Files: `frontend/src/components/UrlInput.vue`, `frontend/src/components/FormatPicker.vue`, `frontend/src/App.vue`
- Do: UrlInput.vue: text input with paste handler, Submit button, calls `GET /api/formats?url=` on submit (or on debounced input). Shows loading spinner during extraction. On format response, shows FormatPicker. FormatPicker.vue: dropdown/list showing resolution, codec, ext, filesize for each format. "Best available" as default option. Submit button calls downloads store `submitDownload()`. Handle edge cases: no formats returned (show "Best available" only), extraction error (show error message), empty URL (disable submit). Optional "More options" expandable area with output_template override (R025).
- Verify: Visual verification in browser — paste URL, see format picker populate, submit download
- Done when: User can paste a URL, see formats load, select one, and submit. Error states handled gracefully.
- [x] **T04: Download queue + progress display** `est:45m`
- Why: The core feedback loop — users need to see their downloads progressing in real-time.
- Files: `frontend/src/components/DownloadQueue.vue`, `frontend/src/components/DownloadItem.vue`, `frontend/src/components/ProgressBar.vue`, `frontend/src/App.vue`
- Do: DownloadQueue.vue: renders list of DownloadItem components from downloads store jobs. Status filter tabs (All / Active / Completed / Failed). Empty state message when no downloads. DownloadItem.vue: shows URL/filename, status badge (queued=gray, downloading=blue, completed=green, failed=red), ProgressBar with percent + speed + ETA, cancel button (calls store.cancelDownload). ProgressBar.vue: animated CSS bar, displays percent text. Wire SSE events: job_update → progress bar updates in real-time, job_removed → item disappears. Handle status transitions: queued → extracting → downloading → completed/failed.
- Verify: Visual verification — submit a download, watch progress bar fill from SSE events, see status change to completed
- Done when: Queue shows all session jobs with live progress, cancel works, status badges reflect current state, completed/failed jobs show final state
- [x] **T05: Responsive layout + mobile view** `est:45m`
- Why: R013 requires mobile-responsive layout. >50% of self-hoster interactions happen on phone/tablet.
- Files: `frontend/src/components/AppLayout.vue`, `frontend/src/components/AppHeader.vue`, `frontend/src/App.vue`, `frontend/src/assets/base.css`
- Do: AppLayout.vue: responsive shell. Desktop (≥768px): header bar with title, main content area with URL input at top, queue below. Mobile (<768px): bottom tab bar (Submit / Queue tabs), URL input fills width, queue uses card layout instead of table rows. AppHeader.vue: app title/logo, connection status indicator. Base CSS: set up CSS custom properties for colors, spacing, typography that S05 will formalize into the theme contract. Use system font stack for now (S05 brings JetBrains Mono). Ensure all interactive elements have minimum 44px touch targets on mobile. Test at 375px (iPhone SE) and 768px breakpoint.
- Verify: Browser verification at 375px and 1280px viewports. All interactive elements ≥44px on mobile.
- Done when: Desktop layout shows header + content. Mobile layout shows bottom tabs + card view. 375px viewport is usable. Touch targets meet 44px minimum.
## Files Likely Touched
- `frontend/` — entire new directory
- `frontend/package.json`
- `frontend/vite.config.ts`
- `frontend/tsconfig.json`
- `frontend/index.html`
- `frontend/src/main.ts`
- `frontend/src/App.vue`
- `frontend/src/api/client.ts`
- `frontend/src/api/types.ts`
- `frontend/src/stores/downloads.ts`
- `frontend/src/stores/config.ts`
- `frontend/src/composables/useSSE.ts`
- `frontend/src/components/UrlInput.vue`
- `frontend/src/components/FormatPicker.vue`
- `frontend/src/components/DownloadQueue.vue`
- `frontend/src/components/DownloadItem.vue`
- `frontend/src/components/ProgressBar.vue`
- `frontend/src/components/AppLayout.vue`
- `frontend/src/components/AppHeader.vue`
- `frontend/src/assets/base.css`

View file

@ -0,0 +1,127 @@
# S03: Frontend Core — Research
## Scope
Full Vue 3 SPA consuming the S01/S02 backend: URL submission → format selection → real-time progress via SSE → completed downloads queue. Mobile-first responsive layout. No theming system yet (S05) — use simple CSS custom properties with a minimal dark style.
## API Surface to Consume
From S01:
- `POST /api/downloads` — submit URL + optional format_id/quality/output_template
- `GET /api/downloads` — list all jobs for current session
- `DELETE /api/downloads/{id}` — cancel/remove a job
- `GET /api/formats?url=` — live yt-dlp format extraction
From S02:
- `GET /api/events` — SSE stream (init, job_update, job_removed, ping)
- `GET /api/health` — health check
- `GET /api/config/public` — session_mode, default_theme, purge_enabled, max_concurrent_downloads
- Session cookie auto-set by middleware (no auth header needed)
## SSE Event Contract
```
event: init
data: {"jobs": [<Job>, ...]}
event: job_update
data: {"job_id": "...", "status": "...", "percent": ..., "speed": "...", "eta": "...", ...}
event: job_removed
data: {"job_id": "..."}
event: ping
data: ""
```
## Frontend Architecture
### Project Structure
```
frontend/
index.html
vite.config.ts
tsconfig.json
tsconfig.node.json
package.json
src/
main.ts
App.vue
api/
client.ts — fetch wrapper with base URL
types.ts — TypeScript types matching backend models
stores/
downloads.ts — Pinia store: job state, SSE connection, CRUD actions
config.ts — Pinia store: public config from /api/config/public
components/
UrlInput.vue — URL paste + submit + format selection
FormatPicker.vue — Format/quality dropdown populated from /api/formats
DownloadQueue.vue — Job list with progress bars, status badges, cancel
DownloadItem.vue — Single job row (desktop: table row, mobile: card)
ProgressBar.vue — Animated progress bar component
AppHeader.vue — Header with logo/title
AppLayout.vue — Responsive layout shell (header + main + mobile nav)
composables/
useSSE.ts — EventSource connection management + reconnect
```
### Key Decisions
1. **No router needed for S03** — single-page app with URL input + queue. Router can be added in S04 for admin panel.
2. **SSE in a composable, not the store**`useSSE()` composable manages EventSource lifecycle, reconnect logic, and dispatches events to the downloads store. Store stays pure state.
3. **Fetch, not axios** — per stack research. Native fetch + a thin wrapper for base URL and error handling.
4. **CSS custom properties for styling** — establish a minimal set that S05 will expand. No Tailwind (per original stack decisions). No component library — hand-rolled.
5. **Vite dev proxy** — proxy `/api` to `http://localhost:8000` during development so CORS is not an issue.
6. **Playlist support deferred within S03** — The R006 parent/child playlist model requires backend changes (parent_job_id field, playlist extraction creating child jobs). The frontend can show the data once it exists, but the backend work is not in S02. We'll build the DownloadItem component to support a `children` array, but full playlist support comes when the backend supports it (likely S04 or a dedicated slice). For now, individual URL downloads are the focus.
## Task Breakdown (Risk-Ordered)
### T01: Scaffold Vue 3 + Vite + TypeScript + Pinia project
- `npm create vite@latest frontend -- --template vue-ts`
- Install pinia
- Configure vite proxy to backend
- Verify `npm run dev` serves a blank page
- Verify `npm run build` produces dist/
- Risk: LOW — standard scaffold
### T02: API client, TypeScript types, and Pinia stores
- Type definitions matching backend Job, ProgressEvent, FormatInfo, PublicConfig
- Fetch-based API client with error handling
- Downloads store: jobs map, addJob, updateJob, removeJob, fetchJobs actions
- Config store: load public config on app init
- SSE composable: EventSource to /api/events, reconnect on close, dispatch to store
- Risk: MEDIUM — SSE reconnect logic needs careful handling
### T03: URL input + format picker components
- UrlInput.vue: paste/type URL, submit button, loading state during format extraction
- FormatPicker.vue: populated from /api/formats response, shows resolution/codec/ext/filesize
- Wire to downloads store: submit → POST /api/downloads
- Risk: MEDIUM — format extraction can be slow (3-10s), needs good loading UX
### T04: Download queue + progress display
- DownloadQueue.vue: list of DownloadItem components, filter by status
- DownloadItem.vue: status badge, progress bar, speed/ETA, cancel button
- ProgressBar.vue: animated fill bar
- Wire to downloads store SSE updates
- Risk: LOW-MEDIUM — straightforward rendering, SSE wiring already done
### T05: Responsive layout (desktop + mobile)
- AppLayout.vue: desktop sidebar + main content, mobile bottom tabs + card view
- Breakpoint at 768px
- Mobile: bottom tab bar (Submit/Queue), full-width URL input, card list
- Desktop: header bar, URL input at top, table-style queue below
- 44px minimum touch targets on mobile
- Risk: MEDIUM — responsive CSS without a framework requires care
## Verification Strategy
- `npm run build` — zero errors
- `vue-tsc --noEmit` — TypeScript checks pass
- Vitest unit tests for stores (downloads, config) and SSE composable
- Manual browser verification against running backend
- Mobile layout verification at 375px viewport

View file

@ -0,0 +1,95 @@
---
id: S03
milestone: M001
status: complete
tasks_completed: 5
tasks_total: 5
test_count_frontend: 21
test_count_backend: 122
started_at: 2026-03-18
completed_at: 2026-03-18
---
# S03: Frontend Core — Summary
**Delivered a complete Vue 3 SPA consuming the S01/S02 backend: URL submission, live format extraction, real-time SSE progress, download queue with filters, and responsive layout with mobile bottom tabs. 21 frontend tests + 122 backend tests pass.**
## What Was Built
### Project Foundation (T01)
- Vue 3.5 + TypeScript + Vite 6.4 + Pinia scaffolded
- Vite dev proxy: `/api``http://localhost:8000`
- CSS custom properties dark theme baseline (S05 will formalize)
- TypeScript interfaces matching all backend models
### Data Layer (T02)
- **API client** (`api/client.ts`): Fetch-based GET/POST/DELETE with error handling via `ApiError` class
- **Downloads store** (`stores/downloads.ts`): Reactive `Map<string, Job>`, SSE event handlers (`handleInit`, `handleJobUpdate`, `handleJobRemoved`), CRUD actions, computed getters (jobList, activeJobs, completedJobs, failedJobs)
- **Config store** (`stores/config.ts`): Loads `GET /api/config/public` on app init
- **SSE composable** (`composables/useSSE.ts`): EventSource to `/api/events`, exponential backoff reconnect (1s → 30s max), `connectionStatus` ref, dispatches events to downloads store
### UI Components (T03-T05)
- **UrlInput**: Text input with paste auto-extract, loading spinner during format extraction, form reset on submit
- **FormatPicker**: Grouped display (video+audio / video-only / audio-only), codec and filesize info, "Best available" default
- **DownloadQueue**: Filtered job list with All/Active/Completed/Failed tabs and counts, animated TransitionGroup
- **DownloadItem**: Filename display, status badge with color-coded left border, speed/ETA, cancel button
- **ProgressBar**: Animated CSS fill bar with percentage text overlay
- **AppHeader**: Logo with "media.rip()" monospace title, SSE connection status dot
- **AppLayout**: Responsive shell — desktop (header + main content), mobile (<768px: bottom tab bar + section toggling)
## Key Decisions
- No vue-router for S03 — single-page with tabs. Router deferred to S04 for admin panel
- SSE lives in a composable, not the store — separation of transport from state
- Native fetch, not axios — per stack research
- Status normalization: yt-dlp "finished" → our "completed" in store handler
- CSS custom properties (not Tailwind, not component library) — hand-rolled for full theme control
## Requirements Addressed
| Req | Description | Status |
|-----|------------|--------|
| R002 | Format/quality extraction and selection | Proven — FormatPicker populated from live /api/formats |
| R003 | Real-time SSE progress | Proven — job_update events flow to DownloadItem progress bars |
| R005 | Download queue view, cancel, filter | Proven — DownloadQueue with status filters and cancel |
| R013 | Mobile-responsive layout | Proven — 375px viewport with bottom tabs, card list |
| R025 | Per-download output template override | Stubbed — UI structure ready, input wiring deferred |
## Verification
- `vue-tsc --noEmit` — zero type errors
- `npm run build` — clean production build (88KB JS + 11KB CSS gzipped: 34KB + 2.6KB)
- `vitest run` — 21/21 tests pass (4 test files)
- Browser verification: complete download flow with real yt-dlp against YouTube
- Mobile verification: 375px viewport shows bottom tabs, stacked layout
## Test Coverage
| Test File | Tests | Focus |
|-----------|-------|-------|
| types.test.ts | 1 | Type sanity |
| stores/downloads.test.ts | 13 | handleInit, handleJobUpdate, handleJobRemoved, computed getters, isTerminal, status normalization |
| stores/config.test.ts | 3 | Initial state, successful load, error handling |
| composables/useSSE.test.ts | 4 | Store dispatch patterns, MockEventSource lifecycle |
## Files Created
- `frontend/package.json`, `frontend/vite.config.ts`, `frontend/tsconfig.json`, `frontend/tsconfig.node.json`
- `frontend/index.html`, `frontend/env.d.ts`, `frontend/src/main.ts`
- `frontend/src/App.vue`
- `frontend/src/api/client.ts`, `frontend/src/api/types.ts`
- `frontend/src/stores/downloads.ts`, `frontend/src/stores/config.ts`
- `frontend/src/composables/useSSE.ts`
- `frontend/src/components/UrlInput.vue`, `frontend/src/components/FormatPicker.vue`
- `frontend/src/components/DownloadQueue.vue`, `frontend/src/components/DownloadItem.vue`
- `frontend/src/components/ProgressBar.vue`
- `frontend/src/components/AppHeader.vue`, `frontend/src/components/AppLayout.vue`
- `frontend/src/assets/base.css`
- `frontend/src/tests/**` (4 test files)
## What S04/S05 Consumes
- Vue component structure referencing CSS custom properties → S05 formalizes the theme contract
- AppLayout slot pattern → S04 can add admin routes alongside
- Pinia stores → S04 admin panel can extend with admin-specific stores
- SSE composable pattern → reusable for any future real-time features

View file

@ -0,0 +1,32 @@
---
estimated_steps: 5
estimated_files: 10
---
# T01: Scaffold Vue 3 + Vite + TypeScript + Pinia project
**Slice:** S03 — Frontend Core
**Milestone:** M001
## Description
Create the frontend project from scratch with Vue 3, TypeScript, Vite, and Pinia. Configure the Vite dev proxy so `/api` routes hit the FastAPI backend. Set up vitest for testing. Define TypeScript interfaces matching the backend models. Establish a minimal dark CSS baseline.
## Steps
1. Create `frontend/` directory in the worktree
2. Initialize with `npm create vite@latest` (vue-ts template) or manually scaffold
3. Install runtime deps: `vue`, `pinia`
4. Install dev deps: `vitest`, `vue-tsc`, `@vitejs/plugin-vue`, `typescript`
5. Configure `vite.config.ts` with proxy `/api``http://localhost:8000`
6. Set up `tsconfig.json` and `tsconfig.node.json`
7. Create `src/api/types.ts` with TypeScript interfaces
8. Create minimal `src/assets/base.css` with CSS custom properties
9. Update `App.vue` and `main.ts` with Pinia setup
10. Verify: `npm run build`, `npx vue-tsc --noEmit`, `npx vitest run`
## Verification
- `cd frontend && npm run build` — zero errors
- `cd frontend && npx vue-tsc --noEmit` — zero type errors
- `cd frontend && npx vitest run` — runs (0 tests ok, framework functional)