Add FastAPI backend and React frontend subprojects

Backend:
- FastAPI app with 17 REST endpoints covering dashboard, monitoring,
  media index, file browser, and jobs
- Reuses existing clients/domain/services unchanged
- pydantic-settings config, dependency injection, CORS setup
- Auto-generated OpenAPI docs at /docs

Frontend:
- Vite + React + TypeScript SPA
- @tanstack/react-query for data fetching with polling
- ag-grid-react for media table and file browser
- recharts for monitoring charts
- Tailwind CSS styling
- 4 pages: Dashboard, Monitoring, Media, File Browser
- Typed API client matching all backend endpoints

Also:
- docs/MIGRATION_PLAN.md with full architecture plan
- Updated .gitignore for both subprojects
- Streamlit app preserved for now (can coexist)
This commit is contained in:
2026-04-30 21:40:18 +02:00
parent 1acdfbc6ba
commit 3c432473e5
63 changed files with 7778 additions and 202 deletions
+115
View File
@@ -0,0 +1,115 @@
import { useCounts, useLibraries, useNowPlaying } from "../hooks/useDashboard";
import { useMonitoringMetrics, useDiskSpace } from "../hooks/useMonitoring";
import { NowPlaying } from "../components/NowPlaying";
import { MetricCard } from "../components/MetricCard";
import { LibraryOverview } from "../components/LibraryOverview";
function formatBytes(bytes: number): string {
if (!bytes || bytes === 0) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
let value = bytes;
let unitIdx = 0;
while (value >= 1000 && unitIdx < units.length - 1) {
value /= 1000;
unitIdx++;
}
return `${value.toFixed(1)} ${units[unitIdx]}`;
}
function formatRate(bytes: number): string {
return `${formatBytes(bytes)}/s`;
}
export function Dashboard() {
const { data: counts } = useCounts();
const { data: libraries } = useLibraries();
const { data: nowPlaying } = useNowPlaying();
const { data: metrics } = useMonitoringMetrics();
const { data: disk } = useDiskSpace();
const latest = metrics?.samples?.at(-1);
return (
<div className="space-y-8">
{/* Now Playing */}
<section>
<h2 className="text-lg font-semibold mb-3">Now playing</h2>
{nowPlaying && <NowPlaying sessions={nowPlaying} />}
</section>
<hr />
{/* Server Overview */}
<section>
<h2 className="text-lg font-semibold mb-3">Server overview</h2>
<div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-7 gap-3">
<MetricCard
label="CPU"
value={latest ? `${latest.cpu_pct.toFixed(1)}%` : "-"}
/>
<MetricCard
label="IO Wait"
value={latest ? `${(latest.iowait_pct ?? 0).toFixed(1)}%` : "-"}
/>
<MetricCard
label="RAM"
value={latest ? `${latest.mem_pct.toFixed(1)}%` : "-"}
/>
<MetricCard
label="Net down"
value={latest ? formatRate(latest.net_rx_bytes_per_sec) : "-"}
/>
<MetricCard
label="Net up"
value={latest ? formatRate(latest.net_tx_bytes_per_sec) : "-"}
/>
<MetricCard
label="Disk read"
value={latest ? formatRate(latest.disk_read_bps) : "-"}
/>
<MetricCard
label="Disk write"
value={latest ? formatRate(latest.disk_write_bps) : "-"}
/>
</div>
{disk && (
<div className="mt-3 grid grid-cols-4 gap-3">
<MetricCard label="Disk used" value={formatBytes(disk.used)} />
<MetricCard
label="Disk available"
value={formatBytes(disk.available)}
/>
<MetricCard label="Disk total" value={formatBytes(disk.size)} />
<MetricCard label="Used %" value={disk.used_pct} />
</div>
)}
</section>
<hr />
{/* Media Library Overview */}
<section>
<h2 className="text-lg font-semibold mb-3">Media library overview</h2>
{counts && (
<div className="grid grid-cols-4 gap-3 mb-4">
<MetricCard
label="Total"
value={(
counts.movies +
counts.series +
counts.episodes
).toLocaleString()}
/>
<MetricCard label="Movies" value={counts.movies.toLocaleString()} />
<MetricCard label="Series" value={counts.series.toLocaleString()} />
<MetricCard
label="Episodes"
value={counts.episodes.toLocaleString()}
/>
</div>
)}
{libraries && <LibraryOverview libraries={libraries} />}
</section>
</div>
);
}