refactor: unify backend and frontend servers (#3)

* refactor: unify backend and frontend servers

* refactor: correct paths for openapi & drizzle

* refactor: move api-client to client

* fix: drizzle paths

* chore: fix linting issues

* fix: form reset issue
This commit is contained in:
Nico
2025-11-13 20:11:46 +01:00
committed by GitHub
parent 8d7e50508d
commit 95a0d44b45
240 changed files with 5171 additions and 5875 deletions

View File

@@ -0,0 +1,19 @@
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}

View File

@@ -0,0 +1,151 @@
import { useEffect, useRef } from "react";
import { useQueryClient } from "@tanstack/react-query";
type ServerEventType =
| "connected"
| "heartbeat"
| "backup:started"
| "backup:progress"
| "backup:completed"
| "volume:mounted"
| "volume:unmounted"
| "volume:updated";
export interface BackupEvent {
scheduleId: number;
volumeName: string;
repositoryName: string;
status?: "success" | "error";
}
export interface BackupProgressEvent {
scheduleId: number;
volumeName: string;
repositoryName: string;
seconds_elapsed: number;
percent_done: number;
total_files: number;
files_done: number;
total_bytes: number;
bytes_done: number;
current_files: string[];
}
export interface VolumeEvent {
volumeName: string;
}
type EventHandler = (data: unknown) => void;
/**
* Hook to listen to Server-Sent Events (SSE) from the backend
* Automatically handles cache invalidation for backup and volume events
*/
export function useServerEvents() {
const queryClient = useQueryClient();
const eventSourceRef = useRef<EventSource | null>(null);
const handlersRef = useRef<Map<ServerEventType, Set<EventHandler>>>(new Map());
useEffect(() => {
const eventSource = new EventSource("/api/v1/events");
eventSourceRef.current = eventSource;
eventSource.addEventListener("connected", () => {
console.log("[SSE] Connected to server events");
});
eventSource.addEventListener("heartbeat", () => {});
eventSource.addEventListener("backup:started", (e) => {
const data = JSON.parse(e.data) as BackupEvent;
console.log("[SSE] Backup started:", data);
handlersRef.current.get("backup:started")?.forEach((handler) => {
handler(data);
});
});
eventSource.addEventListener("backup:progress", (e) => {
const data = JSON.parse(e.data) as BackupProgressEvent;
handlersRef.current.get("backup:progress")?.forEach((handler) => {
handler(data);
});
});
eventSource.addEventListener("backup:completed", (e) => {
const data = JSON.parse(e.data) as BackupEvent;
console.log("[SSE] Backup completed:", data);
queryClient.invalidateQueries();
queryClient.refetchQueries();
handlersRef.current.get("backup:completed")?.forEach((handler) => {
handler(data);
});
});
eventSource.addEventListener("volume:mounted", (e) => {
const data = JSON.parse(e.data) as VolumeEvent;
console.log("[SSE] Volume mounted:", data);
handlersRef.current.get("volume:mounted")?.forEach((handler) => {
handler(data);
});
});
eventSource.addEventListener("volume:unmounted", (e) => {
const data = JSON.parse(e.data) as VolumeEvent;
console.log("[SSE] Volume unmounted:", data);
handlersRef.current.get("volume:unmounted")?.forEach((handler) => {
handler(data);
});
});
eventSource.addEventListener("volume:updated", (e) => {
const data = JSON.parse(e.data) as VolumeEvent;
console.log("[SSE] Volume updated:", data);
queryClient.invalidateQueries();
handlersRef.current.get("volume:updated")?.forEach((handler) => {
handler(data);
});
});
eventSource.addEventListener("volume:status_updated", (e) => {
const data = JSON.parse(e.data) as VolumeEvent;
console.log("[SSE] Volume status updated:", data);
queryClient.invalidateQueries();
handlersRef.current.get("volume:updated")?.forEach((handler) => {
handler(data);
});
});
eventSource.onerror = (error) => {
console.error("[SSE] Connection error:", error);
};
return () => {
console.log("[SSE] Disconnecting from server events");
eventSource.close();
eventSourceRef.current = null;
};
}, [queryClient]);
const addEventListener = (event: ServerEventType, handler: EventHandler) => {
if (!handlersRef.current.has(event)) {
handlersRef.current.set(event, new Set());
}
handlersRef.current.get(event)?.add(handler);
return () => {
handlersRef.current.get(event)?.delete(handler);
};
};
return { addEventListener };
}

View File

@@ -0,0 +1,18 @@
import { useQuery } from "@tanstack/react-query";
import { getSystemInfoOptions } from "../api-client/@tanstack/react-query.gen";
export function useSystemInfo() {
const { data, isLoading, error } = useQuery({
...getSystemInfoOptions(),
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
refetchOnWindowFocus: false,
});
return {
capabilities: data?.capabilities ?? { docker: false, rclone: false },
isLoading,
error,
systemInfo: data,
};
}