# Feature Specification: Reaction Image Board v1

**Feature Branch**: `001-reaction-image-board`
**Created**: 2026-05-02
**Status**: Draft
**Input**: User description: "Read docs/SPEC.md, from which we will create the official spec"

## User Scenarios & Testing *(mandatory)*

### User Story 1 — Upload an Image (Priority: P1)

A user drags and drops (or browses to select) a single image file from their
device, optionally adds tags, and submits the upload. The image appears in
the library immediately. If the same image was already uploaded before, the
system recognises the duplicate and shows the existing entry without creating
a second copy.

**Why this priority**: This is the core data-entry action. Without it no
library content exists to browse, search, or manage.

**Independent Test**: Upload a JPEG, verify it appears in the library grid,
then re-upload the same file and verify only one copy exists with an
"Already in your library" notification.

**Acceptance Scenarios**:

1. **Given** a supported image file (JPEG, PNG, GIF, or WebP) under 50 MB,
   **When** the user submits the upload form,
   **Then** the image is stored, appears in the library, and the user is
   taken to the image's detail page.

2. **Given** an image already in the library,
   **When** the user uploads the same file again,
   **Then** no duplicate is stored, the user sees an "Already in your library"
   notification, and is navigated to the existing image's detail page.

3. **Given** an unsupported file type (e.g. PDF, MP4),
   **When** the user attempts to upload it,
   **Then** an inline error is shown and the user remains on the upload page.

4. **Given** a file larger than 50 MB,
   **When** the user attempts to upload it,
   **Then** an inline error is shown before any storage is attempted.

5. **Given** a tag name longer than 64 characters or containing characters
   outside lowercase letters, digits, hyphens, and underscores,
   **When** the user submits the upload,
   **Then** an inline validation error identifies the problematic tag and the
   upload does not proceed.

---

### User Story 2 — Browse and Filter the Library (Priority: P1)

A user opens the application and sees a grid of all their uploaded images as
thumbnails. They can filter the grid by selecting one or more tags; the grid
updates to show only images that carry **all** selected tags. Filters can be
added and removed interactively without reloading the page.

**Why this priority**: The library view is the default landing page and the
primary way to find and re-use reaction images.

**Independent Test**: Seed the library with tagged images, apply a single tag
filter and verify only matching images are shown, then add a second filter and
verify both tags are required on every visible result.

**Acceptance Scenarios**:

1. **Given** the library contains images,
   **When** the user opens the application,
   **Then** all images are shown in reverse chronological order as thumbnails
   with their tags displayed beneath each one.

2. **Given** a non-empty library,
   **When** the user selects one or more tags in the filter bar,
   **Then** only images that have every selected tag are shown.

3. **Given** active tag filters,
   **When** the user removes a filter chip,
   **Then** the grid expands to reflect the remaining filters (or shows all
   images if no filters remain).

4. **Given** a large library (more images than fit on screen),
   **When** the user scrolls to the bottom or clicks "Load more",
   **Then** additional images load without replacing the already-visible ones.

---

### User Story 3 — View Image Detail and Edit Tags (Priority: P2)

A user clicks an image in the library to open a detail page showing the
full-size image and its current tags. They can add new tags or remove
existing ones. Changes are saved on blur or pressing Enter, not on every
keystroke.

**Why this priority**: Tag management is the primary organisation mechanism;
editing must be accessible from the image itself.

**Independent Test**: Open any image detail page, add a new tag, navigate
back to the library, filter by that tag, and confirm the image appears.

**Acceptance Scenarios**:

1. **Given** the user navigates to an image detail page,
   **When** the page loads,
   **Then** the full-size image is displayed alongside all its current tags.

2. **Given** the user types a new tag into the tag input and presses Enter
   (or moves focus away),
   **Then** the tag is added to the image and the display updates immediately.

3. **Given** the user clicks the remove (×) button on an existing tag chip,
   **Then** the tag is removed from the image.

4. **Given** a tag value that exceeds 64 characters or contains invalid
   characters,
   **When** the user tries to save it,
   **Then** an inline error is shown and the invalid tag is not persisted.

---

### User Story 4 — Delete an Image (Priority: P2)

A user chooses to permanently remove an image from their library. A
confirmation step prevents accidental deletion. After deletion, the image
is gone from the library view and from storage.

**Why this priority**: Users must be able to remove unwanted content from a
personal collection.

**Independent Test**: Delete a known image, confirm it no longer appears in
the library, and confirm that navigating to its former detail URL shows a
"not found" screen.

**Acceptance Scenarios**:

1. **Given** the user is on an image detail page,
   **When** they click the delete button and confirm,
   **Then** the image and its stored file are permanently removed and the user
   is returned to the library.

