docs(quick-260420-vk0): Fix UAT issues: image fetch-from-URL, image cropping, tag routing, duplicate tag error, tag form UX
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ milestone_name: Admin Foundation
|
||||
status: executing
|
||||
stopped_at: Completed 38-02-PLAN.md — admin tag management client UI
|
||||
last_updated: "2026-04-19T20:32:22Z"
|
||||
last_activity: 2026-04-19
|
||||
last_activity: 2026-04-20
|
||||
progress:
|
||||
total_phases: 20
|
||||
completed_phases: 10
|
||||
@@ -96,6 +96,12 @@ Resolved in 35-02:
|
||||
|
||||
None.
|
||||
|
||||
### Quick Tasks Completed
|
||||
|
||||
| # | Description | Date | Commit | Directory |
|
||||
|---|-------------|------|--------|-----------|
|
||||
| 260420-vk0 | Fix UAT issues: image fetch-from-URL, image cropping, tag routing, duplicate tag error, tag form UX | 2026-04-20 | ddf9b95 | [260420-vk0-fix-uat-issues-image-fetch-from-url-imag](./quick/260420-vk0-fix-uat-issues-image-fetch-from-url-imag/) |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
Items carried forward from v2.3:
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
---
|
||||
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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@.planning/quick/260420-vk0-fix-uat-issues-image-fetch-from-url-imag/260420-vk0-PLAN.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@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
|
||||
|
||||
<interfaces>
|
||||
From src/server/services/image.service.ts:
|
||||
```typescript
|
||||
interface FetchImageResult {
|
||||
filename: string;
|
||||
sourceUrl: string;
|
||||
dominantColor: string | null;
|
||||
}
|
||||
export async function fetchImageFromUrl(url: string): Promise<FetchImageResult>;
|
||||
```
|
||||
|
||||
From src/server/services/storage.service.ts:
|
||||
```typescript
|
||||
export async function getImageUrl(filename: string): Promise<string>;
|
||||
```
|
||||
|
||||
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;
|
||||
}
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Fix image fetch-from-URL preview and cropping</name>
|
||||
<files>src/server/routes/images.ts, src/client/routes/admin/items/$itemId.tsx</files>
|
||||
<action>
|
||||
**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).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun run lint</automated>
|
||||
</verify>
|
||||
<done>After fetching an image from URL on the admin item edit page, the image preview displays immediately and the crop button is visible/functional.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Fix tag edit page routing</name>
|
||||
<files>src/client/routes/admin/tags/$tagId.tsx</files>
|
||||
<action>
|
||||
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).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bunx tsr generate && bun run lint</automated>
|
||||
</verify>
|
||||
<done>Navigating to /admin/tags/$tagId renders the tag edit form, not the tag list page.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Handle duplicate tag name error + polish inline form</name>
|
||||
<files>src/server/routes/admin-tags.ts, src/client/routes/admin/tags/index.tsx</files>
|
||||
<action>
|
||||
**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 */}
|
||||
<div className="mb-6 rounded-xl border border-gray-100 bg-white p-4">
|
||||
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wide mb-3">Add Tag</p>
|
||||
<form onSubmit={handleCreate} className="flex items-end gap-3">
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs text-gray-500 mb-1">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
placeholder="e.g. Bikepacking"
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-48">
|
||||
<label className="block text-xs text-gray-500 mb-1">Parent</label>
|
||||
<select
|
||||
value={newParentId ?? ""}
|
||||
onChange={(e) => setNewParentId(e.target.value ? Number(e.target.value) : null)}
|
||||
className={`${inputClass} appearance-none bg-white`}
|
||||
>
|
||||
<option value="">No parent (top-level)</option>
|
||||
{data?.map((t) => (
|
||||
<option key={t.id} value={t.id}>{t.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={createMutation.isPending || !newName.trim()}
|
||||
className="px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium transition-colors disabled:opacity-50 shrink-0"
|
||||
>
|
||||
{createMutation.isPending ? "Adding..." : "Add Tag"}
|
||||
</button>
|
||||
</form>
|
||||
{createError && (
|
||||
<p className="text-sm text-red-500 mt-2">{createError}</p>
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
|
||||
This wraps the form in a card with labels, making it visually consistent with the rest of the admin UI and easier to navigate.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun run lint</automated>
|
||||
</verify>
|
||||
<done>Creating 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.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, verify all fixes manually and commit with message:
|
||||
`fix(admin): resolve UAT issues — image fetch preview, tag routing, duplicate error, form UX`
|
||||
</output>
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
phase: quick
|
||||
plan: 260420-vk0
|
||||
subsystem: admin
|
||||
tags: [bugfix, uat, image-upload, routing, tags]
|
||||
dependency_graph:
|
||||
requires: []
|
||||
provides: [image-fetch-preview, tag-duplicate-error, tag-form-polish]
|
||||
affects: [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]
|
||||
tech_stack:
|
||||
added: []
|
||||
patterns: [presigned-url-passthrough, api-error-message-surfacing, card-form-pattern]
|
||||
key_files:
|
||||
created: []
|
||||
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
|
||||
decisions:
|
||||
- "ApiError.message carries the server's error field from JSON response body — no custom parsing needed in catch blocks"
|
||||
- "Task 2 (tag routing) was already resolved by commit 31a9e3c before this quick task ran — no changes needed"
|
||||
metrics:
|
||||
duration: "~8 minutes"
|
||||
completed: "2026-04-20"
|
||||
tasks_completed: 3
|
||||
files_modified: 4
|
||||
---
|
||||
|
||||
# Quick Task 260420-vk0: Fix UAT Issues — Image Fetch Preview, Tag Routing, Duplicate Error, Form UX
|
||||
|
||||
**One-liner:** Presigned URL passthrough to client fixes fetch-from-URL image preview; 409 unique constraint handler + card-form polish closes tag UAT gaps.
|
||||
|
||||
## Tasks Completed
|
||||
|
||||
| Task | Status | Commit | Files |
|
||||
|------|--------|--------|-------|
|
||||
| 1: Fix image fetch-from-URL preview and cropping | Done | b41b832 | images.ts, $itemId.tsx |
|
||||
| 2: Fix tag edit page routing | Done (pre-existing) | 31a9e3c | routeTree.gen.ts (prior commit) |
|
||||
| 3: Handle duplicate tag name error + polish inline form | Done | 113e689 | admin-tags.ts, tags/index.tsx |
|
||||
|
||||
## What Was Built
|
||||
|
||||
**Task 1 — Image fetch-from-URL preview:**
|
||||
- `images.ts`: Added `getImageUrl` import from storage service; after `fetchImageFromUrl` completes, calls `getImageUrl(result.filename)` to generate a presigned URL and includes it in the response as `presignedUrl`.
|
||||
- `$itemId.tsx`: Updated `handleFetchFromUrl` type annotation to include `presignedUrl` and `dominantColor`; after successful fetch, sets `imageUrl: result.presignedUrl` and `dominantColor` in form state. This causes `ImageUpload` to receive the presigned URL via its `imageUrl` prop and render the image preview immediately, making the crop button visible and functional.
|
||||
|
||||
**Task 2 — Tag edit page routing:**
|
||||
Already resolved by commit 31a9e3c (which moved both items and tags to directory-based routing: `items/index.tsx` + `items/$itemId.tsx` / `tags/index.tsx` + `tags/$tagId.tsx`). The route tree correctly registers `AdminTagsTagIdRoute` and `AdminTagsIndexRoute` as siblings under `AdminRoute`. No additional changes required.
|
||||
|
||||
**Task 3 — Duplicate tag name error + form polish:**
|
||||
- `admin-tags.ts`: Wrapped `createTag` in try/catch; detects SQLite UNIQUE constraint violations by checking `err.message` for "UNIQUE constraint failed" or "unique constraint", returning `{ error: "A tag named \"X\" already exists" }` with status 409.
|
||||
- `tags/index.tsx`: Updated `handleCreate` catch to use `err instanceof Error ? err.message : "Failed to create tag"` — `ApiError` (thrown by `apiPost`) carries the server's `error` field as its message, so the friendly 409 message surfaces directly.
|
||||
- `tags/index.tsx`: Replaced bare flex-row form with card-style wrapper (`rounded-xl border border-gray-100 bg-white p-4`), section header, field labels for Name and Parent, and `shrink-0` on the submit button. Error message moved inside the card below the form.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Pre-resolved Issues
|
||||
|
||||
**Task 2 already fixed before this quick task ran**
|
||||
- **Found during:** Investigation of tag routing
|
||||
- **Issue:** Plan described investigating why `/admin/tags/$tagId` showed the list — but commit 31a9e3c (landed same day, prior to this quick task) already moved tag routes to directory structure and regenerated the route tree. Routes are correctly registered as siblings under AdminRoute.
|
||||
- **Action taken:** Verified route tree, confirmed no changes needed, documented as pre-resolved. Continued to Task 3.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None. All implemented functionality is wired to real data.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
None. No new network endpoints or auth paths introduced.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- [x] `src/server/routes/images.ts` exists and contains `getImageUrl` import and `presignedUrl` in response
|
||||
- [x] `src/client/routes/admin/items/$itemId.tsx` contains `result.presignedUrl` and `imageUrl: result.presignedUrl` in form state update
|
||||
- [x] `src/server/routes/admin-tags.ts` contains try/catch with 409 for UNIQUE constraint
|
||||
- [x] `src/client/routes/admin/tags/index.tsx` contains card-style form with labels
|
||||
- [x] Commit b41b832 exists (Task 1)
|
||||
- [x] Commit 113e689 exists (Task 3)
|
||||
- [x] `bun run lint` passes with no errors
|
||||
Reference in New Issue
Block a user