From bfd36ad6327177def29fa0914aaa7be748c7c87f Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Mon, 13 Jul 2026 05:30:17 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20address=20final=20review=20=E2=80=94=204?= =?UTF-8?q?04=20error=20field,=20aggregate=20estFps,=20recent=20entries,?= =?UTF-8?q?=20panel=20props?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/api/__tests__/plugin-public.test.ts | 31 ++++++++++- apps/web/lib/api/plugin-public.ts | 28 +++++++--- .../src/components/LibraryAppPanel.tsx | 52 +++++++++++++------ .../decky-vault/src/patches/LibraryApp.tsx | 18 +++++++ 4 files changed, 105 insertions(+), 24 deletions(-) diff --git a/apps/web/lib/api/__tests__/plugin-public.test.ts b/apps/web/lib/api/__tests__/plugin-public.test.ts index 41b37dd..afd9415 100644 --- a/apps/web/lib/api/__tests__/plugin-public.test.ts +++ b/apps/web/lib/api/__tests__/plugin-public.test.ts @@ -40,10 +40,10 @@ import { buildPluginGameResponse } from "@/lib/api/plugin-public" describe("buildPluginGameResponse — shape contract", () => { beforeEach(() => vi.clearAllMocks()) - it("returns { game: null, error } shape when game is missing", async () => { + it("returns { game: null } shape (no error) when game is missing", async () => { const r = await buildPluginGameResponse({ game: null }) expect(r.game).toBeNull() - expect(typeof r.error).toBe("string") + expect(r.error).toBeUndefined() expect(r.estFps).toBeNull() expect(r.topEntries).toEqual([]) expect(r.recentEntries).toEqual([]) @@ -78,6 +78,33 @@ describe("buildPluginGameResponse — shape contract", () => { expect(r.topEntries[0].id).toBe("e1") // pinned first }) + it("uses provided estFps aggregate over entries-derived computation", async () => { + const entries = [ + { id: "e1", hardwareSlug: "steamdeck-oled", fpsAvg: 60, fpsLow: 40, fpsOnePercentLow: 45, fpsHigh: 90, + upscalerType: "none", frameGenMethod: "none", protonVersion: "9", osVersion: "SteamOS 3", tdpWatts: 12, + settingsJson: null, upvotes: 5, isPinned: true, createdAt: new Date("2026-01-01"), + userName: "u", userImage: null }, + { id: "e2", hardwareSlug: "steamdeck-oled", fpsAvg: 80, fpsLow: 55, fpsOnePercentLow: 60, fpsHigh: 120, + upscalerType: "fsr", frameGenMethod: "none", protonVersion: "9", osVersion: "SteamOS 3", tdpWatts: 15, + settingsJson: null, upvotes: 2, isPinned: false, createdAt: new Date("2026-02-01"), + userName: "u2", userImage: null }, + ] + // Aggregate says 25 entries averaging 72.3, distinct from the 2-row top-3 avg of 70 + const r = await buildPluginGameResponse({ + game: { id: "g1", steamAppId: 123, title: "X", slug: "x" }, + entries, recent: entries, + estFps: { avg: 72.3, low: 35, onePct: 38, high: 140, count: 25 }, + }) + expect(r.estFps).not.toBeNull() + expect(r.estFps!.avg).toBe(72.3) + expect(r.estFps!.count).toBe(25) + expect(r.estFps!.high).toBe(140) + expect(r.estFps!.low).toBe(35) + // Entries are still trimmed/rendered from the provided rows + expect(r.topEntries.length).toBe(2) + expect(r.recentEntries.length).toBe(2) + }) + it("handles all-null fps values gracefully", async () => { const entries = [ { id: "e1", hardwareSlug: "steamdeck-oled", fpsAvg: 60, fpsLow: null, fpsOnePercentLow: null, fpsHigh: null, diff --git a/apps/web/lib/api/plugin-public.ts b/apps/web/lib/api/plugin-public.ts index a13db86..c2e9a41 100644 --- a/apps/web/lib/api/plugin-public.ts +++ b/apps/web/lib/api/plugin-public.ts @@ -71,22 +71,23 @@ export async function buildPluginGameResponse(args: { game: PluginGameRow | null entries?: PluginEntryRow[] recent?: PluginEntryRow[] + estFps?: { avg: number; low: number | null; onePct: number | null; high: number | null; count: number } | null }): Promise { if (!args.game) { - return { game: null, estFps: null, topEntries: [], recentEntries: [], error: "Game not in DeckyVault" } + return { game: null, estFps: null, topEntries: [], recentEntries: [] } } const entries = args.entries ?? [] const recent = args.recent ?? [] - if (entries.length === 0) { - return { game: { ...args.game }, estFps: null, topEntries: [], recentEntries: [] } - } - const fpsAvgVals = entries.map((e) => e.fpsAvg) - const estFps = { - avg: Math.round((fpsAvgVals.reduce((a, b) => a + b, 0) / fpsAvgVals.length) * 10) / 10, + // Use provided estFps if available, otherwise compute from entries + const estFps = args.estFps ?? (entries.length > 0 ? { + avg: Math.round((entries.reduce((a, b) => a + b.fpsAvg, 0) / entries.length) * 10) / 10, low: entries.reduce((m, e) => (m == null ? e.fpsLow : Math.min(m, e.fpsLow ?? m)), null), onePct: entries.reduce((m, e) => (m == null ? e.fpsOnePercentLow : Math.min(m, e.fpsOnePercentLow ?? m)), null), high: entries.reduce((m, e) => (m == null ? e.fpsHigh : Math.max(m, e.fpsHigh ?? m)), null), count: entries.length, + } : null) + if (!estFps) { + return { game: { ...args.game }, estFps: null, topEntries: [], recentEntries: [] } } const topEntries = entries.map(trimEntry) const recentEntries = recent.map(trimEntry) @@ -206,12 +207,25 @@ export const pluginPublicRoutes = new Elysia({ .orderBy(desc(performanceEntries.createdAt)) .limit(query.limit ?? 3) + // Aggregate estFps from ALL matching entries (not just top-3) + const [agg] = await db + .select({ + avg: sql`round(avg(${performanceEntries.fpsAvg})::numeric, 1)`, + low: sql`min(${performanceEntries.fpsLow})`, + onePct: sql`min(${performanceEntries.fpsOnePercentLow})`, + high: sql`max(${performanceEntries.fpsHigh})`, + count: sql`count(*)::int`, + }) + .from(performanceEntries) + .where(baseWhere) + set.headers["Cache-Control"] = "public, max-age=60" set.headers["Vary"] = "search-params" return await buildPluginGameResponse({ game, entries: topRows as unknown as PluginEntryRow[], recent: recentRows as unknown as PluginEntryRow[], + estFps: agg.avg ? { avg: agg.avg, low: agg.low, onePct: agg.onePct, high: agg.high, count: agg.count } : null, }) }, { diff --git a/plugins/decky-vault/src/components/LibraryAppPanel.tsx b/plugins/decky-vault/src/components/LibraryAppPanel.tsx index 80d0974..ea45ea4 100644 --- a/plugins/decky-vault/src/components/LibraryAppPanel.tsx +++ b/plugins/decky-vault/src/components/LibraryAppPanel.tsx @@ -140,23 +140,33 @@ export default function LibraryAppPanel({ appId, title, hardwareSlug, baseUrl }: - {/* Est FPS */} - -
- Est FPS -
-
- {data.estFps ? ( - -
- {data.estFps.avg} avg · {data.estFps.low ?? "—"} low · {data.estFps.onePct ?? "—"} 1% · {data.estFps.high ?? "—"} high - · {data.estFps.count} entries -
-
+ {/* Est FPS — hidden when "All devices" selected to avoid mixing hardware */} + {device ? ( + <> + +
+ Est FPS +
+
+ {data.estFps ? ( + +
+ {data.estFps.avg} avg · {data.estFps.low ?? "—"} low · {data.estFps.onePct ?? "—"} 1% · {data.estFps.high ?? "—"} high + · {data.estFps.count} entries +
+
+ ) : ( + +
+ No entries for this device yet — be the first: open the DeckyVault plugin and record. +
+
+ )} + ) : ( -
- No entries for this device yet — be the first: open the DeckyVault plugin and record. +
+ Select a device to see estimated FPS.
)} @@ -182,6 +192,18 @@ export default function LibraryAppPanel({ appId, title, hardwareSlug, baseUrl }: {data.topEntries.map((e) => )} )} + + {/* Recent entries */} + {data.recentEntries.length > 0 && ( + <> + +
+ Recent entries +
+
+ {data.recentEntries.map((e) => )} + + )} ) } \ No newline at end of file diff --git a/plugins/decky-vault/src/patches/LibraryApp.tsx b/plugins/decky-vault/src/patches/LibraryApp.tsx index 3648fd3..4c4658d 100644 --- a/plugins/decky-vault/src/patches/LibraryApp.tsx +++ b/plugins/decky-vault/src/patches/LibraryApp.tsx @@ -7,6 +7,7 @@ import { import { routerHook } from "@decky/api" import type { ReactElement } from "react" import LibraryAppPanel from "../components/LibraryAppPanel" +import { getSettings } from "../lib/api" // Mirror of HLTB-for-Deck's patchAppPage, guarded so a Steam UI change // degrades to "section not shown" instead of crashing Steam. @@ -20,7 +21,24 @@ export function setLibraryAppPanelProps(p: { hardwareSlug: string | null; baseUr panelProps = p } +// Read settings directly from the Python backend so the panel has them even +// if the QAM Content tab has never mounted (which is what populates panelProps +// via setLibraryAppPanelProps). Keeps hardwareSlug/baseUrl in sync eagerly. +async function loadPanelProps() { + try { + const s = await getSettings() + panelProps = { + hardwareSlug: (s.hardwareSlug as string) || null, + baseUrl: (s.baseUrl as string) || "https://deckyvault.xyz", + } + } catch { + // Use defaults — panelProps retains its last known value + } +} + export function registerLibraryAppPatch() { + // Load settings eagerly so the panel has them even if Content never mounted + loadPanelProps() return routerHook.addPatch("/library/app/:appid", (routerTree: any) => { try { const routeProps = findInReactTree(routerTree, (x: any) => x?.renderFunc)