2. **Given** the user clicks the delete button,
   **When** they dismiss the confirmation dialog (cancel),
   **Then** no deletion occurs and the user remains on the detail page.

---

### User Story 5 — Browse and Search Tags (Priority: P3)

A user can view a list of all tags currently in use, along with how many
images each tag is applied to. They can type a prefix to narrow the list.

**Why this priority**: Useful for discovering existing tags and maintaining a
consistent vocabulary, but the library filter bar already enables tag selection
so this is supplementary.

**Independent Test**: Open the tag browser and verify every tag present in the
library appears with a correct image count, then type a prefix and verify only
matching tags remain visible.

**Acceptance Scenarios**:

1. **Given** the user opens the tag browser,
   **When** the page loads,
   **Then** all tags are listed alphabetically with their image counts.

2. **Given** the user types a prefix into the search input,
   **Then** only tags whose names begin with that prefix are shown.

---

### Edge Cases

- What happens when the library is empty? → An empty-state prompt is shown
  encouraging the user to upload their first image.
- What happens when a tag filter matches zero images? → The grid shows an
  empty-results message (not an error).
- What happens when the user navigates to a non-existent image ID? → A "Not
  found" screen is shown with a link back to the library.
- What happens when the user navigates to an unknown route? → A "Not found"
  screen is shown.
- What happens when the upload form is submitted with no tags? → The image is
  stored with no tags; no validation error is raised.

## Requirements *(mandatory)*

### Functional Requirements

- **FR-001**: System MUST accept uploads of JPEG, PNG, GIF, and WebP images
  up to 50 MB per file; all other types MUST be rejected.
- **FR-002**: System MUST detect duplicate image content at upload time and
  return the existing record without writing a duplicate to storage.
- **FR-003**: Users MUST be able to attach zero or more tags to an image at
  upload time via a comma-or-space-separated text input.
- **FR-004**: Tag names MUST be normalised (lowercased, whitespace-trimmed)
  before storage and MUST conform to: lowercase letters, digits, hyphens, and
  underscores only, 1–64 characters.
- **FR-005**: Users MUST be able to filter the image library by one or more
  tags; the filter logic MUST be AND (every specified tag must be present on
  the result).
- **FR-006**: All list views MUST support pagination; no view may load the
  entire library at once.
- **FR-007**: Users MUST be able to replace the complete tag set on an
  existing image (add new tags, remove existing tags) from the detail view.
- **FR-008**: Users MUST be able to permanently delete an image including its
  stored file and all tag associations, after a confirmation step.
- **FR-009**: Images MUST be viewable in the browser (thumbnail in library,
  full-size on detail page) without exposing permanent internal storage
  credentials or addresses.
- **FR-010**: Users MUST be able to list all tags sorted alphabetically with
  associated image counts, with optional prefix filtering.
- **FR-011**: Tags MUST be created implicitly on first use; no explicit
  tag-creation step is required.
- **FR-012**: Removing a tag from an image MUST NOT delete the shared tag
  record or affect other images that use the same tag.

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

- **Image**: A single uploaded file. Key attributes: unique content
  fingerprint, original filename, file type, pixel dimensions, file size,
  upload timestamp, associated tags.
- **Tag**: A normalised text label that can be applied to many images.
  Key attributes: name (unique, always lowercase), creation timestamp, count
  of images currently using it.
- **ImageTag**: The many-to-many association between an image and a tag.

## Success Criteria *(mandatory)*

### Measurable Outcomes

- **SC-001**: A user can upload an image with tags and see it appear in the
  library in under 10 seconds on a local network connection.
- **SC-002**: Re-uploading an identical image produces no duplicate library
  entry; duplicate detection is invisible to the user except for the
  informational notification.
- **SC-003**: The library's first page of results loads in under 2 seconds
  for a collection of 1,000 images.
- **SC-004**: A tag-filtered search with 1–3 active tags returns results in
  under 2 seconds across a library of 1,000 images.
- **SC-005**: A user can add or remove a tag on an existing image within
  5 seconds of interaction on a local network connection.
- **SC-006**: The complete application starts from a clean checkout with a
  single command and requires no manual setup beyond copying the example
  environment file.

## Assumptions

- The application serves a single user (the owner) on a local network.
  No authentication, access control, or multi-user isolation is required in v1.
- Only one image file can be submitted per upload action; bulk upload is out
  of scope for v1.
- Images are immutable after upload: file content is never replaced; only the
  tag associations may change.
- The deployment environment provides S3-compatible object storage (locally
  via MinIO for development).
- Target clients are modern evergreen desktop browsers; mobile-native
  experience is explicitly out of scope for v1.
- OR/NOT tag logic, collections/albums, image editing, alternative sort
  orders, and multi-user features are all explicitly out of scope for v1.
