# Devices Redesign & Admin Panel — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Fix critical device page bugs, redesign devices list/detail pages with proper SEO, remove the broken Community Trust graph, and add an admin management panel with nested routes for Users, Hardware, and Games. **Architecture:** Server components fetch data and pass as props (true RSC pattern). Admin panel uses a `(admin)` route group with shared sidebar layout and nested routes. All devices code fixes the `"handled"` → `"handheld"` enum typo and the `upscalerBreakdown`/`fsrBreakdown` property mismatch. **Tech Stack:** Next.js 16 (App Router), React 19, Tailwind CSS v4, Drizzle ORM (PostgreSQL), better-auth (admin plugin), Elysia API, Framer Motion v12, ECharts, Lucide React icons --- ### Task 1: DB Migration — Rename Enum & Add Image Column **Files:** - Create: `drizzle/0011_rename-handled-to-handheld-add-image.sql` - Modify: `lib/db/schema/hardware.ts` - Modify: `lib/db/seed.ts` - [ ] **Step 1: Create the SQL migration file** Create `drizzle/0011_rename-handled-to-handheld-add-image.sql`: ```sql -- Rename pg_enum value from 'handled' to 'handheld' ALTER TYPE device_type RENAME VALUE 'handled' TO 'handheld'; -- Add nullable image column to hardware table ALTER TABLE hardware ADD COLUMN image text; ``` - [ ] **Step 2: Update the Drizzle schema to match** In `lib/db/schema/hardware.ts`, change the enum and add the image column: ```ts import { integer, pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core" export const deviceTypeEnum = pgEnum("device_type", ["handheld", "console"]) export const hardware = pgTable("hardware", { slug: text("slug").primaryKey(), name: text("name").notNull(), deviceType: deviceTypeEnum("device_type").notNull(), image: text("image"), sortOrder: integer("sort_order").default(0).notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), }) ``` - [ ] **Step 3: Update the seed file** In `lib/db/seed.ts`, change all `"handled" as const` to `"handheld" as const`: ```ts const devices = [ { slug: "steamdeck-oled", name: "Steam Deck OLED", deviceType: "handheld" as const, sortOrder: 0, }, { slug: "steamdeck-lcd", name: "Steam Deck LCD", deviceType: "handheld" as const, sortOrder: 1, }, { slug: "steam-machine", name: "Steam Machine", deviceType: "console" as const, sortOrder: 2, }, ] ``` - [ ] **Step 4: Run the migration** Run: `cd /Users/adrianbonpin/Documents/Code/personal/deckyvault && bunx drizzle-kit push` Expected: Migration applies — enum value renamed, image column added. - [ ] **Step 5: Run the seed to verify** Run: `cd /Users/adrianbonpin/Documents/Code/personal/deckyvault && bun run lib/db/seed.ts` Expected: No conflict errors, devices seeded with `"handheld"` type. - [ ] **Step 6: Commit** ```bash git add drizzle/0011_rename-handled-to-handheld-add-image.sql lib/db/schema/hardware.ts lib/db/seed.ts git commit -m "fix: rename device_type enum 'handled' to 'handheld', add image column" ``` --- ### Task 2: Fix API Bug Fixes — avgFps Nulls & upscalerBreakdown Consistency **Files:** - Modify: `lib/api/hardware-stats.ts` - [ ] **Step 1: Fix avgFps calculation to exclude null FPS entries** In `lib/api/hardware-stats.ts`, locate the `/:slug/stats` handler and replace the `avgFps` calculation: **Old code (around line ~90):** ```ts const avgFps = Math.round( (entries.reduce((s, e) => s + (e.fpsAvg ?? 0), 0) / totalBenchmarks) * 10 ) / 10 ``` **New code:** ```ts // Filter out null FPS entries before computing average (SQL avg() ignores nulls) const fpsEntries = entries.filter((e) => e.fpsAvg !== null) const avgFps = fpsEntries.length > 0 ? Math.round( (fpsEntries.reduce((s, e) => s + e.fpsAvg!, 0) / fpsEntries.length) * 10 ) / 10 : null ``` - [ ] **Step 2: Verify the API response shape uses `upscalerBreakdown` consistently** The API already returns `upscalerBreakdown` (this is the correct name). Confirm there is no `fsrBreakdown` in the API file. No changes needed here — the fix happens in the client component (Task 5). - [ ] **Step 3: Run the dev server and test the endpoint** Run: `cd /Users/adrianbonpin/Documents/Code/personal/deckyvault && curl -s http://localhost:3000/api/hardware/steamdeck-oled/stats | head -c 200` Expected: JSON response with `avgFps` reflecting only non-null entries, and `upscalerBreakdown` (not `fsrBreakdown`). - [ ] **Step 4: Commit** ```bash git add lib/api/hardware-stats.ts git commit -m "fix: exclude null FPS entries from avgFps calculation in device stats API" ``` --- ### Task 3: Remove Community Trust Chart **Files:** - Modify: `app/game/[id]/game-page-client.tsx` - Modify: `lib/api/game-stats.ts` - Delete: `components/charts/TrustBar.tsx` - [ ] **Step 1: Remove TrustBar from game-page-client.tsx** In `app/game/[id]/game-page-client.tsx`: 1. **Remove the import** (line ~34): Delete: `import { TrustBar } from "@/components/charts/TrustBar"` 2. **Remove trust from the stats interface** (around line ~143): Delete the entire `trust` array from the interface: ```ts // DELETE these lines: trust: Array<{ id: string hardwareSlug: string upvotes: number downvotes: number verifiedAt: string | null userNotes: string | null createdAt: string }> ``` 3. **Remove trust from filteredStats** (around line ~326): Delete the line: `trust: filterByDevice(stats.trust),` 4. **Remove the Community Trust render section** (around line ~995): Delete the entire block: ```tsx {/* Row 4 — Trust Bar */} {filteredStats && filteredStats.trust.length > 0 && (

