Files
deckyvault/docs/2025-05-05-sync-pwa-architectural-spec.md
T

260 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Architectural Spec: Sync Unification, Nav Cleanup & PWA
**Date:** 2025-05-05
**Status:** Draft — Awaiting Tactical Planning
**Author:** Autonomous Architect
---
## 1. Problem Statement
Three distinct objectives converge on a single architectural concern: **data integrity through pipeline unification**. The remaining objectives (nav removal, PWA) are scoped-down implementation tasks that share no domain coupling with the sync work.
### 1.1 Observed Symptoms
| Symptom | Root Cause |
|---|---|
| Steam review scores missing after initial game insert | Two separate code paths insert games; only one fetches review data |
| Review data appears only after manual sync from `/manage` | Manual sync uses the complete `syncSteamGame` pipeline; initial insert does not |
| Playability status not recalculated on initial insert | `recalculatePlayability` is called only in `syncSteamGame`, not in stub creation |
### 1.2 Three Divergent Sync Paths
**Path A — Initial Insert (two locations):**
- `lib/api/game-stub.ts``/games/stub` (Elysia API route)
- `app/game/[id]/page.tsx``createGameStub()` (Next.js server component)
- Fetches Steam API directly, inserts game row, returns
- **Missing**: `steamReviewScore`, `steamReviewSentiment`, `steamReviewCount`, `recalculatePlayability` call, error tracking fields (`syncRetryCount`, `syncNextRetry`, `syncError`)
**Path B — Stale-While-Revalidate Auto-Sync:**
- `app/game/[id]/page.tsx` → calls `syncSteamGame()`
- `app/games/page.tsx` → calls `syncSteamGame()`
- Uses full pipeline: Steam API + reviews + playability + error tracking
**Path C — Manual Dashboard Sync:**
- `lib/api/games.ts``gameSyncRoutes``POST /games/:gameId/sync`
- Uses same `syncSteamGame()` as Path B
**Conclusion**: Paths B and C are identical. Path A diverges entirely.
---
## 2. Architecture Boundaries
### 2.1 Source of Truth
`syncSteamGame(steamAppId: number)` in `lib/steam/sync.ts` is the **single canonical sync pipeline**. Every code path that creates or updates a Steam game's data MUST route through this function.
### 2.2 Forbidden Patterns
- No direct `fetch("https://store.steampowered.com/api/appdetails/...")` calls outside of `lib/steam/sync.ts`
- No manual `db.insert(games)` or `db.update(games)` for Steam-sourced games that populates fields covered by `syncSteamGame`
- No duplicate field-mapping logic between Steam API response and DB schema
### 2.3 Allowed Extensions
- `syncSteamGame` may be extended with new fields (e.g., Steam Deck verification status)
- Wrapper functions may call `syncSteamGame` with pre/post hooks (e.g., batch sync with progress reporting)
- The stub-creation flow may insert a **minimal stub** (appId + title only) and then delegate to `syncSteamGame` for full population
---
## 3. Sync Unification Design
### 3.1 Two-Stage Insert Pattern
For initial game creation, use a **stub-then-sync** pattern:
```
User provides steamAppId
┌─────────────────────────────┐
│ 1. Check if exists │
│ (by steamAppId) │
│ ├─ Exists → return │
│ └─ New → continue │
├─────────────────────────────┤
│ 2. Insert MINIMAL stub │
│ Fields: steamAppId, │
│ source="steam", │
│ title (placeholder), │
│ syncStatus="pending" │
├─────────────────────────────┤
│ 3. Call syncSteamGame() │
│ Populates ALL fields: │
│ title, developer, │
│ publisher, description, │
│ genres, images, prices, │
│ metacritic, reviews, │
│ platforms, requirements, │
│ categories, releaseDate, │
│ lastSync, syncStatus, │
│ error tracking │
├─────────────────────────────┤
│ 4. recalculatePlayability() │
│ (called inside │
│ syncSteamGame) │
├─────────────────────────────┤
│ 5. Return complete game │
└─────────────────────────────┘
```
### 3.2 Affected Files
| File | Change |
|---|---|
| `lib/api/game-stub.ts` | Replace direct Steam fetch + insert with stub insert → `syncSteamGame` call |
| `app/game/[id]/page.tsx` `createGameStub()` | Same refactor; delegate to `syncSteamGame` after minimal stub insert |
| `lib/steam/sync.ts` `syncSteamGame()` | Verify it handles the case where a game row exists but has only stub fields (idempotent update) — current implementation uses `db.update()` so it already requires the row to exist. This is correct for the two-stage pattern. |
### 3.3 Error Semantics
- **Stub insert failure**: Return 500 immediately — database is unreachable
- **Sync failure after stub exists**: The stub persists with `syncStatus="error"` and `syncError` populated. The game page renders with partial data. Retry is governed by exponential backoff in `recordSyncFailure`.
- **Type rejection** (e.g., DLC, soundtrack): The stub should not be inserted. Validate type before inserting the stub, or insert the stub and let `syncSteamGame` mark it as errored (prefer validating upfront to avoid dead rows).
### 3.4 Validation Checklist
After unification, verify:
- [ ] New game insert via search: `steamReviewScore` is populated immediately
- [ ] New game insert via direct URL: `steamReviewScore` is populated immediately
- [ ] Existing stale game visited after 7+ days: auto-sync refreshes all fields
- [ ] Manual sync from `/manage`: no regression (already uses `syncSteamGame`)
- [ ] Non-game Steam IDs (DLC, demos) are rejected before stub creation
- [ ] Playability is recalculated on every sync (insert or update)
---
## 4. Navbar: Remove "Updates"
### 4.1 Scope
Single-file change. No cascading impacts.
### 4.2 Affected Files
| File | Change |
|---|---|
| `lib/routes.ts` | Remove the `{ title: "Updates", href: "/updates" }` entry from the `routes` array |
### 4.3 Considerations
- The `/updates` page directory (`app/updates/`) may remain on disk (dead code). Can be removed in a separate cleanup pass. Priority: remove nav entry immediately.
- No import references to check — `routes` is consumed only by `components/navbar.tsx` and the mobile sidebar, both of which iterate `routes` dynamically.
---
## 5. PWA & Steam Deck UX
### 5.1 Current State
- ✅ Web manifest exists (`app/manifest.ts`) with `display: "standalone"`, theme color, icons
- ❌ No service worker registered — zero offline capability
- ❌ No touch-optimized interactions (hover-dependent UI elements)
- ❌ No gamepad navigation support
- ❌ No `viewport` meta with `user-scalable=no` for installed PWA feel
-`next.config.ts` has remote image patterns configured (Steam CDN, SteamGridDB)
### 5.2 Design Decisions (Deferred to Tactical Planning)
These require explicit user input — the following are **constraints**, not implementation details:
#### 5.2.1 Offline Strategy
**Constraint**: Must use Workbox or next-pwa for service worker generation. The SW must:
- Precache the app shell (layout, navbar, CSS, fonts)
- Cache game pages on first visit (stale-while-revalidate for HTML, cache-first for images from Steam CDN)
- Cache API responses from the Elysia backend for a short TTL (5 minutes for listings, 1 hour for game details)
- Provide a "You're offline" fallback UI when uncached pages are requested
**Open question**: Should offline mode show a curated "previously viewed" list, or a generic offline message?
#### 5.2.2 Touch Optimization
**Constraint**: All interactive elements must meet a minimum 44×44px touch target (WCAG 2.1 AA). Specific areas:
- Navbar links and hamburger menu
- Game card tap targets (currently `Link` wrapping entire card — verify touch area)
- Filter controls on `/games` (chips, dropdowns)
- Comment submission buttons
- Profile/settings forms
**Open question**: Should touch optimization include a dedicated "Deck Mode" layout toggle, or be applied universally as responsive CSS?
#### 5.2.3 Gamepad Navigation
**Constraint**: Use the Gamepad API (`navigator.getGamepads()`). Implementation must:
- Map D-pad/left stick to focus navigation (roving tabindex)
- Map A button to `click()` on focused element
- Map B button to browser back
- Map L1/R1 (bumpers) to tab switching on game pages
- Provide a visual focus ring distinct from `:focus-visible` so mouse users aren't affected
- Only activate when a gamepad input is detected (not on page load)
- Disable when mouse/keyboard input is detected (reclaim interaction)
**Open question**: Should gamepad support be a global utility hook or scoped to the game detail page only?
### 5.3 PWA Technical Boundaries
- Service worker must NOT cache authenticated pages (profile, manage) — these require live data
- Service worker must NOT cache POST/PATCH/DELETE API responses
- The `manifest.ts` must be updated with proper icon sizes (192px maskable, 512px)
- A `viewport` meta tag must be set in `layout.tsx`: `content="width=device-width, initial-scale=1, viewport-fit=cover"`
---
## 6. Security & Data Integrity Constraints
### 6.1 Steam API Keys
- The Steam Web API key (for reviews) is called server-side only in `syncSteamGame`. No client-side exposure risk.
- The SteamGridDB API key is also server-side only. No change needed.
### 6.2 Rate Limiting
- `syncSteamGame` already has 15s timeout on Steam API calls
- Batch sync in `/manage` has 1.5s inter-request delay
- The background sync in `games/page.tsx` also has 1.5s delay
- **Constraint**: The stub-then-sync pattern must not introduce new burst traffic. Single-game insert is inherently rate-limit-safe (one request per user action).
### 6.3 Database Integrity
- `steamAppId` has a UNIQUE constraint — prevents duplicate stubs
- `syncSteamGame` uses `db.update().where(eq(games.steamAppId, ...))` — safe for re-entry
- **Risk**: If two requests race to create a stub for the same appId, one will fail on UNIQUE constraint. The loser must gracefully return the existing game.
---
## 7. Scope Boundaries
### In Scope
1. Unify initial insert to use `syncSteamGame` pipeline (2 code locations)
2. Remove "Updates" from `lib/routes.ts`
3. PWA architecture constraints and implementation boundaries (spec only — implementation is a separate planning cycle)
### Out of Scope
- Refactoring `syncSteamGame` internals (already correct)
- Removing `/app/updates/` directory (cosmetic cleanup, deferred)
- Adding new Steam API fields beyond what `syncSteamGame` already fetches
- Offline support for `/manage` or authenticated routes
- Native mobile app packaging (PWA only)
---
## 8. Risk Register
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| Stub-then-sync pattern doubles API calls for new games | Certain | Low (2 calls instead of 1, 1.5KB payload each) | Acceptable trade-off for data integrity |
| Race condition: two simultaneous stub creations | Low | Low (one returns 409, can be caught) | UNIQUE constraint on `steamAppId` handles this |
| Service worker caches stale game data | Medium | Medium (user sees old reviews/prices) | Stale-while-revalidate strategy + short TTL for game pages |
| Gamepad API not available on all Steam Deck browser versions | Low | High (feature non-functional) | Feature-detect and silently degrade; SteamOS 3.5+ ships Chromium 114+ with Gamepad API support |
---
## 9. Dependencies
- **Sync unification**: No external dependencies. Pure refactor of existing code.
- **Nav removal**: No dependencies.
- **PWA**: Requires `next-pwa` or `@serwist/next` package, plus `workbox-webpack-plugin` configuration. These are new production dependencies that must be vetted for Next.js 15+ compatibility.