diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index d48bc6f..63e9a3a 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -144,7 +144,7 @@ Plans: | 21. Item & Catalog Detail Pages | v2.0 | 3/3 | Complete | 2026-04-06 | | 22. Add-from-Catalog & Thread Integration | v2.0 | 2/2 | Complete | 2026-04-06 | | 23. Manual Entry Fallback | v2.0 | 1/1 | Complete | 2026-04-06 | -| 24. Public Access & Infrastructure | v2.1 | 2/2 | Complete | 2026-04-10 | +| 24. Public Access & Infrastructure | v2.1 | 2/2 | Complete | 2026-04-10 | | 25. Catalog Enrichment & Agent Tools | v2.1 | 0/TBD | Not started | - | | 26. Discovery Landing Page | v2.1 | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 8da6b43..f6533f8 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,7 +4,7 @@ milestone: v2.1 milestone_name: Public Discovery status: verifying stopped_at: Completed 24-02-PLAN.md -last_updated: "2026-04-10T08:11:06.987Z" +last_updated: "2026-04-10T08:17:36.904Z" last_activity: 2026-04-10 progress: total_phases: 6 @@ -25,8 +25,8 @@ See: .planning/PROJECT.md (updated 2026-04-09) ## Current Position -Phase: 24 (public-access-infrastructure) — EXECUTING -Plan: 2 of 2 +Phase: 999.1 +Plan: Not started Status: Phase complete — ready for verification Last activity: 2026-04-10 diff --git a/.planning/phases/24-public-access-infrastructure/24-VERIFICATION.md b/.planning/phases/24-public-access-infrastructure/24-VERIFICATION.md new file mode 100644 index 0000000..cf3dc24 --- /dev/null +++ b/.planning/phases/24-public-access-infrastructure/24-VERIFICATION.md @@ -0,0 +1,153 @@ +--- +phase: 24-public-access-infrastructure +verified: 2026-04-10T12:00:00Z +status: gaps_found +score: 5/6 must-haves verified +re_verification: false +gaps: + - truth: "Anonymous visitor can view a public setup with its items and totals" + status: partial + reason: "Setup items display correctly but item images are missing for anonymous viewers. getPublicSetupWithItems does not call withImageUrls, so no presigned S3 URLs are generated. The $setupId.tsx component passes item.imageUrl (undefined) to ItemCard — confirmed TS2339 type error at line 284." + artifacts: + - path: "src/server/services/profile.service.ts" + issue: "getPublicSetupWithItems (line 87) does not call withImageUrls on the returned item list, unlike the private getSetupWithItems in setup.service.ts" + - path: "src/client/routes/setups/$setupId.tsx" + issue: "Line 284: item.imageUrl is passed to ItemCard but SetupItemWithCategory only defines imageFilename. TypeScript error TS2339 confirms property does not exist on the type. Images silently not displayed for anonymous users." + missing: + - "Call withImageUrls on items in getPublicSetupWithItems, or add imageUrl to the service return type by enriching from storage service" + - "Remove item.imageUrl reference from $setupId.tsx ItemCard props (or add imageUrl to SetupItemWithCategory after enrichment)" +human_verification: + - test: "Anonymous visitor can view a public setup page" + expected: "Setup renders with item list, weight totals, and cost totals. No Add Items / Delete / Public toggle buttons visible. Back arrow link works." + why_human: "Visual confirmation of rendered output and write-action absence requires browser" + - test: "Auth prompt modal behavior on catalog detail page" + expected: "Clicking 'Add to Collection' or 'Add to Thread' shows the modal. Backdrop click closes it. Escape key closes it. Both buttons route to /login." + why_human: "Modal interaction and keyboard events require browser verification" + - test: "No auth spinner or redirect on first anonymous visit" + expected: "App renders immediately at / and /global-items without redirect to /login or loading spinner" + why_human: "Render timing and redirect behavior requires browser verification in an incognito session" +--- + +# 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: `` 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)_