---
phase: 32-setup-sharing-system
plan: 04
type: execute
wave: 4
depends_on: [01, 02, 03]
files_modified:
  - src/client/routes/setups/$setupId.tsx
  - src/client/hooks/useSetups.ts
autonomous: true
requirements:
  - TBD

must_haves:
  truths:
    - "Anonymous user visiting /setups/:id?share=token sees the shared setup with items"
    - "Shared setup viewer shows a 'Shared setup' banner at the top"
    - "Invalid or expired share tokens show an error message"
    - "Short URL /s/:token redirects to /setups/:id?share=token"
    - "Shared viewer is read-only — no edit buttons, no share button, no delete button"
  artifacts:
    - path: "src/client/routes/setups/$setupId.tsx"
      provides: "Enhanced setup detail page with share token detection and shared view mode"
    - path: "src/client/hooks/useSetups.ts"
      provides: "useSharedSetup hook for fetching shared setup data"
      exports: ["useSharedSetup"]
  key_links:
    - from: "src/client/routes/setups/$setupId.tsx"
      to: "src/client/hooks/useSetups.ts"
      via: "useSharedSetup hook for share token access"
      pattern: "useSharedSetup"
    - from: "src/client/routes/setups/$setupId.tsx"
      to: "/api/shared/:token"
      via: "API fetch for shared setup data"
      pattern: "api/shared"
---

<objective>
Add shared setup viewer functionality to the existing setup detail page — detect share token in URL, fetch via shared endpoint, and display read-only view with shared banner.

Purpose: This completes the user-facing share flow (D-06, D-17). When someone receives a share link, they can view the setup without authentication.

Output: Updated setup detail page with share token detection and shared viewing mode.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/32-setup-sharing-system/32-CONTEXT.md
@.planning/phases/32-setup-sharing-system/32-UI-SPEC.md
@.planning/phases/32-setup-sharing-system/32-01-SUMMARY.md
@.planning/phases/32-setup-sharing-system/32-02-SUMMARY.md

<interfaces>
<!-- Key types and contracts from Plans 01 and 02. -->

Shared access API endpoint (from Plan 02):
```
GET /api/shared/:token → Setup object with items array (same format as public view)
Returns 404 for invalid/expired/revoked tokens
```

Short URL redirect (from Plan 02):
```
GET /s/:token → 302 redirect to /setups/:setupId?share=:token
```

From src/client/routes/setups/$setupId.tsx (current structure):
```typescript
// Three-way data source: private (auth), public (no auth), shared (token)
const { data: auth } = useAuth();
const isAuthenticated = !!auth?.user;
const privateSetup = useSetup(isAuthenticated ? numericId : null);
const publicSetup = usePublicSetup(!isAuthenticated ? numericId : null);
```

From @tanstack/react-router:
```typescript
// URL search params access
const search = Route.useSearch(); // needs searchSchema defined on route
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add useSharedSetup hook and share token detection to setup detail page</name>
  <files>src/client/hooks/useSetups.ts, src/client/routes/setups/$setupId.tsx</files>
  <read_first>src/client/hooks/useSetups.ts, src/client/routes/setups/$setupId.tsx, src/client/lib/api.ts</read_first>
  <action>
**Add `useSharedSetup` hook to `src/client/hooks/useSetups.ts`:**

```typescript
export function useSharedSetup(token: string | null) {
  return useQuery({
    queryKey: ["shared-setup", token],
    queryFn: () => apiGet<SetupWithItems>(`/api/shared/${token}`),
    enabled: !!token,
    retry: false, // Don't retry on 404
  });
}
```

Use the same `SetupWithItems` type used by `useSetup` and `usePublicSetup`.

**Update `src/client/routes/setups/$setupId.tsx`:**

1. Add search params validation to the route definition to capture the `share` query param:
```typescript
import { z } from "zod";

export const Route = createFileRoute("/setups/$setupId")({
  component: SetupDetailPage,
  validateSearch: z.object({
    share: z.string().optional(),
  }),
});
```

2. In `SetupDetailPage`, detect the share token:
```typescript
const { share: shareToken } = Route.useSearch();
```

3. Update the three-way data source logic:
```typescript
const { data: auth } = useAuth();
const isAuthenticated = !!auth?.user;

