bb8b040657
Update docs to reflect that Manage no longer scrapes its own system metrics (slices 1-2). AGENTS.md, REQUIREMENTS.md (decision log + observability section), monitoring-logging-design.md, MIGRATION_PLAN.md. Gate: docs only; backend pytest (173) + frontend build/lint/test (22/63) remain green from slices 1-2.
10 KiB
10 KiB
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? (legacy/removed) |
/api/monitoring/metrics |
GET | resources.read_resource_metrics() |
Last-hour JSONL samples (legacy/removed) |
/api/monitoring/disk |
GET | resources.disk_space() |
df for media root (removed 2026-06-17; metrics now in Prometheus/Grafana) |
/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) streamlitandstreamlit-aggriddependencies
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)
- Dashboard (simplest — just fetches and displays)
- Monitoring (charts + controls)
- Media (AG Grid + filters)
- 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)