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
+26
View File
@@ -0,0 +1,26 @@
import { useQuery } from "@tanstack/react-query";
import { fetchCounts, fetchLibraries, fetchNowPlaying } from "../api/client";
export function useCounts() {
return useQuery({
queryKey: ["dashboard", "counts"],
queryFn: fetchCounts,
staleTime: 5 * 60 * 1000,
});
}
export function useLibraries() {
return useQuery({
queryKey: ["dashboard", "libraries"],
queryFn: fetchLibraries,
staleTime: 5 * 60 * 1000,
});
}
export function useNowPlaying() {
return useQuery({
queryKey: ["dashboard", "now-playing"],
queryFn: fetchNowPlaying,
refetchInterval: 15_000,
});
}
+49
View File
@@ -0,0 +1,49 @@
import { useQuery, useMutation } from "@tanstack/react-query";
import {
fetchDirectoryListing,
fetchFfprobe,
fetchStat,
fetchJobTemplates,
runJob,
} from "../api/client";
export function useDirectoryListing(path: string) {
return useQuery({
queryKey: ["files", "list", path],
queryFn: () => fetchDirectoryListing(path),
enabled: !!path,
staleTime: 30_000,
});
}
export function useFfprobe(path: string, enabled = false) {
return useQuery({
queryKey: ["files", "ffprobe", path],
queryFn: () => fetchFfprobe(path),
enabled: enabled && !!path,
staleTime: 5 * 60_000,
});
}
export function useStat(path: string, enabled = false) {
return useQuery({
queryKey: ["files", "stat", path],
queryFn: () => fetchStat(path),
enabled: enabled && !!path,
});
}
export function useJobTemplates() {
return useQuery({
queryKey: ["jobs", "templates"],
queryFn: fetchJobTemplates,
staleTime: 60_000,
});
}
export function useRunJob() {
return useMutation({
mutationFn: ({ jobKey, path }: { jobKey: string; path: string }) =>
runJob(jobKey, path),
});
}
+40
View File
@@ -0,0 +1,40 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { fetchMediaStatus, buildMediaIndex, queryMedia } from "../api/client";
export function useMediaStatus() {
return useQuery({
queryKey: ["media", "status"],
queryFn: fetchMediaStatus,
staleTime: 60_000,
});
}
export function useMediaQuery(params: {
libraries?: string;
types?: string;
search?: string;
hdr_filter?: string;
sort_key?: string;
sort_order?: string;
limit?: number;
offset?: number;
enabled?: boolean;
}) {
const { enabled = true, ...queryParams } = params;
return useQuery({
queryKey: ["media", "query", queryParams],
queryFn: () => queryMedia(queryParams),
enabled,
staleTime: 30_000,
});
}
export function useBuildIndex() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: buildMediaIndex,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["media"] });
},
});
}
+54
View File
@@ -0,0 +1,54 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
fetchMonitoringStatus,
fetchMonitoringMetrics,
fetchDiskSpace,
startCollector,
stopCollector,
restartCollector,
} from "../api/client";
export function useMonitoringStatus() {
return useQuery({
queryKey: ["monitoring", "status"],
queryFn: fetchMonitoringStatus,
refetchInterval: 30_000,
});
}
export function useMonitoringMetrics(lastSeconds = 3600) {
return useQuery({
queryKey: ["monitoring", "metrics", lastSeconds],
queryFn: () => fetchMonitoringMetrics(lastSeconds),
refetchInterval: 15_000,
});
}
export function useDiskSpace() {
return useQuery({
queryKey: ["monitoring", "disk"],
queryFn: fetchDiskSpace,
staleTime: 60_000,
});
}
export function useCollectorControls() {
const queryClient = useQueryClient();
const invalidate = () =>
queryClient.invalidateQueries({ queryKey: ["monitoring"] });
const start = useMutation({
mutationFn: startCollector,
onSuccess: invalidate,
});
const stop = useMutation({
mutationFn: stopCollector,
onSuccess: invalidate,
});
const restart = useMutation({
mutationFn: restartCollector,
onSuccess: invalidate,
});
return { start, stop, restart };
}