Files
deckyvault/docs/superpowers/specs/2026-04-27-devices-admin-design.md

14 KiB

Devices Page Redesign & Admin Panel — Design Spec

Date: 2026-04-27 Scope: Fix devices page bugs, redesign devices list/detail pages, add admin management panel, remove broken Community Trust graph


1. Bug Fixes

1.1 "handled""handheld" Enum Typo

The device_type PostgreSQL enum has "handled" instead of "handheld". This propagates to every layer:

  • DB: pgEnum("device_type", ["handled", "console"]) in lib/db/schema/hardware.ts
  • Seed: "handled" as const in lib/db/seed.ts
  • API responses: deviceType: "handled" returned from /api/hardware/stats and /api/hardware/:slug/stats
  • Client maps: List page uses { handheld: "Handheld" } (never matches), detail page uses { handled: "Handheld" } (matches the typo)
  • JSON-LD: Outputs "handled" as the category field on device detail pages

Fix: Create a Drizzle migration that:

  1. Renames the enum value from "handled" to "handheld" using ALTER TYPE device_type RENAME VALUE 'handled' TO 'handheld'
  2. Updates any existing rows that reference the old value (though enum values are stored by reference, not value, so the ALTER TYPE handles this)

Then update all code references:

  • lib/db/schema/hardware.ts: pgEnum("device_type", ["handheld", "console"])
  • lib/db/seed.ts: "handheld" as const
  • app/devices/page-client.tsx: deviceTypeLabel map (already correct with "handheld" key)
  • app/devices/[slug]/device-detail-client.tsx: deviceTypeLabel map (change "handled""handheld")

1.2 upscalerBreakdown vs fsrBreakdown Property Mismatch

The API (hardware-stats.ts) returns upscalerBreakdown on the detail stats, but the client (device-detail-client.tsx) TypeScript interface and render code uses fsrBreakdown. At runtime, stats.fsrBreakdown is undefined, silently breaking the upscaler section.

Fix: Rename the client interface and rendering code to use upscalerBreakdown to match the API. Update the TypeScript interface and all references in device-detail-client.tsx.

1.3 avgFps Includes Nulls as Zero

In lib/api/hardware-stats.ts, the /:slug/stats endpoint computes:

const avgFps = Math.round((entries.reduce((s, e) => s + (e.fpsAvg ?? 0), 0) / totalBenchmarks) * 10) / 10

This treats null FPS entries as 0, reducing the true average. The /stats list endpoint already handles this correctly using SQL avg() which ignores nulls.

Fix: Filter out null FPS entries before computing the average:

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

2. Devices Page Redesign

2.1 List Page (/devices)

Architecture: Server component fetches devices with aggregated stats from DB, passes serialized data as props to client component. Eliminates the redundant client-side fetch to /api/hardware/stats.

Layout:

  • Hero header with title "Devices" and description "Browse benchmarks for handheld and console devices"
  • Pill-based device type filter tabs: All / Handheld / Console (matches game-page filter pattern)
  • Responsive card grid (1-col mobile, 2-col tablet, 3-col desktop)
  • Stagger animation capped at min(i * 0.05, 0.5) seconds max delay

Card design:

  • Device image (or icon fallback with colored background) — top/leading section
  • Device name (bold, hover → primary color)
  • Device type badge (colored pill: Handheld = pink, Console = purple)
  • 4-column stat row: Benchmarks / Avg FPS / Games / Verified
  • Top game preview (title + FPS)
  • Arrow icon on hover

SEO:

  • Static metadata with title, description, keywords, openGraph, alternates
  • JSON-LD ItemList with correct device type labels ("Handheld" not "handled")

2.2 Detail Page (/devices/[slug])

Architecture: Server component fetches device info + stats from the API endpoint (server-side), passes serialized data as props. Eliminates client-side fetch. Falls back to notFound() for invalid slugs.

Hero section:

  • Device image (or large styled Gamepad2Icon in colored container as fallback)
  • Device name (large, bold)
  • Device type badge
  • Verified count badge

Stats row: 4 stat cards — Total Benchmarks / Average FPS / Games Tested / Verified (same StatCard component pattern as game page)

Charts section:

  • Historical FPS line chart (same as current)
  • FPS Boxplot per game (same as current)
  • Genre Breakdown donut (same as current)
  • Proton Version Distribution bar (same as current)
  • Upscaler Version Performance list (renamed from FSR, same data)
  • Top Games grid (same as current, but fixed device color per device)

