diff --git a/CHANGELOG.md b/CHANGELOG.md
index 44f5e57..f8841d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,7 @@
All notable changes to DeckyVault will be documented in this file.
-## [Unreleased] - 2026-04-30
+## [2026.0.95] - 2026-04-30
### Added
diff --git a/app/(manage)/manage/games/games-client.tsx b/app/(manage)/manage/games/games-client.tsx
index 1e05524..5155b72 100644
--- a/app/(manage)/manage/games/games-client.tsx
+++ b/app/(manage)/manage/games/games-client.tsx
@@ -326,7 +326,7 @@ export function GamesClient() {
setLoading(true)
try {
const res = await fetch(
- `/api/games?limit=${LIMIT}&offset=${offset}&search=${encodeURIComponent(search)}`
+ `/api/games?limit=${LIMIT}&offset=${offset}&search=${encodeURIComponent(search)}&sort=createdAt&order=desc`
)
if (res.ok && !cancelled) {
const json = (await res.json()) as GamesApiResponse
diff --git a/app/game/[id]/game-page-client.tsx b/app/game/[id]/game-page-client.tsx
index 95ffbfa..472a4d4 100644
--- a/app/game/[id]/game-page-client.tsx
+++ b/app/game/[id]/game-page-client.tsx
@@ -79,6 +79,7 @@ interface Game {
playabilityStatus?: "great" | "playable" | "needs_tweaks" | "unplayable" | "unknown" | null
steamReviewScore?: number | null
steamReviewSentiment?: string | null
+ steamReviewCount?: number | null
}
interface Counts {
diff --git a/app/game/[id]/page.tsx b/app/game/[id]/page.tsx
index c1d6158..03b5215 100644
--- a/app/game/[id]/page.tsx
+++ b/app/game/[id]/page.tsx
@@ -254,16 +254,25 @@ export default async function GamePage({
.orderBy(desc(performanceEntries.isPinned), desc(performanceEntries.upvotes)),
])
- // ── Sync logic: force or stale-while-revalidate ─────────────────
+ // ── Sync logic: force or stale-while-revalidate ────────────────
const shouldSync =
game.source === "steam" &&
game.steamAppId &&
(forceSync || isSyncStale(game.lastSync))
if (shouldSync) {
- after(async () => {
+ if (forceSync) {
+ // Block render on forced sync so user sees fresh data immediately
await syncSteamGame(game.steamAppId!)
- })
+ // Re-fetch game after sync so serialized data is fresh
+ const refreshed = await resolveGame(game.steamAppId!.toString())
+ if (refreshed) game = refreshed
+ } else {
+ // Stale sync happens after response so page isn't delayed
+ after(async () => {
+ await syncSteamGame(game.steamAppId!)
+ })
+ }
}
// Serialize for client component (Dates → strings)
@@ -293,6 +302,9 @@ export default async function GamePage({
releaseDate: game.releaseDate,
categories: game.categories,
platforms: game.platforms,
+ steamReviewScore: game.steamReviewScore,
+ steamReviewSentiment: game.steamReviewSentiment,
+ steamReviewCount: game.steamReviewCount,
}
const serializedPresets = presetRows.map((p) => ({
diff --git a/app/game/[id]/submit/page.tsx b/app/game/[id]/submit/page.tsx
index 5b16f16..4dfa7ea 100644
--- a/app/game/[id]/submit/page.tsx
+++ b/app/game/[id]/submit/page.tsx
@@ -2,7 +2,7 @@ import { notFound } from "next/navigation"
import { db } from "@/lib/db/index"
import { games, gameVersions, performanceEntries, gamePlatformSupport } from "@/lib/db/schema"
import { eq, sql } from "drizzle-orm"
-import { GameEntryWizard } from "@/components/wizard/game-entry-wizard"
+import { GameEntryWizard, type GameVersionInfo } from "@/components/wizard/game-entry-wizard"
// This page needs live data — skip static generation at build time
export const dynamic = "force-dynamic"
@@ -47,37 +47,34 @@ export default async function SubmitBenchmarkPage({
notFound()
}
- // Get or create the latest game version
- let [version] = await db
- .select()
+ // Fetch all game versions for version selection
+ const allVersions = await db
+ .select({
+ id: gameVersions.id,
+ versionString: gameVersions.versionString,
+ buildId: gameVersions.buildId,
+ isLatest: gameVersions.isLatest,
+ })
.from(gameVersions)
- .where(
- eq(gameVersions.gameId, game.id),
- )
+ .where(eq(gameVersions.gameId, game.id))
.orderBy(sql`${gameVersions.createdAt} DESC`)
- .limit(1)
// Create a default version if none exists
- if (!version) {
- [version] = await db
+ if (allVersions.length === 0) {
+ const [newVersion] = await db
.insert(gameVersions)
.values({
gameId: game.id,
isLatest: true,
})
.returning()
- }
-
- // Fetch platform support for anti-cheat awareness
- const platformSupport = await db
- .select({
- hardwareSlug: gamePlatformSupport.hardwareSlug,
- antiCheatRelevant: gamePlatformSupport.antiCheatRelevant,
- antiCheatName: gamePlatformSupport.antiCheatName,
- antiCheatStatus: gamePlatformSupport.antiCheatStatus,
+ allVersions.push({
+ id: newVersion.id,
+ versionString: newVersion.versionString,
+ buildId: newVersion.buildId,
+ isLatest: newVersion.isLatest,
})
- .from(gamePlatformSupport)
- .where(eq(gamePlatformSupport.gameId, game.id))
+ }
// If editing, fetch the existing performance entry
let editEntry = null
@@ -90,6 +87,26 @@ export default async function SubmitBenchmarkPage({
editEntry = entry ?? null
}
+ // Determine default version: when editing, use the entry's version;
+ // otherwise, use the latest (first in DESC order)
+ let defaultVersionId = allVersions[0].id
+ if (editEntry?.versionId) {
+ defaultVersionId = editEntry.versionId
+ }
+
+ const gameVersionInfos: GameVersionInfo[] = allVersions
+
+ // Fetch platform support for anti-cheat awareness
+ const platformSupport = await db
+ .select({
+ hardwareSlug: gamePlatformSupport.hardwareSlug,
+ antiCheatRelevant: gamePlatformSupport.antiCheatRelevant,
+ antiCheatName: gamePlatformSupport.antiCheatName,
+ antiCheatStatus: gamePlatformSupport.antiCheatStatus,
+ })
+ .from(gamePlatformSupport)
+ .where(eq(gamePlatformSupport.gameId, game.id))
+
return (
@@ -104,7 +121,8 @@ export default async function SubmitBenchmarkPage({
diff --git a/app/games/games-page-client.tsx b/app/games/games-page-client.tsx
index 611a142..8cd92e2 100644
--- a/app/games/games-page-client.tsx
+++ b/app/games/games-page-client.tsx
@@ -677,7 +677,7 @@ function GameCard({ game }: { game: GamesListItem }) {
{game.playabilityStatus && (
-
+
)}
{game.antiCheatRelevant && game.antiCheatStatus === "unsupported" && (
g.source === "steam" && g.steamAppId && isSyncStale(g.lastSync))
+ .map((g) => g.steamAppId!)
+
+ if (staleSteamAppIds.length > 0) {
+ after(async () => {
+ // Sync stale games sequentially with a small delay to avoid rate-limiting
+ for (const appId of staleSteamAppIds) {
+ try {
+ await syncSteamGame(appId)
+ } catch {
+ // Stale sync failure is non-fatal — data will be refreshed on next visit
+ }
+ await new Promise((r) => setTimeout(r, 1500))
+ }
+ })
+ }
+
// JSON-LD ItemList
const jsonLd = {
"@context": "https://schema.org",
diff --git a/app/search/page.tsx b/app/search/page.tsx
index c0ffec6..681cb9d 100644
--- a/app/search/page.tsx
+++ b/app/search/page.tsx
@@ -245,7 +245,7 @@ function SearchResultCard({
transition={{ duration: 0.3, delay: index * 0.02 }}
whileHover={{ scale: 1.005 }}
whileTap={{ scale: 0.995 }}
- className="group relative bg-text/[0.03] border border-border rounded-xl p-4 sm:p-5 transition-colors duration-200 hover:border-text/30 hover:bg-text/[0.06] cursor-pointer focus-within:outline-none focus-within:ring-2 focus-within:ring-text/20 focus-within:ring-offset-2 focus-within:ring-offset-background"
+ className="group relative bg-text/3 border border-border rounded-xl p-4 sm:p-5 transition-colors duration-200 hover:border-text/30 hover:bg-text/[0.06] cursor-pointer focus-within:outline-none focus-within:ring-2 focus-within:ring-text/20 focus-within:ring-offset-2 focus-within:ring-offset-background"
title={isLocal
? "Click to view game details, benchmarks, and settings"
: "Click to add this game to DeckyVault and view its page"
diff --git a/components/comments/comment-section.tsx b/components/comments/comment-section.tsx
index 33c728e..29a7478 100644
--- a/components/comments/comment-section.tsx
+++ b/components/comments/comment-section.tsx
@@ -21,6 +21,7 @@ interface CommentsApiResponse {
export function CommentSection({ gameId, initialCount }: CommentSectionProps) {
const { data: session } = useSession()
+ const [mounted, setMounted] = useState(false)
const [comments, setComments] = useState([])
const [total, setTotal] = useState(initialCount)
const [offset, setOffset] = useState(0)
@@ -29,6 +30,10 @@ export function CommentSection({ gameId, initialCount }: CommentSectionProps) {
const [submitting, setSubmitting] = useState(false)
const limit = 20
+ useEffect(() => {
+ setMounted(true)
+ }, [])
+
// Initial load
useEffect(() => {
let cancelled = false
@@ -125,8 +130,8 @@ export function CommentSection({ gameId, initialCount }: CommentSectionProps) {
({total})
- {/* Compose */}
- {session ? (
+ {/* Compose — suppress until mounted to avoid hydration mismatch */}
+ {mounted && session ? (
- ) : (
+ ) : mounted ? (
+ ) : (
+
)}
{/* Comment list */}
diff --git a/components/playability-badge.tsx b/components/playability-badge.tsx
index 1665a38..f4fa228 100644
--- a/components/playability-badge.tsx
+++ b/components/playability-badge.tsx
@@ -68,7 +68,7 @@ export function PlayabilityBadge({
)}
title={config.label}
>
-
+
{showLabel && config.label}
);
diff --git a/components/wizard/game-entry-wizard.tsx b/components/wizard/game-entry-wizard.tsx
index 5b03eca..806e6c8 100644
--- a/components/wizard/game-entry-wizard.tsx
+++ b/components/wizard/game-entry-wizard.tsx
@@ -4,8 +4,8 @@ import { useState, useCallback, useEffect } from "react"
import { useRouter } from "next/navigation"
import { motion, AnimatePresence } from "motion/react"
import { StepIndicator } from "@/components/wizard/step-indicator"
-import { HardwareStep } from "@/components/wizard/steps/hardware-step"
-import { AntiCheatStep } from "@/components/wizard/steps/anti-cheat-step"
+import { SetupStep, type GameVersionInfo } from "@/components/wizard/steps/setup-step"
+import { AntiCheatStep, type AntiCheatData } from "@/components/wizard/steps/anti-cheat-step"
import { PerformanceStep, type PerformanceData } from "@/components/wizard/steps/performance-step"
import { SettingsStep } from "@/components/wizard/steps/settings-step"
import { EnvironmentStep, type EnvironmentData } from "@/components/wizard/steps/environment-step"
@@ -13,9 +13,11 @@ import { ReviewStep } from "@/components/wizard/steps/review-step"
import type { SettingCategory } from "@/components/wizard/settings-editor"
import { performanceEntries } from "@/lib/db/schema"
+// Export GameVersionInfo so the server page can use it
+export type { GameVersionInfo }
+
const STEPS = [
- { label: "Hardware", tooltip: "Choose the hardware you tested this game on" },
- { label: "Anti-Cheat", tooltip: "Review anti-cheat compatibility for your selected hardware" },
+ { label: "Setup", tooltip: "Choose the hardware, game version, and anti-cheat status" },
{ label: "Performance", tooltip: "Enter the performance metrics you observed. FPS Average is required." },
{ label: "Settings", tooltip: "Configure the game settings you used. Add categories and settings to help others replicate your setup." },
{ label: "Environment", tooltip: "Specify the software environment and any launch options used" },
@@ -25,30 +27,63 @@ const STEPS = [
interface PlatformSupportEntry {
hardwareSlug: string
antiCheatRelevant: boolean
- antiCheatStatus: "none" | "supported" | "unsupported" | "unknown"
antiCheatName: string | null
+ antiCheatStatus: "none" | "supported" | "unsupported" | "unknown"
}
interface GameEntryWizardProps {
gameId: string
- gameVersionId: string
+ gameVersions: GameVersionInfo[]
+ defaultVersionId: string
editEntry?: typeof performanceEntries.$inferSelect | null
platformSupport: PlatformSupportEntry[]
}
-export function GameEntryWizard({ gameId, gameVersionId, editEntry, platformSupport }: GameEntryWizardProps) {
+export function GameEntryWizard({ gameId, gameVersions, defaultVersionId, editEntry, platformSupport }: GameEntryWizardProps) {
const router = useRouter()
const [currentStep, setCurrentStep] = useState(0)
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState
(null)
const [success, setSuccess] = useState(false)
- // Step 1: Hardware
+ // Step 0: Setup — Hardware
const [hardwareSlug, setHardwareSlug] = useState(editEntry?.hardwareSlug ?? "")
const [hardwareName, setHardwareName] = useState("")
- // Step 2: Anti-Cheat (informational)
- // Step 3: Performance
+ // Step 0: Setup — Game Version
+ const [selectedVersionId, setSelectedVersionId] = useState(defaultVersionId)
+ const [newVersionString, setNewVersionString] = useState("")
+ const [isCreatingVersion, setIsCreatingVersion] = useState(false)
+
+ // Step 0: Setup — Anti-Cheat
+ const [antiCheat, setAntiCheat] = useState({
+ antiCheatRelevant: false,
+ antiCheatName: "",
+ antiCheatStatus: "none",
+ })
+
+ // Initialize anti-cheat from existing platformSupport when editing
+ useEffect(() => {
+ const entry = platformSupport.find(
+ (p) => p.hardwareSlug === hardwareSlug && p.antiCheatRelevant
+ ) ?? platformSupport.find((p) => p.antiCheatRelevant)
+
+ if (entry) {
+ setAntiCheat({
+ antiCheatRelevant: entry.antiCheatRelevant,
+ antiCheatName: entry.antiCheatName ?? "",
+ antiCheatStatus: entry.antiCheatStatus,
+ })
+ } else {
+ setAntiCheat({
+ antiCheatRelevant: false,
+ antiCheatName: "",
+ antiCheatStatus: "none",
+ })
+ }
+ }, [hardwareSlug, platformSupport])
+
+ // Step 1: Performance
const [performance, setPerformance] = useState(
editEntry
? {
@@ -62,12 +97,12 @@ export function GameEntryWizard({ gameId, gameVersionId, editEntry, platformSupp
: {},
)
- // Step 4: Settings
+ // Step 2: Settings
const [settingsJson, setSettingsJson] = useState(
editEntry?.settingsJson ?? [],
)
- // Step 5: Environment
+ // Step 3: Environment
const [environment, setEnvironment] = useState(
editEntry
? {
@@ -86,7 +121,7 @@ export function GameEntryWizard({ gameId, gameVersionId, editEntry, platformSupp
},
)
- // Step 6: Notes
+ // Step 4: Notes
const [userNotes, setUserNotes] = useState(editEntry?.userNotes ?? "")
// Fetch hardware name when slug changes
@@ -128,19 +163,30 @@ export function GameEntryWizard({ gameId, gameVersionId, editEntry, platformSupp
return () => { cancelled = true }
}, [editEntry, hardwareSlug])
+ // Resolve the version label for display
+ const getVersionLabel = useCallback(() => {
+ if (selectedVersionId === "__new__") {
+ return newVersionString || "New version"
+ }
+ const v = gameVersions.find((v) => v.id === selectedVersionId)
+ if (!v) return "Unknown"
+ return v.versionString || (v.buildId ? `Build ${v.buildId}` : "Unknown version")
+ }, [selectedVersionId, newVersionString, gameVersions])
+
const canProceed = () => {
switch (currentStep) {
- case 0:
- return hardwareSlug !== ""
- case 1:
- return true // Anti-cheat is informational
- case 2:
+ case 0: // Setup
+ if (hardwareSlug === "") return false
+ // If new version selected, require version string
+ if (selectedVersionId === "__new__" && !newVersionString.trim()) return false
+ return true
+ case 1: // Performance
return performance.fpsAvg !== undefined && performance.fpsAvg > 0
- case 3:
- return true // Settings are optional
- case 4:
- return true // Environment is optional
- case 5:
+ case 2: // Settings
+ return true
+ case 3: // Environment
+ return true
+ case 4: // Review
return true
default:
return false
@@ -165,13 +211,46 @@ export function GameEntryWizard({ gameId, gameVersionId, editEntry, platformSupp
}
}
+ // Resolve the final version ID — create a new version if needed
+ const resolveVersionId = async (): Promise => {
+ if (selectedVersionId !== "__new__") {
+ return selectedVersionId
+ }
+
+ // Create a new version via API
+ setIsCreatingVersion(true)
+ try {
+ const res = await fetch(`/api/games/${gameId}/versions`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ versionString: newVersionString.trim(),
+ isLatest: false,
+ }),
+ })
+
+ if (!res.ok) {
+ const data = await res.json()
+ throw new Error(data.error || "Failed to create game version")
+ }
+
+ const data = await res.json() as { data: { id: string } }
+ return data.data.id
+ } finally {
+ setIsCreatingVersion(false)
+ }
+ }
+
const handleSubmit = async () => {
setIsSubmitting(true)
setError(null)
try {
+ // Resolve version ID (may create a new version)
+ const versionId = await resolveVersionId()
+
const payload = {
- versionId: gameVersionId,
+ versionId,
hardwareSlug,
fpsAvg: Number(performance.fpsAvg),
fpsOnePercentLow: performance.fpsOnePercentLow !== undefined ? Number(performance.fpsOnePercentLow) : null,
@@ -189,6 +268,9 @@ export function GameEntryWizard({ gameId, gameVersionId, editEntry, platformSupp
customSystem: environment.customSystem ?? false,
settingsJson: settingsJson.length > 0 ? settingsJson : null,
userNotes: userNotes || null,
+ antiCheatRelevant: antiCheat.antiCheatRelevant,
+ antiCheatName: antiCheat.antiCheatName || null,
+ antiCheatStatus: antiCheat.antiCheatStatus,
}
const url = editEntry
@@ -260,25 +342,38 @@ export function GameEntryWizard({ gameId, gameVersionId, editEntry, platformSupp
className="min-h-[300px]"
>
{currentStep === 0 && (
-
+
)}
{currentStep === 1 && (
-
- )}
- {currentStep === 2 && (
)}
- {currentStep === 3 && (
+ {currentStep === 2 && (
)}
- {currentStep === 4 && (
+ {currentStep === 3 && (
)}
- {currentStep === 5 && (
+ {currentStep === 4 && (
{/* Navigation Buttons */}
- {currentStep < 5 && (
+ {currentStep < 4 && (