Files
manage/docs/MIGRATION_PLAN.md
T
Developer bb8b040657 docs(monitoring): record legacy poller decommission (slice 3)
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.
2026-06-17 20:55:24 +00:00

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)
  • 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)