Removed: Community Trust bar chart (TrustBar). No replacement — the section is simply removed from both the game detail page and the device detail page.

Error handling: Error state with retry button when fetch fails.

generateStaticParams: Added with revalidate = 3600 (ISR, 1-hour cache).

Per-device OG image: app/devices/[slug]/opengraph-image.tsx renders device name + type + stats on branded dark background.

SEO: Dynamic generateMetadata with title, description, keywords, openGraph, alternates, robots.

2.3 Hardware Schema — Image Column

Add nullable image column to hardware table:

image: text("image"), // nullable — URL to device image

Migration adds the column as optional. Seed updated to include image URLs where available. Where image is null, UI shows a styled icon fallback.

2.4 Device-specific Colors

Currently getDeviceColor(0) hardcodes pink for all devices. Fix to use device slug as index:

const colorIndex = CHART_THEME.deviceColors.length > 0
  ? devices.findIndex(d => d.slug === device.slug) % CHART_THEME.deviceColors.length
  : 0

Or simpler: pass device list index from server.


3. Community Trust Removal

Remove the TrustBar chart from both pages where it appears:

  • app/game/[id]/game-page-client.tsx: Remove TrustBar import, remove trust from the stats interface, remove the "Community Trust" section from the render
  • components/charts/TrustBar.tsx: Delete the file
  • lib/api/game-stats.ts: Remove section 9 (trust data) from the response — stop shipping trust array
  • lib/api/hardware-stats.ts: No trust data in this endpoint (already doesn't have it)

The trust/upvote/downvote columns remain in the database — they're not removed. The vote API endpoints (/:id/upvote, /:id/downvote) also remain. This is purely a UI removal of a chart that displays meaningless data. A proper voting system can be built in the future and the chart re-added at that time.


4. Admin Panel

4.1 Route Structure

app/(admin)/
  layout.tsx              — shared layout: sidebar + auth guard
  admin/
    page.tsx              — redirect to /admin/users
    users/
      page.tsx            — server: fetch users, pass to client
      users-client.tsx    — client: user management UI
    hardware/
      page.tsx            — server: fetch hardware, pass to client
      hardware-client.tsx — client: hardware CRUD UI
    games/
      page.tsx            — server: fetch games, pass to client
      games-client.tsx    — client: game management UI

The (admin) route group uses parentheses to avoid nesting in URLs. The layout at app/(admin)/layout.tsx wraps all admin pages with the sidebar and auth guard.

URL structure:

  • /admin → redirects to /admin/users
  • /admin/users → user management
  • /admin/hardware → hardware management
  • /admin/games → game management

4.2 Auth Guard

Server-side: The admin layout component checks auth.api.getSession() from headers. If no session or user.role !== "admin", call redirect("/") from next/navigation.

Client-side: As a fallback, the client components check useSession() and redirect non-admins.

Metadata: All admin pages set robots: { index: false } to prevent search engine indexing.

4.3 Sidebar Component

components/admin/admin-sidebar.tsx — reusable sidebar nav component.

Pattern: Matches the existing settings-container.tsx sidebar pattern (vertical on desktop, horizontal scroll on mobile).

  • Desktop: md:w-48 shrink-0 sidebar with border-right, active item gets bg-primary/10 text-primary border-l-primary border-l-2
  • Mobile: Horizontal scrollable tabs with overflow-x-auto
  • Active detection: Uses usePathname() to highlight current section
  • Items:
    • Users (icon: UsersIcon)
    • Hardware (icon: CpuIcon)
    • Games (icon: Gamepad2Icon)

4.4 Admin — Users Page

Data: Uses authClient.admin.listUsers() with pagination. Falls back to a custom API endpoint if better-auth's listUsers doesn't provide enough data.

UI:

  • Search input (filters by name/email)
  • Table: Avatar (or initials) | Name | Email | Role (badge) | Status (active/banned) | Joined | Actions
  • Actions dropdown: Change Role (user/contributor/admin), Ban/Unban
  • Role badges: admin in red, contributor in yellow, user in gray
  • Status: active (green dot) / banned (red dot)

API: Uses better-auth admin plugin client methods:

  • authClient.admin.listUsers() for listing
  • authClient.admin.banUser({ userId }) / authClient.admin.unbanUser({ userId }) for banning
  • authClient.admin.setRole({ userId, role }) for role changes

4.5 Admin — Hardware Page

Data: Fetches from /api/hardware/stats (existing endpoint) for the list with aggregated stats. For create/edit, uses /api/hardware CRUD endpoints.

UI:

  • "Add Device" button at top
  • Card list (not table — matches the public devices page pattern)
  • Each card: device image (or icon) | name | slug | type badge | stats row (benchmarks / FPS / games) | Edit / Delete buttons
  • Edit modal: inline form for name, slug, device type, sort order, image URL
  • Delete: confirmation dialog, calls DELETE /api/hardware/:slug
  • Create: modal form, calls POST /api/hardware

4.6 Admin — Games Page

Data: Fetches from /api/games with pagination and search support (existing CRUD endpoint).

UI:

  • Search input with debounce
  • Table: Cover image | Title | Developer | Source (steam/manual) | Sync status | Benchmarks count | Actions
  • Actions: View on site (link to /game/:id), Sync now (triggers re-sync from Steam), Delete
  • Pagination controls at bottom

4.7 Navbar Update

Add admin link to the profile dropdown in components/navbar.tsx:

{session?.user?.role === "admin" && (
  <Link href="/admin" className="flex items-center gap-2 ...">
    <ShieldIcon className="h-4 w-4" />
    Admin
  </Link>
)}

Conditionally rendered based on session.user.role === "admin".

4.8 Routes Update

Add admin route to lib/routes.ts — but only for reference, not in the main nav. Admin link goes in the profile dropdown only.

4.9 Sitemap Update

Exclude /admin and its subpages from app/sitemap.ts. Admin pages should not appear in the sitemap.


5. SEO Improvements Summary

Page Current Improved
/devices Basic title/description Full OG, keywords, correct JSON-LD
/devices/[slug] Basic metadata generateMetadata with full OG, keywords, per-device OG image
/admin/* N/A robots: { index: false }, noIndex: true
All device pages "handled" in JSON-LD "Handheld" corrected label
Device detail No generateStaticParams ISR with 1-hour revalidation

6. Files to Create

File Purpose
drizzle/0011_rename-handled-to-handheld-add-image.sql DB migration
app/devices/opengraph-image.tsx Per-device OG image
app/(admin)/layout.tsx Admin layout with sidebar + auth guard
app/(admin)/admin/page.tsx Redirect to /admin/users
app/(admin)/admin/users/page.tsx Users server component
app/(admin)/admin/users/users-client.tsx Users management UI
app/(admin)/admin/hardware/page.tsx Hardware server component
app/(admin)/admin/hardware/hardware-client.tsx Hardware CRUD UI
app/(admin)/admin/games/page.tsx Games server component
app/(admin)/admin/games/games-client.tsx Games management UI
components/admin/admin-sidebar.tsx Reusable admin sidebar nav

7. Files to Modify

File Changes
lib/db/schema/hardware.ts Fix enum to "handheld", add image column
lib/db/seed.ts Fix "handled""handheld"
lib/api/hardware-stats.ts Fix avgFps nulls, rename fsrBreakdownupscalerBreakdown (already named that)
app/devices/page.tsx RSC data fetching, enhanced metadata
app/devices/page-client.tsx Full redesign with filter tabs, cards, fix deviceTypeLabel
app/devices/[slug]/page.tsx Enhanced metadata, generateStaticParams, RSC data passing
app/devices/[slug]/device-detail-client.tsx Fix bugs, remove TrustBar, image/icon hero, error state
app/game/[id]/game-page-client.tsx Remove TrustBar import and section
lib/api/game-stats.ts Remove trust from response
components/charts/TrustBar.tsx Delete
lib/routes.ts Add admin route reference
components/navbar.tsx Add conditional admin link
app/sitemap.ts Exclude admin routes

8. Out of Scope

  • Reports management page — deferred to future iteration
  • Admin dashboard/overview — deferred to future iteration
  • Performance entry verification UI in admin — deferred, can be added to a future admin/performance page
  • Voting system redesign — removing TrustBar is in scope, but building a proper vote deduplication system is out of scope
  • Device image uploads — image column accepts URLs; upload flow is out of scope
  • SQL aggregation optimization for device stats endpoint — beneficial but out of scope for this iteration