# Feature Specification: Upload Thumbnails

**Feature Branch**: `003-upload-thumbnails`
**Created**: 2026-05-03
**Status**: Draft
**Input**: User description: "When users load the UI, full size images are fetched, which may cause considerable load time when there are a lot of images -- a grid of 20 images could silently pull several hundred megabytes. We will solve this by pre-generating thumbnails on upload: when an image is uploaded, immediately produce one (or a few) fixed-size thumbnail variants and store them alongside the original. The library always fetches the thumbnail key, the detail page fetches the original key. Zero resize cost at serve time. A single fixed-size re-encoded as WebP for smaller bytes will cover the grid view."

## User Scenarios & Testing *(mandatory)*

### User Story 1 — Fast Library Load (Priority: P1)

A user opens the application or runs a tag-filtered search. The image grid
loads quickly even when the library contains many images, because each grid
cell fetches a compact thumbnail rather than the full-size original.

**Why this priority**: This is the core motivation. A grid of 20 images today
could pull hundreds of megabytes; thumbnails bring that down to a few
megabytes, making the library usable on slow or metered connections.

**Independent Test**: Upload 20 images of varying sizes (including some near
the 50 MB limit). Open the library. Measure total bytes transferred while the
grid loads. Compare against loading the same library before this feature.
Verify the grid renders fully and that each thumbnail is visually recognisable
as the correct image.

**Acceptance Scenarios**:

1. **Given** a library with multiple images, **When** the user opens the
   library page, **Then** each grid cell displays a thumbnail that is visually
   recognisable as its image, and the total data transferred to render the
   full grid is substantially less than the sum of the original file sizes.

2. **Given** a user applies one or more tag filters, **When** the filtered
   results are displayed, **Then** thumbnails are shown for all matching
   images with the same reduced data footprint.

3. **Given** a library with images of mixed types (JPEG, PNG, GIF, WebP),
   **When** the grid loads, **Then** thumbnails for all types display
   correctly.

---

### User Story 2 — Full-Size Image on Detail Page (Priority: P1)

A user clicks an image in the library grid to open its detail page. The
full-size original is displayed, not the thumbnail. The experience is
unchanged from before this feature.

**Why this priority**: The detail page is where the user inspects or copies
an image; showing the thumbnail there would degrade the product's core value.

**Independent Test**: Open any image's detail page. Verify the image
displayed matches the original resolution and file size, not the thumbnail
dimensions.

**Acceptance Scenarios**:

1. **Given** the user clicks an image thumbnail in the library, **When** the
   detail page loads, **Then** the full-size original image is displayed at
   its native resolution.

2. **Given** the user navigates directly to an image's detail URL, **When**
   the page loads, **Then** the full-size original is displayed.

---

### User Story 3 — Thumbnails Generated Automatically on Upload (Priority: P1)

A user uploads a new image. Without any additional action, a thumbnail is
available immediately. There is no separate step or explicit request to
generate a thumbnail.

**Why this priority**: The value of the feature depends entirely on thumbnails
being present for every image. Manual generation or lazy generation would
create inconsistencies in the grid.

**Independent Test**: Upload a new image. Immediately open the library. Verify
the new image's thumbnail appears in the grid without any extra action.

**Acceptance Scenarios**:

1. **Given** the user uploads a supported image, **When** the upload
   completes, **Then** a thumbnail is available and appears correctly in
   the library grid.

2. **Given** the user uploads a duplicate image (already in the library),
   **When** the upload completes, **Then** no redundant thumbnail is
   generated — the existing thumbnail is reused.

3. **Given** the user uploads an image at or near the maximum supported
   file size (50 MB), **When** the upload completes, **Then** the thumbnail
   is generated successfully and the upload response time remains acceptable.

---

### Edge Cases

- What happens when thumbnail generation fails during upload? → The upload
  still succeeds and the original image is stored; a fallback to the original
  is shown in the grid, or the item is hidden until the thumbnail is available
  (assumption: fall back to original rather than silently drop the image).
- What happens when an image is deleted? → Both the original and its thumbnail
  are removed from storage.
- What happens with existing images that were uploaded before this feature? →
  Those images have no pre-generated thumbnail; the grid falls back to the
  original for those entries until a backfill is performed (backfill is out of
  scope for v1 of this feature).
- What happens with animated GIFs? → A static thumbnail is generated from the
  first frame.

## Requirements *(mandatory)*

### Functional Requirements

- **FR-001**: The system MUST generate a thumbnail for every newly uploaded
  image as part of the upload operation, before the upload response is
  returned to the caller.
- **FR-002**: Thumbnails MUST be stored in the same object storage as the
  original, addressable by a distinct key derived from the image.
- **FR-003**: The thumbnail MUST be encoded as WebP regardless of the original
  image format.
- **FR-004**: The thumbnail MUST fit within a fixed maximum dimension on its
  longest side, preserving the original aspect ratio; no dimension of the
  thumbnail MAY exceed 400 pixels.
- **FR-005**: The library grid view MUST fetch and display thumbnails instead
  of original images.
- **FR-006**: The image detail view MUST continue to fetch and display the
  full-size original.
- **FR-007**: When a duplicate image is uploaded, the thumbnail MUST NOT be
  regenerated or re-stored; the existing thumbnail is reused.
- **FR-008**: When an image is deleted, its thumbnail MUST also be deleted
  from storage.
- **FR-009**: If thumbnail generation fails during upload, the upload MUST
  still succeed; the system MUST fall back to serving the original image in
  the grid for that entry.
- **FR-010**: The API MUST expose a way for clients to retrieve the thumbnail
  content for a given image, distinct from the full-size content endpoint.

### Key Entities *(include if feature involves data)*

- **Image**: Gains a new optional attribute indicating whether a thumbnail is
  available and the key under which the thumbnail is stored.
- **Thumbnail**: A derived, smaller representation of an Image. Key
  attributes: storage key, dimensions (width × height), format (WebP),
  relationship to its source Image.

## Success Criteria *(mandatory)*

### Measurable Outcomes

- **SC-001**: The total data transferred to render a 20-image library grid is
  reduced by at least 80% compared to fetching full-size originals for the
  same images.
- **SC-002**: The library grid's first page loads in under 2 seconds on a
  local network connection for a library of 1,000 images, with thumbnails
  visible without a second load.
- **SC-003**: Thumbnails are available immediately after upload completes —
  no polling or manual refresh is required.
- **SC-004**: The detail page continues to show the full-size original; no
  regression in detail-page image quality is introduced.
- **SC-005**: Deleting an image removes both the original and its thumbnail;
  no orphaned thumbnail objects remain in storage after deletion.

## Assumptions

- A single thumbnail size (longest side ≤ 400 px, WebP) is sufficient for
  the library grid view in v1. Additional sizes or formats are out of scope.
- Thumbnail generation happens synchronously during the upload request. Async
  background processing is not required for v1.
- Existing images uploaded before this feature are not automatically
  backfilled with thumbnails in v1; the grid falls back to the original for
  those entries.
- Animated GIF thumbnails capture only the first frame; animation is not
  preserved in the thumbnail.
- The thumbnail storage key is derived deterministically from the image's
  existing content hash, so no additional database column is strictly required
  to locate it — however the Image record will track thumbnail availability
  for correctness.
- No change is required to tag management, duplicate detection, or any other
  upload behaviour beyond adding thumbnail generation.