// Priority: share token > authenticated owner > public viewer
const sharedSetup = useSharedSetup(shareToken ?? null);
const privateSetup = useSetup(!shareToken && isAuthenticated ? numericId : null);
const publicSetup = usePublicSetup(!shareToken && !isAuthenticated ? numericId : null);

const isSharedView = !!shareToken;
const { data: setup, isLoading, isError } = isSharedView
  ? sharedSetup
  : isAuthenticated
    ? privateSetup
    : publicSetup;
```

4. Add shared banner (per 32-UI-SPEC.md) — render above the header bar when `isSharedView`:
```tsx
{isSharedView && setup && (
  <div className="flex items-center gap-2 px-4 py-2 bg-blue-50 border-b border-blue-100">
    <LucideIcon name="link" size={16} className="text-blue-500" />
    <span className="text-sm text-blue-700">Shared setup</span>
  </div>
)}
```

5. Add error state for invalid/expired share tokens:
```tsx
{isSharedView && isError && (
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 text-center">
    <LucideIcon name="link" size={48} className="text-gray-300 mx-auto mb-4" />
    <h2 className="text-xl font-semibold text-gray-900 mb-2">Link not available</h2>
    <p className="text-sm text-gray-500">This share link has expired or is no longer valid.</p>
  </div>
)}
```

6. Hide owner-only controls when in shared view — conditionally hide these elements when `isSharedView` is true:
   - Add Items button (both desktop and mobile variants)
   - Share button (both desktop and mobile variants)
   - Delete Setup button (both desktop and mobile variants)
   - Classification dropdowns on items
   - Remove item buttons
   
   Wrap each with: `{!isSharedView && isAuthenticated && ( ... )}`

7. The shared view shows the same read-only content as the public view: item list grouped by category, weight summary card, setup name header.
  </action>
  <verify>
    <automated>grep -q "useSharedSetup" src/client/hooks/useSetups.ts && grep -q "shareToken\|share:" src/client/routes/setups/\$setupId.tsx && grep -q "Shared setup" src/client/routes/setups/\$setupId.tsx && bun run lint && echo "PASS" || echo "FAIL"</automated>
  </verify>
  <acceptance_criteria>
    - `src/client/hooks/useSetups.ts` exports `useSharedSetup(token)` that fetches `/api/shared/:token`
    - `src/client/routes/setups/$setupId.tsx` validates `share` search param via Zod
    - When `?share=token` is present, setup data is fetched via shared endpoint (not owner or public)
    - Shared banner (`Shared setup` with link icon in blue-50) appears at top of page when share token present
    - Invalid/expired token shows error state with "Link not available" message
    - Owner-only controls (add items, share, delete, classification, remove item) are hidden in shared view
    - `bun run lint` passes
  </acceptance_criteria>
  <done>Shared setup viewer with token detection, shared banner, error handling, and read-only mode</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| URL search params | Share token from URL — untrusted user input |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-32-09 | Spoofing | share token in URL | mitigate | Token validated server-side by /api/shared/:token — client only passes through, no client-side authorization decisions |
| T-32-10 | Information Disclosure | shared view content | accept | Shared setup data is intentionally visible to anyone with the token — this is the feature |
</threat_model>

<verification>
1. `bun run lint` passes
2. Visit `/setups/1?share=valid-token` — shows setup with shared banner, no edit controls
3. Visit `/setups/1?share=invalid-token` — shows error state
4. Visit `/s/valid-token` — redirects to `/setups/:id?share=token`, displays shared view
5. Owner visiting their own setup normally (no share param) — sees all controls as before
</verification>

<success_criteria>
- Share links use `/s/{token}` short URL AND `/setups/:id?share={token}` (per D-06)
- Shared setup viewer works for anonymous users (per D-17)
- No owner-only actions visible in shared view
- No changes to discovery feed or profile page (per D-18)
</success_criteria>

<output>
After completion, create `.planning/phases/32-setup-sharing-system/32-04-SUMMARY.md`
</output>
