Files
GearBox/.planning/quick/260420-vk0-fix-uat-issues-image-fetch-from-url-imag/260420-vk0-PLAN.md
Jean-Luc Makiola 1f2e8e18c4
All checks were successful
CI / ci (push) Successful in 1m56s
CI / e2e (push) Has been skipped
CI / deploy (push) Successful in 17s
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>
2026-04-20 22:52:12 +02:00

11 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
quick 260420-vk0 execute 1
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
true
truths artifacts key_links
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
path provides
src/server/routes/images.ts from-url endpoint returns presignedUrl for immediate display
path provides
src/client/routes/admin/items/$itemId.tsx Updates imageUrl in form state after fetch-from-URL
path provides
src/server/routes/admin-tags.ts Catches unique constraint violations, returns 409
path provides
src/client/routes/admin/tags/index.tsx Polished inline tag creation form
from to via pattern
src/client/routes/admin/items/$itemId.tsx /api/images/from-url apiPost then updates form.imageUrl with returned presignedUrl 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.

<execution_context> @.planning/quick/260420-vk0-fix-uat-issues-image-fetch-from-url-imag/260420-vk0-PLAN.md </execution_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 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:

export async function getImageUrl(filename: string): Promise<string>;

From src/client/components/ImageUpload.tsx:

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 cropping src/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:

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 lint After 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 routing src/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 lint Navigating to /admin/tags/$tagId renders the tag edit form, not the tag list page.

Task 3: Handle duplicate tag name error + polish inline form src/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:
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.

  1. Polish the inline tag creation form. Replace the plain flex items-center gap-3 row with a more visually consistent card-style form:
{/* 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. cd /home/jlmak/Projects/jlmak/GearBox && bun run lint 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.

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

<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>
After completion, verify all fixes manually and commit with message: `fix(admin): resolve UAT issues — image fetch preview, tag routing, duplicate error, form UX`