From 9423ed85ff539483cc5ca71e50c2a4dec6475d8c Mon Sep 17 00:00:00 2001 From: Sanju Sivalingam Date: Tue, 17 Feb 2026 21:02:07 +0530 Subject: [PATCH] feat: add device stats query and enrich listDevices with metrics Co-Authored-By: Claude Opus 4.6 --- web/src/lib/api/devices.remote.ts | 88 ++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 8 deletions(-) diff --git a/web/src/lib/api/devices.remote.ts b/web/src/lib/api/devices.remote.ts index 21236a1..49b5ea8 100644 --- a/web/src/lib/api/devices.remote.ts +++ b/web/src/lib/api/devices.remote.ts @@ -1,7 +1,7 @@ import { query, getRequestEvent } from '$app/server'; import { db } from '$lib/server/db'; import { device, agentSession, agentStep } from '$lib/server/db/schema'; -import { eq, desc, and } from 'drizzle-orm'; +import { eq, desc, and, count, avg, sql } from 'drizzle-orm'; export const listDevices = query(async () => { const { locals } = getRequestEvent(); @@ -13,13 +13,85 @@ export const listDevices = query(async () => { .where(eq(device.userId, locals.user.id)) .orderBy(desc(device.lastSeen)); - return devices.map((d) => ({ - deviceId: d.id, - name: d.name, - status: d.status, - deviceInfo: d.deviceInfo, - lastSeen: d.lastSeen?.toISOString() ?? d.createdAt.toISOString() - })); + // Get last session for each device + const deviceIds = devices.map((d) => d.id); + const lastSessions = + deviceIds.length > 0 + ? await db + .select({ + deviceId: agentSession.deviceId, + goal: agentSession.goal, + status: agentSession.status, + startedAt: agentSession.startedAt + }) + .from(agentSession) + .where(sql`${agentSession.deviceId} IN ${deviceIds}`) + .orderBy(desc(agentSession.startedAt)) + : []; + + // Group last session per device (first occurrence = latest due to ORDER BY) + const lastSessionMap = new Map(); + for (const s of lastSessions) { + if (!lastSessionMap.has(s.deviceId)) { + lastSessionMap.set(s.deviceId, s); + } + } + + return devices.map((d) => { + const info = d.deviceInfo as Record | null; + const last = lastSessionMap.get(d.id); + return { + deviceId: d.id, + name: d.name, + status: d.status, + model: (info?.model as string) ?? null, + manufacturer: (info?.manufacturer as string) ?? null, + androidVersion: (info?.androidVersion as string) ?? null, + screenWidth: (info?.screenWidth as number) ?? null, + screenHeight: (info?.screenHeight as number) ?? null, + batteryLevel: (info?.batteryLevel as number) ?? null, + isCharging: (info?.isCharging as boolean) ?? false, + lastSeen: d.lastSeen?.toISOString() ?? d.createdAt.toISOString(), + lastGoal: last + ? { goal: last.goal, status: last.status, startedAt: last.startedAt.toISOString() } + : null + }; + }); +}); + +export const getDeviceStats = query(async (deviceId: string) => { + const { locals } = getRequestEvent(); + if (!locals.user) return null; + + const stats = await db + .select({ + totalSessions: count(agentSession.id), + successCount: count(sql`CASE WHEN ${agentSession.status} = 'completed' THEN 1 END`), + avgSteps: avg(agentSession.stepsUsed) + }) + .from(agentSession) + .where(and(eq(agentSession.deviceId, deviceId), eq(agentSession.userId, locals.user.id))); + + const lastSession = await db + .select({ + goal: agentSession.goal, + status: agentSession.status, + startedAt: agentSession.startedAt + }) + .from(agentSession) + .where(and(eq(agentSession.deviceId, deviceId), eq(agentSession.userId, locals.user.id))) + .orderBy(desc(agentSession.startedAt)) + .limit(1); + + const s = stats[0]; + return { + totalSessions: Number(s?.totalSessions ?? 0), + successRate: s?.totalSessions + ? Math.round((Number(s.successCount) / Number(s.totalSessions)) * 100) + : 0, + avgSteps: Math.round(Number(s?.avgSteps ?? 0)), + lastGoal: lastSession[0] ?? null + }; }); export const listDeviceSessions = query(async (deviceId: string) => {