Files
osdb-1-claude-opusplan/docs/planning/specs/07-improve-fruit-overview.md
juliaandClaude Sonnet 4.6 1070ba3ac1 docs: add sprint planning — dependency diagram + specs for all 8 stories
Analyzes all Taiga stories, maps hard/soft dependencies, and writes
full specs (Goal, Data Model, Components, Verification, Open Questions)
for every story before any implementation begins.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 11:53:24 +02:00

152 lines
5.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Spec: 07 · Improve Fruit Overview
**Taiga Story:** #7 · `07-improve-fruit-overview`
**Status:** Ready
**Branch:** `feature/07-improve-fruit-overview`
**Depends on:** `02-manage-fruits` (fruit_images), `04-fruit-publications` (publication_fruit_images)
---
## 1. Goal
Replace the plain fruit list with styled cards showing title, OSDB ID, type, synonyms, and an auto-cropped thumbnail. Thumbnails are generated at upload time in Go (stdlib only) and stored as a separate DB column. A backfill endpoint handles existing images.
---
## 2. Background / Scope / Context / Constraints
- Thumbnail selection priority:
1. Fruit's own image with `image_type = 'fruit'` (first found)
2. First `publication_fruit_images` entry linked to the fruit
3. No image → no thumbnail (graceful empty state)
- Thumbnail constraint: image may be scaled down but MUST NOT be cropped (aspect ratio preserved). Empty borders allowed — no cutting.
- Auto-crop algorithm: find bounding box of non-background content → add small padding → crop → re-encode as JPEG. Falls back to original if image decoding fails.
- Thumbnail stored in `thumbnail_data BYTEA` column (nullable) on `fruit_images` and `publication_fruit_images`.
- New serve endpoints for thumbnails, falling back to full `data` if `thumbnail_data` is NULL.
- Backfill: HTTP endpoint `POST /api/v1/admin/backfill-thumbnails` (or CLI command) processes all rows without `thumbnail_data`.
- No external libraries (OpenCV etc.) — Go stdlib only (`image/jpeg`, `image/png`).
---
## 3. Necessary Refactorings
- Add `thumbnail_data` migration for both image tables.
- On-upload path for both `fruit_images` and `publication_fruit_images`: run crop before INSERT.
- `ListFruits` response: include `thumbnail_url` pointing to new `/thumbnail` endpoint.
- `FruitList.vue`: replace table rows with card components.
---
## 4. Related Data Model
```sql
-- Migration: 000004_add_thumbnails.up.sql
ALTER TABLE fruit_images
ADD COLUMN thumbnail_data BYTEA;
ALTER TABLE publication_fruit_images
ADD COLUMN thumbnail_data BYTEA;
```
---
## 5. Affected Places / Related Components
### Backend
| File | Change |
|------|--------|
| `backend/internal/imaging/crop.go` | Auto-crop function: `CropToContent(data []byte) ([]byte, error)` |
| `backend/internal/repository/fruit_repo.go` | `CreateImage`: call `CropToContent`, store both `data` + `thumbnail_data` |
| `backend/internal/repository/publication_repo.go` | Same for `publication_fruit_images` |
| `backend/internal/handler/fruit_handler.go` | Add `GET /fruits/:id/images/:imageId/thumbnail` |
| `backend/internal/handler/publication_handler.go` | Add `GET /publications/:id/fruit-images/:imgId/thumbnail` |
| `backend/internal/handler/admin_handler.go` | `POST /api/v1/admin/backfill-thumbnails` |
| `backend/cmd/server/router.go` | Register new routes |
| `migrations/000004_add_thumbnails.up.sql` | ALTER TABLE |
### Crop Algorithm (Go stdlib)
```go
// Pseudocode — no external deps
func CropToContent(src []byte) ([]byte, error) {
img, _, err := image.Decode(bytes.NewReader(src))
if err != nil { return src, nil } // fallback: return original
bounds := img.Bounds()
// Detect background: sample corner pixel as bg color
bgColor := img.At(bounds.Min.X, bounds.Min.Y)
// Find bounding box of non-bg pixels
minX, minY, maxX, maxY := findContentBounds(img, bgColor, threshold=10)
// Add padding (e.g. 5px each side)
// Crop to bounding box
// Encode as JPEG
return encodedBytes, nil
}
```
### Frontend
| File | Change |
|------|--------|
| `frontend/src/views/FruitList.vue` | Replace rows with `<FruitCard>` components |
| `frontend/src/components/FruitCard.vue` | Card: thumbnail + name + OSDB ID + type + synonyms |
| `frontend/src/api/fruits.ts` | `listFruits` response includes `thumbnail_url` |
### API Changes
| New endpoint | Description |
|-------------|-------------|
| `GET /api/v1/fruits/:id/images/:imageId/thumbnail` | Serves `thumbnail_data`, falls back to `data` |
| `GET /api/v1/publications/:id/fruit-images/:imgId/thumbnail` | Same |
| `POST /api/v1/admin/backfill-thumbnails` | Processes all rows missing `thumbnail_data` |
`ListFruits` response shape per fruit:
```json
{
"id": 1,
"name": "Boskop",
"osdb_number": "a001",
"fruit_type": "Apfelsorten",
"synonyms": ["Roter Boskop"],
"thumbnail_url": "/api/v1/fruits/1/images/3/thumbnail"
}
```
---
## 6. Out of Scope
- Interactive image editor / manual crop
- WebP encoding
- CDN / object storage for images
- Pagination of fruit list
---
## 7. Verification
| Check | Expected |
|-------|---------|
| Upload fruit image → `fruit_images.thumbnail_data` is non-null | Pass |
| `GET /thumbnail` endpoint → responds with JPEG, smaller than original | Pass |
| Backfill endpoint: runs on existing rows without thumbnail_data | All rows populated |
| `GET /api/v1/fruits` → each fruit with image has `thumbnail_url` | Pass |
| Fruit with no image → `thumbnail_url: null` | Graceful |
| Thumbnail falls back to publication image if no direct image | Correct priority |
| `FruitList.vue` shows cards with thumbnails | Visual check |
| Image with undecodable format → fallback = original stored, no error | Pass |
| `go test ./...` including imaging unit tests | Green |
---
## 8. Open Questions
| # | Question | Impact |
|---|----------|--------|
| 1 | Background detection threshold: what color tolerance? | Crop quality |
| 2 | Padding around cropped content: 5px? 10px? | Visual quality |
| 3 | Should backfill endpoint be idempotent (skip rows already having thumbnail)? | Yes — only process WHERE thumbnail_data IS NULL |
| 4 | Max concurrent backfill workers? | Performance |
| 5 | Thumbnail max dimensions (e.g. 300×300 max)? | UX + storage |