325 lines
14 KiB
Markdown
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>
|