perf: batch-load app history to eliminate N+1 fetches on board load

Previously each AppWidget fetched /api/apps/{id}/history individually
on mount, causing N sequential HTTP requests. Now the board page
server load fetches all app histories in a single Prisma query via
getBatchStatusHistory() and passes them to AppWidget via Svelte
context. AppWidget uses the pre-loaded data immediately with a
fallback fetch for non-board contexts.
This commit is contained in:
2026-03-25 15:36:06 +03:00
parent 6eb6bba289
commit 92eeeadec0
4 changed files with 66 additions and 5 deletions
+32
View File
@@ -127,6 +127,38 @@ export async function getStatusHistory(appId: string, limit: number = 50) {
});
}
/**
* Batch-fetch status history for multiple apps in a single query.
* Returns a map of appId -> { history, uptimePercent }.
*/
export async function getBatchStatusHistory(
appIds: readonly string[],
limitPerApp: number = 288
): Promise<ReadonlyMap<string, { readonly history: readonly { status: string; responseTime: number | null; checkedAt: Date }[]; readonly uptimePercent: number }>> {
if (appIds.length === 0) return new Map();
const allStatuses = await prisma.appStatus.findMany({
where: { appId: { in: [...appIds] } },
orderBy: { checkedAt: 'desc' },
select: { appId: true, status: true, responseTime: true, checkedAt: true }
});
const result = new Map<string, { history: { status: string; responseTime: number | null; checkedAt: Date }[]; uptimePercent: number }>();
for (const appId of appIds) {
const statuses = allStatuses.filter((s) => s.appId === appId).slice(0, limitPerApp).reverse();
const totalChecks = statuses.length;
const onlineChecks = statuses.filter((s) => s.status === 'online').length;
const uptimePercent = totalChecks > 0 ? Math.round((onlineChecks / totalChecks) * 1000) / 10 : 0;
result.set(appId, {
history: statuses.map((s) => ({ status: s.status, responseTime: s.responseTime, checkedAt: s.checkedAt })),
uptimePercent
});
}
return result;
}
export async function getHealthcheckTargets() {
return prisma.app.findMany({
where: { healthcheckEnabled: true },