- CatalogSearchOverlay: replace handleAddStub with real openAddToCollection/openAddToThread routing based on catalogSearchMode - ConfirmDialog + __root.tsx: swap t() for Trans component on deleteItemMessage, deleteCandidateMessage, pickWinnerMessage — fixes <bold> rendering as literal text - Biome format pass: fix 23 lint/format errors across scripts, services, tests - Planning: mark all UAT and verification gaps resolved for phases 07, 11, 16, 20, 21, 22, 24, 32, 34; close debug sessions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
14 KiB
phase, verified, status, score, re_verification, gaps, human_verification
| phase | verified | status | score | re_verification | gaps | human_verification | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 24-public-access-infrastructure | 2026-04-10T12:00:00Z | complete | 5/6 must-haves verified | false |
|
|
Phase 24: Public Access Infrastructure Verification Report
Phase Goal: Anyone can browse the catalog, public setups, and user profiles without logging in Verified: 2026-04-10T12:00:00Z Status: gaps_found Re-verification: No — initial verification
Goal Achievement
Observable Truths
| # | Truth | Status | Evidence |
|---|---|---|---|
| 1 | Public GET endpoints return 429 after exceeding the configured rate limit | VERIFIED | createRateLimit factory confirmed in rateLimit.ts:23; 11 tests pass |
| 2 | Different endpoint tiers have different rate limit thresholds | VERIFIED | browseTier(120, 60_000) and detailTier(60, 60_000) confirmed in index.ts:122-123 |
| 3 | Existing OAuth rate limiting (5 req/15 min) continues to work unchanged | VERIFIED | rateLimit = createRateLimit(5, 15 * 60 * 1000) at rateLimit.ts:44; backward-compat tests pass |
| 4 | Anonymous visitor sees app content immediately on any public route — no spinner, no redirect | VERIFIED | authLoading spinner block removed from __root.tsx; soft navigate() guard fires only after auth resolves and !authLoading |
| 5 | Anonymous visitor can browse the global item catalog and open catalog detail pages | VERIFIED | isPublicRoute includes pathname.startsWith("/global-items"); auth middleware skips GET /api/global-items; no auth required |
| 6 | Anonymous visitor can view a public setup with its items and totals | PARTIAL | Setup items and totals render correctly. Item images absent for anonymous viewers — getPublicSetupWithItems does not call withImageUrls; item.imageUrl is undefined (TS2339 at $setupId.tsx:284) |
| 7 | Anonymous visitor can view a user profile page | VERIFIED | isPublicRoute includes /users/; auth skips GET /api/users/:id/profile; getPublicProfile queries users + setups from DB |
| 8 | Anonymous visitor clicking 'Add to Collection' or 'Add to Thread' sees a sign-in/sign-up prompt | VERIFIED | openAuthPrompt() called before openAddToCollection/openAddToThread in $globalItemId.tsx:141-158; AuthPromptModal rendered globally in __root.tsx |
| 9 | Authenticated user experience is unchanged — all write actions work as before | VERIFIED | isAuthenticated guards all new branches; mutation hooks retained; private useSetup path unchanged |
Score: 8/9 truths verified (1 partial)
Required Artifacts
| Artifact | Expected | Status | Details |
|---|---|---|---|
src/server/middleware/rateLimit.ts |
createRateLimit factory function | VERIFIED | exports createRateLimit, rateLimit, _resetForTesting; 49 lines, substantive |
src/server/index.ts |
Rate limit middleware applied to public GET endpoints | VERIFIED | browseTier and detailTier instantiated at lines 122-123; applied to 5 endpoint groups before auth middleware at line 151 |
tests/middleware/rateLimit.test.ts |
Tests for configurable rate limit tiers | VERIFIED | 181 lines; two describe blocks; createRateLimit factory with 5 tests; rateLimit backward compat with 6 tests; all 11 pass |
src/client/routes/__root.tsx |
Render-first root layout with expanded isPublicRoute | VERIFIED | No authLoading spinner; isPublicRoute includes /global-items and /setups/; AuthPromptModal rendered |
src/client/stores/uiStore.ts |
showAuthPrompt state for auth modal | VERIFIED | showAuthPrompt, openAuthPrompt, closeAuthPrompt in interface and implementation |
src/client/components/AuthPromptModal.tsx |
Modal prompting anonymous users to sign in or sign up | VERIFIED | Contains "sign in or sign up"; fixed overlay z-50; backdrop dismiss; two /login links |
src/client/hooks/useSetups.ts |
usePublicSetup hook for anonymous setup viewing | VERIFIED | usePublicSetup exported at line 67; calls /api/setups/${setupId}/public; enabled guard; 404-aware retry |
src/client/routes/global-items/$globalItemId.tsx |
Auth-guarded write action buttons on catalog detail | VERIFIED | openAuthPrompt imported and called in both button handlers with !isAuthenticated check |
src/client/routes/setups/$setupId.tsx |
Conditional public vs private setup rendering | PARTIAL | usePublicSetup imported and used; conditional data source correct; but item.imageUrl does not exist on SetupItemWithCategory type |
Key Link Verification
| From | To | Via | Status | Details |
|---|---|---|---|---|
src/server/index.ts |
src/server/middleware/rateLimit.ts |
import createRateLimit | WIRED | Line 13: import { createRateLimit } from "./middleware/rateLimit.ts"; pattern createRateLimit(120, confirmed at line 122 |
src/client/routes/__root.tsx |
src/client/components/AuthPromptModal.tsx |
rendered in root layout | WIRED | Line 15: import; line 184: <AuthPromptModal /> in JSX |
src/client/routes/global-items/$globalItemId.tsx |
src/client/stores/uiStore.ts |
openAuthPrompt action | WIRED | Line 19: const openAuthPrompt = useUIStore((s) => s.openAuthPrompt); called at lines 142 and 154 |
src/client/routes/setups/$setupId.tsx |
src/client/hooks/useSetups.ts |
usePublicSetup hook | WIRED | Line 11: usePublicSetup imported; lines 33-36: conditional fetch logic using hook |
Data-Flow Trace (Level 4)
| Artifact | Data Variable | Source | Produces Real Data | Status |
|---|---|---|---|---|
$setupId.tsx |
setup |
usePublicSetup → GET /api/setups/:id/public → getPublicSetupWithItems |
DB queries: setups table (line 88) + setupItems JOIN items JOIN categories (line 95-132) |
FLOWING (items/totals); imageUrl STATIC (undefined — withImageUrls not called) |
$globalItemId.tsx |
item |
useGlobalItem → GET /api/global-items/:id |
DB query in globalItems service | FLOWING |
$userId.tsx |
profile |
usePublicProfile → GET /api/users/:id/profile → getPublicProfile |
DB queries: users table (line 37) + setups (line 49) with SQL aggregates | FLOWING |
Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|---|---|---|---|
| Rate limit tests pass | bun test tests/middleware/rateLimit.test.ts |
11 pass, 0 fail | PASS |
| Lint clean in src/ | bun run lint (src/ only) |
0 errors in src/ (4 pre-existing .obsidian/ format errors) | PASS |
| createRateLimit factory exported | grep pattern in rateLimit.ts | export function createRateLimit(maxAttempts: number, windowMs: number) at line 23 |
PASS |
| browseTier applied before auth | Line order check in index.ts | Rate limits lines 122-148, auth middleware line 151 | PASS |
| Public setup endpoint exists | setups.ts route check | app.get("/:id/public" at line 45; delegates to getPublicSetupWithItems |
PASS |
| imageUrl on public setup items | TS error check | TS2339 at $setupId.tsx:284 — item.imageUrl does not exist on SetupItemWithCategory | FAIL |
Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|---|---|---|---|---|
| PUBL-01 | 24-02 | Browse global item catalog without logging in | SATISFIED | isPublicRoute includes /global-items; auth middleware skips GET /api/global-items; catalog route accessible |
| PUBL-02 | 24-02 | View public setups without logging in | PARTIAL | Setup items and totals accessible via usePublicSetup; images missing for anon users (withImageUrls not called in public endpoint) |
| PUBL-03 | 24-02 | View user profiles without logging in | SATISFIED | isPublicRoute includes /users/; auth skips GET /api/users/:id/profile; getPublicProfile queries DB |
| PUBL-04 | 24-02 | No auth spinner or redirect on first visit | SATISFIED | authLoading spinner block removed; isPublicRoute expanded; soft navigate() fires only after authLoading resolves |
| PUBL-05 | 24-02 | Login required only for create/edit/delete | SATISFIED | Write actions in $globalItemId.tsx and $setupId.tsx guarded by isAuthenticated; unauthenticated users see AuthPromptModal |
| INFR-01 | 24-01 | Public API endpoints rate-limited | SATISFIED | createRateLimit factory; browseTier (120/min) and detailTier (60/min) applied to all 5 public GET endpoint groups |
All 6 requirements claimed by phase 24 plans are accounted for. No orphaned requirements found in REQUIREMENTS.md traceability table.
Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|---|---|---|---|---|
src/client/routes/setups/$setupId.tsx |
284 | imageUrl={item.imageUrl} — property does not exist on SetupItemWithCategory |
Warning | Item images silently absent for anonymous viewers of public setups. TypeScript error TS2339 confirms the type gap. No runtime crash but visual content gap. |
src/server/services/profile.service.ts |
87-135 | getPublicSetupWithItems returns items without presigned image URLs |
Warning | Companion to above — public endpoint does not enrich items with S3 presigned URLs. Private endpoint calls withImageUrls; public endpoint does not. |
No TODO/FIXME/placeholder comments found in phase-modified files. No empty implementations. No console.log-only handlers.
Human Verification Required
1. Public Setup Page Renders Read-Only
Test: Open an incognito browser window, navigate to a public setup URL (e.g., /setups/1 if a public setup exists). Verify setup name, item list with weights/costs, and total weight/cost render. Confirm no "Add Items", "Delete Setup", or "Public/Private" toggle buttons are visible.
Expected: Full read-only view of the setup. No write-action controls.
Why human: Visual confirmation of rendered content and conditional UI requires browser.
2. Auth Prompt Modal Interaction
Test: Open an incognito window, navigate to a catalog item detail page (/global-items/:id), click "Add to Collection". Verify the AuthPromptModal appears with "Join GearBox" heading and "sign in or sign up" text. Test backdrop click, Escape key, and "Sign in" / "Create account" button routes.
Expected: Modal appears on first click, dismisses on backdrop/Escape, both buttons navigate to /login.
Why human: Modal interaction, keyboard events, and navigation behavior require browser verification.
3. Render-First on Anonymous Visit
Test: Open an incognito window, navigate to /. Verify the app renders immediately without a spinner. Navigate to /global-items. Confirm catalog loads without redirect to /login.
Expected: Instant render, no spinner, no redirect.
Why human: Render timing and absence of auth redirect requires browser observation.
Gaps Summary
One functional gap was found that prevents full goal achievement for PUBL-02:
Image display in public setup views — When an anonymous user views a public setup at /setups/:id, item images will not display. The root cause is a missing withImageUrls call in getPublicSetupWithItems. The private getSetupWithItems in setup.service.ts calls withImageUrls to generate presigned S3 URLs and attaches them as imageUrl on each item. The public equivalent in profile.service.ts does not. The client code ($setupId.tsx:284) passes item.imageUrl to ItemCard, but SetupItemWithCategory has no such field — TypeScript confirms this with TS2339. The result is silent: no crash, items and totals render, but images are absent.
The fix requires either: (a) calling withImageUrls in getPublicSetupWithItems and returning the enriched items, or (b) removing the imageUrl={item.imageUrl} prop from the public render path.
This gap does not block the core browsing experience (items, names, weights, totals all work) but falls short of the full read-only parity the phase intended.
Verified: 2026-04-10T12:00:00Z Verifier: Claude (gsd-verifier)