# Migration Plan: Streamlit → FastAPI + React ## Overview Split the current Streamlit monolith into: - **Backend:** FastAPI (Python) serving a REST API - **Frontend:** React SPA (TypeScript) consuming that API The existing `clients/`, `domain/`, `services/`, `jobs.py`, `utils.py`, and `config.py` are already UI-independent and transfer directly to the FastAPI backend with minimal changes. --- ## Current Architecture ``` src/media_library_viewer/ ├── app.py # Streamlit orchestration + caching (DELETE) ├── config.py # Env/dotenv config ├── jobs.py # SSH job templates ├── utils.py # Formatting helpers ├── clients/ │ ├── jellyfin.py # Jellyfin HTTP client │ ├── resources.py # Remote resource collector │ └── ssh.py # SSH command execution ├── domain/ │ └── media.py # Media normalization ├── services/ │ └── media_index.py # SQLite media index └── ui/ # Streamlit rendering (DELETE) ├── dashboard.py ├── file_browser.py ├── media.py └── preview.py ``` --- ## Target Architecture ``` repo/ ├── backend/ │ ├── main.py # FastAPI app + CORS + lifespan │ ├── config.py # pydantic-settings based config │ ├── dependencies.py # DI for SSH/Jellyfin clients │ ├── routers/ │ │ ├── dashboard.py # /api/dashboard/* │ │ ├── monitoring.py # /api/monitoring/* │ │ ├── media.py # /api/media/* │ │ ├── files.py # /api/files/* │ │ └── jobs.py # /api/jobs/* │ ├── clients/ # Copied unchanged │ │ ├── jellyfin.py │ │ ├── resources.py │ │ └── ssh.py │ ├── domain/ # Copied unchanged │ │ └── media.py │ ├── services/ # Copied unchanged │ │ └── media_index.py │ ├── jobs.py # Copied unchanged │ ├── utils.py # Copied unchanged │ └── pyproject.toml │ ├── frontend/ │ ├── package.json │ ├── vite.config.ts │ ├── tsconfig.json │ └── src/ │ ├── main.tsx │ ├── App.tsx │ ├── api/ │ │ └── client.ts # Typed fetch wrappers │ ├── hooks/ │ │ ├── useDashboard.ts │ │ ├── useMonitoring.ts │ │ ├── useMedia.ts │ │ └── useFiles.ts │ ├── pages/ │ │ ├── Dashboard.tsx │ │ ├── Monitoring.tsx │ │ ├── Media.tsx │ │ └── FileBrowser.tsx │ ├── components/ │ │ ├── NowPlaying.tsx │ │ ├── MetricCard.tsx │ │ ├── LibraryOverview.tsx │ │ ├── MonitoringCharts.tsx │ │ ├── MediaTable.tsx │ │ ├── FileListing.tsx │ │ └── FfprobePreview.tsx │ └── types/ │ └── index.ts │ ├── docker-compose.yml # Optional unified deployment └── README.md ``` --- ## FastAPI Endpoints | Endpoint | Method | Source | Description | | ------------------------------- | ------ | ---------------------------------------- | -------------------------------- | | `/api/dashboard/counts` | GET | `jellyfin.media_counts()` | Movie/series/episode totals | | `/api/dashboard/libraries` | GET | `jellyfin.library_item_counts()` | Per-library breakdown | | `/api/dashboard/now-playing` | GET | `jellyfin.active_sessions()` | Active sessions + transcode info | | `/api/monitoring/status` | GET | `resources.resource_collector_status()` | Collector running? | | `/api/monitoring/metrics` | GET | `resources.read_resource_metrics()` | Last-hour JSONL samples | | `/api/monitoring/disk` | GET | `resources.disk_space()` | df for media root | | `/api/monitoring/start` | POST | `resources.start_resource_collector()` | Start collector | | `/api/monitoring/stop` | POST | `resources.stop_resource_collector()` | Stop collector | | `/api/monitoring/restart` | POST | `resources.restart_resource_collector()` | Restart collector | | `/api/media/status` | GET | `MediaIndex.status()` | Index exists/count/age | | `/api/media/build` | POST | `build_media_index()` | Rebuild index | | `/api/media/query` | GET | `MediaIndex.query()` | Filtered/sorted/paged query | | `/api/files/list?path=` | GET | `ssh.list_dir()` | Directory listing | | `/api/files/ffprobe?path=` | GET | `ssh.ffprobe_json()` | ffprobe JSON | | `/api/files/stat?path=` | GET | `ssh.stat_path()` | stat output | | `/api/files/resolve-path?path=` | GET | `resolve_remote_media_path()` | Jellyfin→SSH path mapping | | `/api/jobs/templates` | GET | `JOB_TEMPLATES` | Available job list | | `/api/jobs/run` | POST | `run_job()` | Execute a job | --- ## React Frontend ### Tech Stack - **Vite** + React 18+ + TypeScript - **@tanstack/react-query** — data fetching with polling - **ag-grid-react** — media table and file browser (same grid lib) - **recharts** — monitoring line charts - **react-router** — page navigation - **tailwindcss** + **shadcn/ui** — styling ### Page → Component Mapping | Page | Components | Polling | | ------------ | --------------------------------------------------------- | -------------------------------- | | Dashboard | NowPlaying, MetricCard (server overview), LibraryOverview | 15s (now-playing), 30s (metrics) | | Monitoring | MonitoringCharts, MetricCard, CollectorControls | 15s | | Media | MediaTable (AG Grid), filter/sort/page controls | on-demand | | File Browser | FileListing (AG Grid), FfprobePreview, JobRunner | on-demand | ### Key Interactions - Media row click → updates client-side file browser path state (no API call) - File browser directory click → fetches `/api/files/list?path=...` - File browser file click → fetches `/api/files/ffprobe?path=...` - Path input Enter → navigates directory - Monitoring charts → recharts line chart from `/api/monitoring/metrics` --- ## What Transfers Unchanged (~1,350 lines) | File | Lines | Notes | | ------------------------- | ----- | ---------------------------- | | `clients/jellyfin.py` | 167 | Remove unused methods if any | | `clients/ssh.py` | 146 | No changes | | `clients/resources.py` | 316 | No changes | | `domain/media.py` | 161 | No changes | | `services/media_index.py` | 278 | No changes | | `jobs.py` | 59 | No changes | | `utils.py` | 227 | No changes | --- ## What Gets Deleted - `src/media_library_viewer/ui/` (all Streamlit rendering) - `src/media_library_viewer/app.py` (Streamlit orchestration) - Root `app.py` (Streamlit launcher) - `streamlit` and `streamlit-aggrid` dependencies --- ## Migration Steps ### Step 1: Backend scaffold - Create `backend/` with FastAPI, pydantic-settings config, DI - Copy `clients/`, `domain/`, `services/`, `jobs.py`, `utils.py` - Implement routers wrapping existing functions - Test with curl ### Step 2: Frontend scaffold - Create `frontend/` with Vite + React + TypeScript - API client with typed wrappers - React Query provider + hooks - Router with 4 pages ### Step 3: Build pages (one at a time) 1. Dashboard (simplest — just fetches and displays) 2. Monitoring (charts + controls) 3. Media (AG Grid + filters) 4. File Browser (AG Grid + ffprobe + jobs) ### Step 4: Validate feature parity - Compare behavior side-by-side with Streamlit - Remove Streamlit code --- ## Effort Estimate | Component | Effort | | ------------------------------------- | ------------- | | FastAPI backend | 1-2 days | | React scaffold + routing + API client | 0.5 day | | Dashboard page | 0.5 day | | Monitoring page + charts | 1 day | | Media page + AG Grid | 1 day | | File browser page | 1 day | | Polish + testing + deployment | 1-2 days | | **Total** | **~6-9 days** | --- ## Key Decisions - **Polling over WebSocket initially** — simpler, matches current behavior; upgrade path exists for later. - **Auth deferred** — rely on network access control for now; add API key or JWT later. - **CORS enabled** — for Vite dev server and production origin. - **Path resolution stays server-side** — frontend sends Jellyfin paths, backend resolves to SSH paths. - **Both can coexist** — Streamlit and FastAPI can run simultaneously during transition since they share the same clients/services. --- ## Optional Post-Migration Enhancements - WebSocket push for real-time monitoring/now-playing - JWT authentication - Background index build with SSE progress - Dark/light theme - Persistent user preferences (localStorage + optional backend sync) - Docker Compose (backend + frontend + nginx reverse proxy)