From 7db930302cf171b996bd88c0887cb1f029ab7ee0 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Mon, 27 Apr 2026 14:03:03 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20add=20consolidation=20spec=20=E2=80=94?= =?UTF-8?q?=20merge=20communityPresets=20into=20performanceEntries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-04-27-consolidate-presets-design.md | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-27-consolidate-presets-design.md diff --git a/docs/superpowers/specs/2026-04-27-consolidate-presets-design.md b/docs/superpowers/specs/2026-04-27-consolidate-presets-design.md new file mode 100644 index 0000000..2c2f3e4 --- /dev/null +++ b/docs/superpowers/specs/2026-04-27-consolidate-presets-design.md @@ -0,0 +1,90 @@ +# Consolidate Community Presets into Performance Entries + +## Problem + +The `community_presets` table is a separate entity that duplicates much of `performance_entries` (settingsJson, hardwareSlug, upvotes, timestamps) while requiring complex JOINs on the game page. There is no UI to create presets — the wizard only creates performance entries. Meanwhile, users cannot delete their own submissions. + +## Design + +### 1. Eliminate communityPresets — settingsJson ≠ null IS the preset + +Any `performanceEntry` where `settingsJson IS NOT NULL` automatically appears as a "Community Preset" on the game page. No new column needed. The `settingsJson` column already exists on `performanceEntries` with the exact same type as `communityPresets.settingsJson`. + +**Display logic**: Preset display names are auto-generated from entry data: `"{hardwareName} · {fpsAvg}fps"` with upscaler/frame-gen badges if present. No `name` or `description` columns added. + +**Migration**: Drop `communityPresets` table entirely (no production data to preserve). No data migration needed. + +### 2. User-owned hard delete + +Users can permanently delete their own performance entries via `DELETE /api/performance/:id`. Authorization: `userId === session.user.id || role === "admin"`. This is a hard `DELETE FROM` — not a soft delete. + +### 3. Unified voting + +The existing `upvotes`/`downvotes` columns on `performanceEntries` replace the single `upvotes` column on `communityPresets`. Entries with settingsJson (presets) use the same upvote/downvote endpoints that already exist on the performance API. + +## Affected Files + +### Schema +- `lib/db/schema/communityPresets.ts` — DELETE entire file +- `lib/db/schema/index.ts` — Remove communityPresets re-export +- `lib/db/schema/performanceEntries.ts` — No changes (already has settingsJson, upvotes, downvotes) + +### API Routes +- `lib/api/presets.ts` — DELETE entire file (CRUD, upvote, settings routes all removed) +- `lib/api/index.ts` — Remove preset route re-exports +- `lib/api/performance.ts` — Add user-scoped hard delete endpoint +- `lib/api/search-unified.ts` — Replace communityPresets count with filtered performanceEntries count + +### Pages / UI +- `app/game/[id]/page.tsx` — Rewrite preset query: SELECT from performanceEntries WHERE settingsJson IS NOT NULL, JOIN gameVersions+hardware. Remove communityPresets imports/queries. +- `app/game/[id]/game-page-client.tsx` — Update Preset interface to match new data shape. Remove `name`/`description` fields. Auto-generate display names. + +### Migration +- New Drizzle migration: DROP TABLE community_presets + +## Query Changes + +### Before (game page presets) +```sql +SELECT cp.*, pe.fps_avg, pe.fps_low, ... +FROM community_presets cp + LEFT JOIN performance_entries pe ON cp.performance_entry_id = pe.id + INNER JOIN hardware h ON cp.hardware_slug = h.slug +WHERE cp.game_id = $1 +ORDER BY cp.upvotes DESC +``` + +### After (game page presets) +```sql +SELECT pe.id, pe.hardware_slug, h.name as hardware_name, + pe.fps_avg, pe.fps_low, pe.fps_high, + pe.upscaler_type, pe.upscaler_version, pe.frame_gen_method, + pe.proton_version, pe.os_version, + pe.settings_json, pe.upvotes, pe.created_at +FROM performance_entries pe + INNER JOIN game_versions gv ON pe.version_id = gv.id + INNER JOIN hardware h ON pe.hardware_slug = h.slug +WHERE gv.game_id = $1 + AND pe.settings_json IS NOT NULL + AND pe.is_removed = false +ORDER BY pe.upvotes DESC +``` + +## Preset Display Name Generation + +```typescript +function generatePresetName(preset: Preset): string { + const parts = [preset.hardwareName] + if (preset.fpsAvg !== null) parts.push(`${preset.fpsAvg}fps`) + return parts.join(' · ') +} +``` + +Upscaler and frame-gen are shown as badges next to the name, not in the name itself — matching the current game-page-client.tsx rendering. + +## Edge Cases + +- **Empty settingsJson array `[]`**: The wizard sends `settingsJson: settingsJson.length > 0 ? settingsJson : null`, so empty arrays are never stored. The query uses `IS NOT NULL` which is sufficient. +- **User tries to delete another user's entry**: 403 Forbidden unless admin. +- **Game page with no entries having settingsJson**: The presets section shows "No presets yet" — same as current empty state. +- **Upvoting presets**: Use the existing `POST /api/performance/:id/upvote` endpoint — works for all entries including presets.