Community Trust

)} ``` - [ ] **Step 2: Remove trust data from game-stats API** In `lib/api/game-stats.ts`: 1. **Remove Section 9** (trust data mapping, around line ~242): Delete: ```ts // ── 9. Community trust ──────────────────────────────────────── const trust = entries.map((e) => ({ id: e.id, hardwareSlug: e.hardwareSlug, upvotes: e.upvotes, downvotes: e.downvotes, verifiedAt: e.verifiedAt ? e.verifiedAt.toISOString() : null, userNotes: e.userNotes, createdAt: e.createdAt.toISOString(), })) ``` 2. **Remove `trust` from the empty return** (around line ~77): Change: `trust: [],` → remove this line 3. **Remove `trust` from the final return object** (around line ~276): Delete the line: `trust,` - [ ] **Step 3: Delete the TrustBar component file** Delete: `components/charts/TrustBar.tsx` - [ ] **Step 4: Verify no other imports of TrustBar exist** Run: `cd /Users/adrianbonpin/Documents/Code/personal/deckyvault && grep -r "TrustBar" --include="*.tsx" --include="*.ts" app/ components/ lib/` Expected: No results (TrustBar fully removed from codebase). - [ ] **Step 5: Commit** ```bash git add -A git commit -m "refactor: remove broken Community Trust chart (TrustBar) - Remove TrustBar component and chart - Remove trust data from game-stats API response - Remove trust section from game detail page - DB columns and vote endpoints preserved for future voting system" ``` --- ### Task 4: Redesign Devices List Page **Files:** - Modify: `app/devices/page.tsx` - Modify: `app/devices/page-client.tsx` - [ ] **Step 1: Rewrite the server component to fetch data and pass as props** Replace the entire content of `app/devices/page.tsx` with: ```tsx import { db } from "@/lib/db/index" import { hardware, performanceEntries, gameVersions, games } from "@/lib/db/schema" import { eq, and, sql, desc } from "drizzle-orm" import { DevicesPageClient } from "./page-client" import type { Metadata } from "next" export const metadata: Metadata = { title: "Devices — DeckyVault", description: "Browse benchmark data for handheld and console gaming devices. Compare FPS, performance stats, and community benchmarks on DeckyVault.", keywords: ["steam deck", "handheld", "console", "benchmarks", "FPS", "performance", "devices"], alternates: { canonical: "https://deckyvault.xyz/devices" }, openGraph: { title: "Devices — DeckyVault", description: "Browse benchmark data for handheld and console gaming devices on DeckyVault.", url: "https://deckyvault.xyz/devices", siteName: "DeckyVault", type: "website", }, } export default async function DevicesPage() { // Fetch all hardware devices ordered by sortOrder const deviceRows = await db .select({ slug: hardware.slug, name: hardware.name, deviceType: hardware.deviceType, image: hardware.image, sortOrder: hardware.sortOrder, }) .from(hardware) .orderBy(hardware.sortOrder) // Get aggregated stats per device const statsPerDevice = await db .select({ hardwareSlug: performanceEntries.hardwareSlug, totalBenchmarks: sql`count(*)::int`, avgFps: sql`round(avg(${performanceEntries.fpsAvg})::numeric, 1)`, verifiedCount: sql`count(*) filter (where ${performanceEntries.verifiedAt} is not null)::int`, gameCount: sql`count(distinct ${gameVersions.gameId})::int`, }) .from(performanceEntries) .innerJoin(gameVersions, eq(performanceEntries.versionId, gameVersions.id)) .innerJoin(games, eq(gameVersions.gameId, games.id)) .innerJoin(hardware, eq(performanceEntries.hardwareSlug, hardware.slug)) .where(eq(performanceEntries.isRemoved, false)) .groupBy(performanceEntries.hardwareSlug) const statsMap = new Map(statsPerDevice.map((s) => [s.hardwareSlug, s])) // Best game per device (highest avg FPS) const bestGames = await db .select({ hardwareSlug: performanceEntries.hardwareSlug, gameId: games.id, gameTitle: games.title, gameHeaderImage: games.headerImage, fpsAvg: sql`round(avg(${performanceEntries.fpsAvg})::numeric, 1)`, }) .from(performanceEntries) .innerJoin(gameVersions, eq(performanceEntries.versionId, gameVersions.id)) .innerJoin(games, eq(gameVersions.gameId, games.id)) .where(eq(performanceEntries.isRemoved, false)) .groupBy( performanceEntries.hardwareSlug, games.id, games.title, games.headerImage, ) .orderBy(desc(sql`avg(${performanceEntries.fpsAvg})`)) const bestGameMap = new Map< string, { id: string; title: string; headerImage: string | null; fpsAvg: number } >() for (const bg of bestGames) { if (!bestGameMap.has(bg.hardwareSlug)) { bestGameMap.set(bg.hardwareSlug, { id: bg.gameId, title: bg.gameTitle, headerImage: bg.gameHeaderImage, fpsAvg: Number(bg.fpsAvg), }) } } // Build device data with stats const devices = deviceRows.map((device, index) => { const stats = statsMap.get(device.slug) const bestGame = bestGameMap.get(device.slug) return { slug: device.slug, name: device.name, deviceType: device.deviceType, image: device.image, sortOrder: device.sortOrder, colorIndex: index, totalBenchmarks: stats?.totalBenchmarks ?? 0, avgFps: stats?.avgFps ? Number(stats.avgFps) : null, gameCount: stats?.gameCount ?? 0, verifiedCount: stats?.verifiedCount ?? 0, bestGame: bestGame ?? null, } }) // JSON-LD structured data const jsonLd = { "@context": "https://schema.org", "@type": "ItemList", itemListElement: devices.map((d, i) => ({ "@type": "ListItem", position: i + 1, name: d.name, url: `https://deckyvault.xyz/devices/${d.slug}`, })), } return ( <>