Files
GearBox/.planning/phases/35-bug-fixes/35-01-PLAN.md

325 lines
14 KiB
Markdown

---
phase: 35-bug-fixes
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/client/routes/threads/$threadId/index.tsx
- src/client/hooks/useItems.ts
- src/client/routes/login.tsx
autonomous: true
requirements:
- FIX-01
- FIX-02
- FIX-04
must_haves:
truths:
- "Clicking Add Candidate on the thread page opens CatalogSearchOverlay in thread mode"
- "The AddCandidateModal component and addCandidateOpen state are deleted from the thread route file"
- "ItemWithCategory includes imageUrl, dominantColor, cropZoom, cropX, cropY, priceCurrency fields"
- "Navigating to /login immediately redirects to the server /login route with no intermediate UI"
artifacts:
- path: "src/client/routes/threads/$threadId/index.tsx"
provides: "Thread detail page — Add Candidate button calls openCatalogSearch('thread')"
contains: "openCatalogSearch"
- path: "src/client/hooks/useItems.ts"
provides: "ItemWithCategory interface with image fields"
contains: "imageUrl: string | null"
- path: "src/client/routes/login.tsx"
provides: "Auto-redirect login page"
contains: "window.location.href = \"/login\""
key_links:
- from: "thread detail toolbar button"
to: "useUIStore.openCatalogSearch('thread')"
via: "onClick handler"
pattern: "openCatalogSearch\\(\"thread\"\\)"
- from: "LoginPage useEffect"
to: "window.location.href = \"/login\""
via: "useEffect with empty deps"
pattern: "useEffect.*window\\.location\\.href"
---
<objective>
Three self-contained type/wiring fixes that resolve wrong-modal, missing-image, and login-redirect bugs from the v2.3 backlog.
Purpose: Clear the modal confusion on thread pages (FIX-01), surface item images that the server already returns but the TypeScript type hides (FIX-02), and skip the redundant intermediate login UI (FIX-04).
Output: Updated thread route, useItems hook, and login route.
</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/phases/35-bug-fixes/35-CONTEXT.md
@.planning/phases/35-bug-fixes/35-UI-SPEC.md
</context>
<interfaces>
<!-- Key contracts the executor needs. Extracted from codebase. -->
From src/client/stores/uiStore.ts:
```typescript
// Catalog search actions
openCatalogSearch: (mode: "collection" | "thread") => void;
closeCatalogSearch: () => void;
catalogSearchOpen: boolean;
catalogSearchMode: "collection" | "thread" | null;
// Session thread tracking (used by CatalogSearchOverlay to scope to a thread)
catalogSessionThreadId: number | null;
setCatalogSessionThreadId: (id: number | null) => void;
```
From src/client/routes/threads/$threadId/index.tsx (current state):
- Line 44: `const [addCandidateOpen, setAddCandidateOpen] = useState(false);`
- Line 144: `onClick={() => setAddCandidateOpen(true)}` — this is the broken Add Candidate button
- Lines 307-313: `{addCandidateOpen && <AddCandidateModal ... />}` — the modal to remove
- Lines 317-639: Full `AddCandidateModal` component and its interfaces/constants — all to delete
From src/client/hooks/useItems.ts (current state):
- `ItemWithCategory` interface (lines 27-43) is missing these fields the server already returns:
- `imageUrl: string | null`
- `dominantColor: string | null`
- `cropZoom: number | null`
- `cropX: number | null`
- `cropY: number | null`
- `priceCurrency: string | null`
From src/client/routes/login.tsx (current state):
- Renders full card UI with a sign-in button that calls `window.location.href = "/login"`
- Has `useAuth` hook check and a `useNavigate` for already-authenticated users
- Both the auth check and full UI need to be removed — replace with immediate useEffect redirect
</interfaces>
<tasks>
<task type="auto">
<name>Task 1: Wire Add Candidate button and delete AddCandidateModal (FIX-01)</name>
<files>src/client/routes/threads/$threadId/index.tsx</files>
<read_first>
- src/client/routes/threads/$threadId/index.tsx (read the full file — understand current modal state, imports, and FAB wiring pattern)
- src/client/stores/uiStore.ts (confirm openCatalogSearch and setCatalogSessionThreadId signatures)
</read_first>
<action>
Make two changes to src/client/routes/threads/$threadId/index.tsx:
**1. Wire the toolbar button (per D-01, D-03):**
Replace the `openCatalogSearch` and `setCatalogSessionThreadId` Zustand selectors in the component — add these two lines to the existing `useUIStore` selectors at the top of `ThreadDetailPage`:
```typescript
const openCatalogSearch = useUIStore((s) => s.openCatalogSearch);
const setCatalogSessionThreadId = useUIStore((s) => s.setCatalogSessionThreadId);
```
Delete the `addCandidateOpen` state (line 44):
```typescript
// DELETE THIS LINE:
const [addCandidateOpen, setAddCandidateOpen] = useState(false);
```
Change the toolbar button's onClick from `() => setAddCandidateOpen(true)` to:
```typescript
onClick={() => {
setCatalogSessionThreadId(threadId);
openCatalogSearch("thread");
}}
```
Remove the cursor-default: the button already has class string — ensure `cursor-pointer` is present (the button has no explicit cursor class currently, so browsers default to pointer for `<button>` — leave as-is, no change needed here).
**2. Delete all dead code (per D-02):**
Remove from the JSX:
```tsx
{addCandidateOpen && (
<AddCandidateModal
threadId={threadId}
onClose={() => setAddCandidateOpen(false)}
/>
)}
```
Delete the entire block from line ~317 to end of file:
- `interface AddCandidateModalProps { ... }`
- `interface ModalFormData { ... }`
- `const INITIAL_MODAL_FORM: ModalFormData = { ... }`
- `function AddCandidateModal({ ... }) { ... }` (the entire function, ~300 lines)
Remove any imports that were only used by `AddCandidateModal` and are no longer needed:
- `useCreateCandidate` from `../../../hooks/useCandidates` — check if used elsewhere in the file; if only in `AddCandidateModal`, remove it
- `useCurrency` from `../../../hooks/useCurrency` — check if used elsewhere; if only in modal, remove it
- `ImageUpload` from `../../../components/ImageUpload` — check if used elsewhere; if only in modal, remove it
Keep all other imports (`CategoryPicker`, `ComparisonTable`, etc.) since they are used in the main page body.
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun run lint 2>&1 | grep -E "threads/\\\$threadId|error" | head -20</automated>
</verify>
<acceptance_criteria>
- `grep -n "addCandidateOpen" src/client/routes/threads/\$threadId/index.tsx` returns no matches
- `grep -n "AddCandidateModal" src/client/routes/threads/\$threadId/index.tsx` returns no matches
- `grep -n "openCatalogSearch" src/client/routes/threads/\$threadId/index.tsx` shows at least one match
- `grep -n "setCatalogSessionThreadId" src/client/routes/threads/\$threadId/index.tsx` shows at least one match
- `bun run lint` passes with no errors on the modified file
</acceptance_criteria>
<done>Thread detail page Add Candidate button calls openCatalogSearch("thread") with the current threadId set as catalogSessionThreadId. The AddCandidateModal and all associated dead code (interfaces, constants, component function) are deleted.</done>
</task>
<task type="auto">
<name>Task 2: Extend ItemWithCategory interface with image fields (FIX-02)</name>
<files>src/client/hooks/useItems.ts</files>
<read_first>
- src/client/hooks/useItems.ts (read fully — see current ItemWithCategory interface at lines 27-43)
</read_first>
<action>
Add the six missing fields to the `ItemWithCategory` interface in `src/client/hooks/useItems.ts` (per D-04).
Current interface ends at line 43. Add these fields before the closing `}`:
```typescript
imageUrl: string | null;
dominantColor: string | null;
cropZoom: number | null;
cropX: number | null;
cropY: number | null;
priceCurrency: string | null;
```
The updated `ItemWithCategory` interface should be:
```typescript
interface ItemWithCategory {
id: number;
name: string;
weightGrams: number | null;
priceCents: number | null;
quantity: number;
categoryId: number;
notes: string | null;
productUrl: string | null;
imageFilename: string | null;
globalItemId: number | null;
brand: string | null;
createdAt: string;
updatedAt: string;
categoryName: string;
categoryIcon: string;
imageUrl: string | null;
dominantColor: string | null;
cropZoom: number | null;
cropX: number | null;
cropY: number | null;
priceCurrency: string | null;
}
```
No server-side changes needed (per D-05) — GET /api/items already returns these fields via withImageUrls().
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun run lint 2>&1 | grep -E "useItems|error" | head -10</automated>
</verify>
<acceptance_criteria>
- `grep -n "imageUrl: string | null" src/client/hooks/useItems.ts` returns a match inside `ItemWithCategory`
- `grep -n "dominantColor: string | null" src/client/hooks/useItems.ts` returns a match
- `grep -n "cropZoom: number | null" src/client/hooks/useItems.ts` returns a match
- `grep -n "cropX: number | null" src/client/hooks/useItems.ts` returns a match
- `grep -n "cropY: number | null" src/client/hooks/useItems.ts` returns a match
- `grep -n "priceCurrency: string | null" src/client/hooks/useItems.ts` returns a match
- `bun run lint` passes with no errors on the modified file
</acceptance_criteria>
<done>ItemWithCategory includes all six image and currency fields. TypeScript no longer reports missing properties when collection overview cards pass imageUrl/dominantColor/crop values to ItemCard.</done>
</task>
<task type="auto">
<name>Task 3: Replace login page UI with immediate useEffect redirect (FIX-04)</name>
<files>src/client/routes/login.tsx</files>
<read_first>
- src/client/routes/login.tsx (read fully — understand current imports, auth check, and full card UI)
</read_first>
<action>
Replace the entire content of `src/client/routes/login.tsx` with the following (per D-09, UI-SPEC Auth Redirect Contract):
```typescript
import { createFileRoute } from "@tanstack/react-router";
import { useEffect } from "react";
export const Route = createFileRoute("/login")({
component: LoginPage,
});
function LoginPage() {
useEffect(() => {
window.location.href = "/login";
}, []);
return (
<div className="flex items-center justify-center h-screen">
<p className="text-sm text-gray-500">Signing in...</p>
</div>
);
}
```
Remove all now-unused imports: `useNavigate` from `@tanstack/react-router`, `useTranslation` from `react-i18next`, `useAuth` from `../hooks/useAuth`.
The `/login` server route handles the Logto OIDC redirect. If the user is already authenticated, the server redirects back to `/`. No client-side auth check is needed (per D-09).
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun run lint 2>&1 | grep -E "login|error" | head -10</automated>
</verify>
<acceptance_criteria>
- `grep -n "window.location.href" src/client/routes/login.tsx` returns exactly one match inside `useEffect`
- `grep -n "useAuth" src/client/routes/login.tsx` returns no matches
- `grep -n "useNavigate" src/client/routes/login.tsx` returns no matches
- `grep -n "useTranslation" src/client/routes/login.tsx` returns no matches
- `grep -n "SignIn\|signInToGearBox\|redirectDescription" src/client/routes/login.tsx` returns no matches (full UI removed)
- File line count is under 25 lines: `wc -l src/client/routes/login.tsx` outputs a number ≤ 25
- `bun run lint` passes with no errors
</acceptance_criteria>
<done>LoginPage renders only a minimal "Signing in..." indicator and immediately redirects via useEffect to the server /login route. No intermediate card UI, no auth check, no translation keys.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| client→server /login | Browser navigates to server-controlled route; server issues Logto OIDC redirect |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-35-01 | Spoofing | login route redirect | accept | Server /login route is Hono-controlled; client just triggers the navigation. No sensitive data exposed client-side. |
| T-35-02 | Information Disclosure | ItemWithCategory type | accept | Type extension only exposes fields already returned by the API to authenticated users. No new data surface. |
</threat_model>
<verification>
After all three tasks complete:
1. Navigate to a thread detail page — clicking "Add Candidate" must open CatalogSearchOverlay (not the old modal form)
2. Confirm no AddCandidateModal UI appears anywhere on thread pages
3. Collection overview cards with images must display images (imageUrl field now typed correctly)
4. Navigate to /login (client-side) — page must immediately redirect to Logto, showing only the brief "Signing in..." text
Run: `bun run lint` — zero errors
Run: `bun test` — all existing tests pass
</verification>
<success_criteria>
- Add Candidate toolbar button on thread page opens CatalogSearchOverlay in thread mode
- AddCandidateModal component is fully deleted (no dead code remaining)
- ItemWithCategory has imageUrl, dominantColor, cropZoom, cropX, cropY, priceCurrency fields
- LoginPage is ≤ 25 lines, redirects immediately via useEffect, renders no form UI
- bun run lint passes with zero errors
</success_criteria>
<output>
After completion, create `.planning/phases/35-bug-fixes/35-01-SUMMARY.md`
</output>