---
phase: quick
plan: 260420-vk0
type: execute
wave: 1
depends_on: []
files_modified:
- src/server/routes/images.ts
- src/client/routes/admin/items/$itemId.tsx
- src/server/routes/admin-tags.ts
- src/client/routes/admin/tags/index.tsx
- src/client/routes/admin/tags/$tagId.tsx
autonomous: true
requirements: []
must_haves:
truths:
- "Fetch-from-URL on admin item edit page shows the fetched image preview immediately"
- "Image cropping works on admin item edit page after fetch-from-URL"
- "Admin tag edit page renders at /admin/tags/$tagId"
- "Duplicate tag name returns user-friendly error (not 500)"
- "Inline tag creation form is visually polished and easy to use"
artifacts:
- path: "src/server/routes/images.ts"
provides: "from-url endpoint returns presignedUrl for immediate display"
- path: "src/client/routes/admin/items/$itemId.tsx"
provides: "Updates imageUrl in form state after fetch-from-URL"
- path: "src/server/routes/admin-tags.ts"
provides: "Catches unique constraint violations, returns 409"
- path: "src/client/routes/admin/tags/index.tsx"
provides: "Polished inline tag creation form"
key_links:
- from: "src/client/routes/admin/items/$itemId.tsx"
to: "/api/images/from-url"
via: "apiPost then updates form.imageUrl with returned presignedUrl"
pattern: "setForm.*imageUrl.*result\\.presignedUrl"
---
Fix 5 UAT issues across Phase 37 (admin item management) and Phase 38 (admin tag management).
Purpose: Close remaining UAT gaps so admin pages are fully functional.
Output: Working image fetch-from-URL with preview, working tag routing, proper error handling for duplicate tags, polished tag form.
@.planning/quick/260420-vk0-fix-uat-issues-image-fetch-from-url-imag/260420-vk0-PLAN.md
@CLAUDE.md
@src/server/routes/images.ts
@src/client/routes/admin/items/$itemId.tsx
@src/server/routes/admin-tags.ts
@src/client/routes/admin/tags/index.tsx
@src/client/routes/admin/tags/$tagId.tsx
@src/client/components/ImageUpload.tsx
From src/server/services/image.service.ts:
```typescript
interface FetchImageResult {
filename: string;
sourceUrl: string;
dominantColor: string | null;
}
export async function fetchImageFromUrl(url: string): Promise;
```
From src/server/services/storage.service.ts:
```typescript
export async function getImageUrl(filename: string): Promise;
```
From src/client/components/ImageUpload.tsx:
```typescript
interface ImageUploadProps {
value: string | null;
imageUrl?: string | null;
dominantColor?: string | null;
initialCrop?: { zoom: number; x: number; y: number } | null;
onChange: (filename: string | null, dominantColor?: string | null) => void;
onCropChange?: (crop: { zoom: number; x: number; y: number }) => void;
}
```
Task 1: Fix image fetch-from-URL preview and croppingsrc/server/routes/images.ts, src/client/routes/admin/items/$itemId.tsx
**Server side** (`src/server/routes/images.ts`):
After `fetchImageFromUrl(url)` returns successfully on line 21, call `getImageUrl(result.filename)` to generate a presigned URL. Return it alongside the existing fields:
```typescript
const result = await fetchImageFromUrl(url);
const presignedUrl = await getImageUrl(result.filename);
return c.json({ ...result, presignedUrl }, 201);
```
Import `getImageUrl` from `"../services/storage.service"`.
**Client side** (`src/client/routes/admin/items/$itemId.tsx`):
In `handleFetchFromUrl` (line 159), update the type annotation to include `presignedUrl: string` and `dominantColor: string | null` in the response type. After a successful fetch, also update `imageUrl` and `dominantColor` in form state:
```typescript
const result = await apiPost<{ filename: string; sourceUrl: string; presignedUrl: string; dominantColor: string | null }>(
"/api/images/from-url",
{ url: fetchUrl.trim() },
);
setForm((prev) => ({
...prev,
imageFilename: result.filename,
imageUrl: result.presignedUrl,
dominantColor: result.dominantColor ?? "",
imageSourceUrl: fetchUrl.trim(),
}));
```
This ensures the `ImageUpload` component receives the presigned URL via `imageUrl` prop and displays the fetched image immediately. With the image displaying, the crop button becomes visible and functional (cropping already works via `ImageCropEditor` — the issue was just that no image was shown to crop).
cd /home/jlmak/Projects/jlmak/GearBox && bun run lintAfter fetching an image from URL on the admin item edit page, the image preview displays immediately and the crop button is visible/functional.Task 2: Fix tag edit page routingsrc/client/routes/admin/tags/$tagId.tsx
The tag edit route file exists at the correct path and the route tree correctly registers it. Investigate why it renders the list instead of the edit form:
1. First, regenerate the route tree: run `bunx tsr generate` (TanStack Router CLI) to ensure the route tree is fresh.
2. If that doesn't resolve it, check if the `createFileRoute` path string in `$tagId.tsx` matches exactly what TanStack Router expects. Currently it's `createFileRoute("/admin/tags/$tagId")` — verify this matches the route tree's registered path pattern. If the route tree expects a different format (e.g., relative path), update accordingly.
3. If the route tree is correct but the page still shows the list, the issue may be that TanStack Router is matching the index route instead of the param route. The fix (same as was done for items in commit 31a9e3c) may involve ensuring the route file path conventions match TanStack Router's expectations for the directory-based approach. Check if items has any difference in file structure or route declaration that makes it work while tags doesn't.
4. After investigation, ensure navigating to `/admin/tags/4` renders `AdminTagEditPage` (not the tag list).
cd /home/jlmak/Projects/jlmak/GearBox && bunx tsr generate && bun run lintNavigating to /admin/tags/$tagId renders the tag edit form, not the tag list page.Task 3: Handle duplicate tag name error + polish inline formsrc/server/routes/admin-tags.ts, src/client/routes/admin/tags/index.tsx
**Backend** (`src/server/routes/admin-tags.ts`):
Wrap the `createTag` call (line 48) in a try/catch that detects SQLite unique constraint violations and returns a 409:
```typescript
app.post("/", zValidator("json", createTagSchema), async (c) => {
const db = c.get("db");
const data = c.req.valid("json");
try {
const tag = await createTag(db, data);
return c.json(tag, 201);
} catch (err) {
if (err instanceof Error && (err.message.includes("UNIQUE constraint failed") || err.message.includes("unique constraint"))) {
return c.json({ error: `A tag named "${data.name}" already exists` }, 409);
}
throw err;
}
});
```
**Frontend** (`src/client/routes/admin/tags/index.tsx`):
1. In `handleCreate`, improve error handling to show the server's error message:
```typescript
async function handleCreate(e: React.FormEvent) {
e.preventDefault();
if (!newName.trim()) return;
setCreateError(null);
try {
await createMutation.mutateAsync({
name: newName.trim(),
parentId: newParentId,
});
setNewName("");
setNewParentId(null);
} catch (err: any) {
const message = err?.response?.error || err?.message || "Failed to create tag";
setCreateError(message);
}
}
```
Note: Check how `apiPost` (or the mutation hook) propagates error response bodies. If the hook uses React Query's `mutateAsync`, the error may be thrown as-is from the API client. Look at how `useCreateAdminTag` is implemented and ensure the `error` field from the 409 JSON response is surfaced. If the api client throws a generic Error, you may need to parse the response body in the hook or catch block.
2. Polish the inline tag creation form. Replace the plain `flex items-center gap-3` row with a more visually consistent card-style form:
```tsx
{/* Quick-add form */}
Add Tag
{createError && (
{createError}
)}
```
This wraps the form in a card with labels, making it visually consistent with the rest of the admin UI and easier to navigate.
cd /home/jlmak/Projects/jlmak/GearBox && bun run lintCreating a duplicate tag shows "A tag named X already exists" error. The inline form has labels, is wrapped in a card, and matches admin UI style.
1. `bun run lint` passes with no errors
2. Start dev server (`bun run dev`) and verify:
- Navigate to /admin/items/{id}, use "Fetch from URL" with a valid image URL — preview appears
- Click crop button on the fetched image — crop editor opens
- Navigate to /admin/tags/{id} — edit form renders (not the list)
- On /admin/tags, try creating a tag with a duplicate name — shows friendly error
- Inline form has card styling with labels
- All 5 UAT issues are resolved
- No lint errors introduced
- Image fetch-from-URL shows preview immediately
- Tag routing renders edit page at /admin/tags/$tagId
- Duplicate tag creation shows 409 error message (not 500)
- Tag form is visually polished with labels and card wrapper