docs: architectural spec for sync unification, nav cleanup, and PWA

This commit is contained in:
2026-05-05 01:10:27 +08:00
parent 863358a8fd
commit 765adfa1d5
@@ -0,0 +1,259 @@
# 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.