docs(32): create phase plans for setup sharing system

4 plans in 3 waves:
- Wave 1: Schema migration (isPublic→visibility) + shares table
- Wave 2: Share link service + API routes
- Wave 3: Share modal UI + shared setup viewer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 17:05:36 +02:00
parent 9965e356de
commit 81a654085d
5 changed files with 1212 additions and 2 deletions

View File

@@ -0,0 +1,231 @@
---
phase: 32-setup-sharing-system
plan: 04
type: execute
wave: 3
depends_on: [01, 02]
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>