From eacbdb5d47562b7bc820a730f00ffa35358d9ffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Sun, 22 Mar 2026 22:55:45 +0100 Subject: [PATCH] =?UTF-8?q?perf:=20optimize=20Activity=20Log=20=E2=80=94?= =?UTF-8?q?=20lazy=20diff,=2030-day=20default,=20getById?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - List query: exclude changes JSONB from select (only metadata) - Default to last 30 days when no date filter (avoids full table scan) - New getById query: fetches full changes JSONB on demand - ExpandedDiff component: fetches diff only when user expands an entry - 5-minute staleTime on expanded diffs (cacheable, rarely changes) Co-Authored-By: claude-flow --- .../components/admin/ActivityLogClient.tsx | 31 ++++++++++++++----- packages/api/src/router/audit-log.ts | 31 ++++++++++++++++++- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/admin/ActivityLogClient.tsx b/apps/web/src/components/admin/ActivityLogClient.tsx index f3cdd97..4550593 100644 --- a/apps/web/src/components/admin/ActivityLogClient.tsx +++ b/apps/web/src/components/admin/ActivityLogClient.tsx @@ -122,6 +122,28 @@ function DiffView({ changes }: { changes: Changes }) { ); } +function ExpandedDiff({ entryId }: { entryId: string }) { + const { data, isLoading } = trpc.auditLog.getById.useQuery( + { id: entryId }, + { staleTime: 300_000 }, + ); + + if (isLoading) { + return ( +
+
+
+ ); + } + + const changes = parseChanges((data as any)?.changes); + return ( +
+ +
+ ); +} + function SummaryCards({ summary }: { summary: { byEntityType: Record; total: number } }) { const sorted = useMemo(() => { return Object.entries(summary.byEntityType) @@ -355,7 +377,6 @@ export function ActivityLogClient() { )} {allEntries.map((entry) => { - const changes = parseChanges(entry.changes); const isExpanded = expandedId === entry.id; const entityLink = ENTITY_LINKS[entry.entityType]?.(entry.entityId); @@ -431,12 +452,8 @@ export function ActivityLogClient() { - {/* Expanded Diff */} - {isExpanded && ( -
- -
- )} + {/* Expanded Diff — fetched on demand */} + {isExpanded && }
); })} diff --git a/packages/api/src/router/audit-log.ts b/packages/api/src/router/audit-log.ts index a720d24..5de9f09 100644 --- a/packages/api/src/router/audit-log.ts +++ b/packages/api/src/router/audit-log.ts @@ -49,10 +49,27 @@ export const auditLogRouter = createTRPCRouter({ ]; } + // Default to last 30 days if no date filter to avoid full table scan + if (!startDate && !endDate && !entityId) { + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + where.createdAt = { ...(where.createdAt as Record ?? {}), gte: thirtyDaysAgo }; + } + const items = await ctx.db.auditLog.findMany({ where, - include: { + select: { + id: true, + entityType: true, + entityId: true, + entityName: true, + action: true, + userId: true, + source: true, + summary: true, + createdAt: true, user: { select: { id: true, name: true, email: true } }, + // Exclude 'changes' from list query — fetch on demand when expanding }, orderBy: { createdAt: "desc" }, take: limit + 1, @@ -68,6 +85,18 @@ export const auditLogRouter = createTRPCRouter({ return { items, nextCursor }; }), + /** + * Get a single audit entry with full changes JSONB (for expand/detail view). + */ + getById: controllerProcedure + .input(z.object({ id: z.string() })) + .query(async ({ ctx, input }) => { + return ctx.db.auditLog.findUniqueOrThrow({ + where: { id: input.id }, + include: { user: { select: { id: true, name: true, email: true } } }, + }); + }), + /** * Get all audit entries for a specific entity (e.g. a project or resource). */