diff --git a/app/devices/page.tsx b/app/devices/page.tsx index 2f162fc..0a134f5 100644 --- a/app/devices/page.tsx +++ b/app/devices/page.tsx @@ -31,6 +31,8 @@ export default async function DevicesPage() { deviceType: hardware.deviceType, image: hardware.image, sortOrder: hardware.sortOrder, + wattHours: hardware.wattHours, + tdpMax: hardware.tdpMax, }) .from(hardware) .orderBy(hardware.sortOrder) @@ -102,6 +104,8 @@ export default async function DevicesPage() { gameCount: stats?.gameCount ?? 0, verifiedCount: stats?.verifiedCount ?? 0, bestGame: bestGame ?? null, + wattHours: device.wattHours ? Number(device.wattHours) : null, + tdpMax: device.tdpMax ? Number(device.tdpMax) : null, } }) diff --git a/lib/api/game-stats.ts b/lib/api/game-stats.ts index 8531530..55d342e 100644 --- a/lib/api/game-stats.ts +++ b/lib/api/game-stats.ts @@ -46,6 +46,7 @@ export const gameStatsRoutes = new Elysia({ prefix: "/games" }).get( userNotes: performanceEntries.userNotes, createdAt: performanceEntries.createdAt, versionId: performanceEntries.versionId, + tdpWatts: performanceEntries.tdpWatts, }) .from(performanceEntries) .innerJoin( @@ -76,6 +77,7 @@ export const gameStatsRoutes = new Elysia({ prefix: "/games" }).get( fpsRange: [], deviceBreakdown: [], filterOptions: { protonVersions: [], osVersions: [] }, + batteryLife: [], } } @@ -245,11 +247,17 @@ export const gameStatsRoutes = new Elysia({ prefix: "/games" }).get( // ── 8. Device breakdown ─────────────────────────────────────── const deviceBreakdown = Array.from(boxplotMap.entries()).map( - ([slug, { hardwareName, values }]) => ({ - hardwareSlug: slug, - hardwareName, - count: values.length, - }), + ([slug, { hardwareName, values }]) => { + const dev = deviceWattHoursMap.get(slug) + return { + hardwareSlug: slug, + hardwareName, + count: values.length, + wattHours: dev?.wattHours ? Number(dev.wattHours) : null, + tdpMax: dev?.tdpMax ? Number(dev.tdpMax) : null, + deviceType: dev?.deviceType ?? null, + } + }, ) // ── 9. Performance tier breakdown per device ──────────────── @@ -279,6 +287,48 @@ export const gameStatsRoutes = new Elysia({ prefix: "/games" }).get( stabilityRatio: e.fpsAvg > 0 ? Math.min(1, e.fpsOnePercentLow! / e.fpsAvg) : 0, })) + // ── 12. Battery life estimates ───────────────────────────── + // Fetch hardware wattHours for the relevant devices + const deviceSlugs = [...new Set(entries.map((e) => e.hardwareSlug))] + const deviceData = await db + .select({ + slug: hardware.slug, + wattHours: hardware.wattHours, + tdpMax: hardware.tdpMax, + deviceType: hardware.deviceType, + }) + .from(hardware) + .where(sql`${hardware.slug} = ANY(${deviceSlugs})`) + + const deviceWattHoursMap = new Map( + deviceData.map((d) => [d.slug, d]), + ) + + const batteryLife = entries + .filter((e) => e.tdpWatts != null && e.tdpWatts > 0) + .map((e) => { + const device = deviceWattHoursMap.get(e.hardwareSlug) + const wh = device?.wattHours ? Number(device.wattHours) : null + const tdpMax = device?.tdpMax ? Number(device.tdpMax) : null + if (!wh) return null + const estimatedBatteryHours = wh / e.tdpWatts! + const estimatedBatteryMin = estimatedBatteryHours * 60 + const estimatedAtMaxTdpMin = tdpMax ? (wh / tdpMax) * 60 : null + return { + id: e.id, + hardwareSlug: e.hardwareSlug, + tdpWatts: Number(e.tdpWatts), + estimatedBatteryMin: Math.round(estimatedBatteryMin), + estimatedBatteryHours: Math.round(estimatedBatteryHours * 10) / 10, + wattHours: wh, + tdpMax, + estimatedAtMaxTdpMin: estimatedAtMaxTdpMin + ? Math.round(estimatedAtMaxTdpMin) + : null, + } + }) + .filter(Boolean) + // ── 11. Filter options ──────────────────────────────────────── const protonVersions = [ ...new Set(entries.map((e) => e.protonVersion).filter(Boolean)), @@ -306,6 +356,7 @@ export const gameStatsRoutes = new Elysia({ prefix: "/games" }).get( deviceBreakdown, performanceTiers, stabilityScatter, + batteryLife, filterOptions: { protonVersions, osVersions }, } }, diff --git a/lib/api/hardware-stats.ts b/lib/api/hardware-stats.ts index f0d9604..c066cca 100644 --- a/lib/api/hardware-stats.ts +++ b/lib/api/hardware-stats.ts @@ -20,6 +20,8 @@ export const hardwareStatsRoutes = new Elysia({ prefix: "/hardware" }) name: hardware.name, deviceType: hardware.deviceType, sortOrder: hardware.sortOrder, + wattHours: hardware.wattHours, + tdpMax: hardware.tdpMax, }) .from(hardware) .orderBy(hardware.sortOrder) @@ -79,6 +81,8 @@ export const hardwareStatsRoutes = new Elysia({ prefix: "/hardware" }) name: device.name, deviceType: device.deviceType, sortOrder: device.sortOrder, + wattHours: device.wattHours ? Number(device.wattHours) : null, + tdpMax: device.tdpMax ? Number(device.tdpMax) : null, totalBenchmarks: stats?.totalBenchmarks ?? 0, avgFps: stats?.avgFps ? Number(stats.avgFps) : null, gameCount: stats?.gameCount ?? 0, @@ -100,6 +104,8 @@ export const hardwareStatsRoutes = new Elysia({ prefix: "/hardware" }) slug: hardware.slug, name: hardware.name, deviceType: hardware.deviceType, + wattHours: hardware.wattHours, + tdpMax: hardware.tdpMax, }) .from(hardware) .where(eq(hardware.slug, slug)) @@ -144,6 +150,8 @@ export const hardwareStatsRoutes = new Elysia({ prefix: "/hardware" }) if (entries.length === 0) { return { ...device, + wattHours: null, + tdpMax: null, totalBenchmarks: 0, avgFps: null, verifiedCount: 0,