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:
- Fruit's own image with
image_type = 'fruit' (first found)
- First
publication_fruit_images entry linked to the fruit
- 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
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)
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:
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 |