perf: batch-load app status history server-side to eliminate N+1 requests

- Load all app sparkline history in a single server query
- Pass preloadedHistory to AppCard to skip client-side fetch
- Polish empty state with icon, hint text, and add button
This commit is contained in:
2026-04-10 19:06:43 +03:00
parent b5166d9768
commit aedc91e321
3 changed files with 74 additions and 38 deletions
+29 -20
View File
@@ -20,34 +20,43 @@
checkedAt: string;
}
interface Props {
app: AppWithStatus;
interface PreloadedHistory {
history: StatusPoint[];
uptimePercent: number;
}
let { app }: Props = $props();
interface Props {
app: AppWithStatus;
preloadedHistory?: PreloadedHistory | null;
}
let historyData: StatusPoint[] = $state([]);
let uptimePercent: number | null = $state(null);
let historyLoading = $state(true);
let { app, preloadedHistory = null }: Props = $props();
let historyData: StatusPoint[] = $state(preloadedHistory?.history ?? []);
let uptimePercent: number | null = $state(preloadedHistory?.uptimePercent ?? null);
let historyLoading = $state(!preloadedHistory);
const currentStatus = $derived(app.statuses?.[0]?.status ?? 'unknown');
onMount(async () => {
try {
const res = await fetch(`/api/apps/${app.id}/history`);
if (res.ok) {
const json = await res.json();
if (json.success && json.data) {
historyData = json.data.history ?? [];
uptimePercent = json.data.uptimePercent ?? null;
// Only fetch client-side if no preloaded data was provided
if (!preloadedHistory) {
onMount(async () => {
try {
const res = await fetch(`/api/apps/${app.id}/history`);
if (res.ok) {
const json = await res.json();
if (json.success && json.data) {
historyData = json.data.history ?? [];
uptimePercent = json.data.uptimePercent ?? null;
}
}
} catch {
// Silently fail — sparkline is non-critical
} finally {
historyLoading = false;
}
} catch {
// Silently fail — sparkline is non-critical
} finally {
historyLoading = false;
}
});
});
}
const iconDisplay = $derived.by(() => {
if (!app.icon) return null;