Phases 28-31 archived to milestones/v2.2-phases/ Requirements and roadmap snapshots archived to milestones/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
10 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 29 | 01 | backend | 1 |
|
true |
Task 1: Install Sharp dependency
Run `bun add sharp` and `bun add -d @types/sharp` to install the Sharp image processing library and its type definitions. grep '"sharp"' package.json && echo "PASS" || echo "FAIL" - package.json contains `"sharp"` in dependencies - @types/sharp in devDependencies - `bun install` completes without errorsTask 2: Add schema fields to database
- src/db/schema.ts Add four new fields to THREE tables in `src/db/schema.ts`:items table — add after brand: text("brand"):
dominantColor: text("dominant_color"),
cropZoom: doublePrecision("crop_zoom"),
cropX: doublePrecision("crop_x"),
cropY: doublePrecision("crop_y"),
globalItems table — add after imageSourceUrl: text("image_source_url"):
dominantColor: text("dominant_color"),
cropZoom: doublePrecision("crop_zoom"),
cropX: doublePrecision("crop_x"),
cropY: doublePrecision("crop_y"),
threadCandidates table — add after imageSourceUrl: text("image_source_url"):
dominantColor: text("dominant_color"),
cropZoom: doublePrecision("crop_zoom"),
cropX: doublePrecision("crop_x"),
cropY: doublePrecision("crop_y"),
Task 3: [BLOCKING] Push schema changes to database
Run `bun run db:generate` to generate the Drizzle migration, then `bun run db:push` to apply it to the database. bun run db:push 2>&1 | tail -5 - Migration generated successfully - `bun run db:push` completes without errors - Database contains dominant_color, crop_zoom, crop_x, crop_y columns on items, global_items, and thread_candidates tablesTask 4: Create dominant color extraction utility
- src/server/services/storage.service.ts - src/server/services/image.service.ts Create a new function `extractDominantColor` in `src/server/services/image.service.ts`:import sharp from "sharp";
/**
* Extract the dominant color from an image buffer.
* Resizes to 1x1 pixel for a perceptually weighted average.
* Returns hex string like '#a3b2c1' or null on failure.
*/
export async function extractDominantColor(buffer: Buffer | ArrayBuffer): Promise<string | null> {
try {
const { data } = await sharp(Buffer.from(buffer))
.resize(1, 1)
.raw()
.toBuffer({ resolveWithObject: true });
const r = data[0];
const g = data[1];
const b = data[2];
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
} catch {
return null;
}
}
Keep the existing fetchImageFromUrl function. Add the import for sharp at the top.
grep "extractDominantColor" src/server/services/image.service.ts && echo "PASS" || echo "FAIL"
<acceptance_criteria>
src/server/services/image.service.tsexportsextractDominantColorfunction- Function accepts
Buffer | ArrayBufferand returnsPromise<string | null> - Uses
sharp(buffer).resize(1, 1).raw().toBuffer()to extract color - Returns hex string in format
#rrggbb - Returns null on error (try/catch) </acceptance_criteria>
Task 5: Integrate dominant color extraction into upload endpoints
- src/server/routes/images.ts - src/server/services/image.service.ts Update `src/server/routes/images.ts` to extract dominant color during upload:POST / (direct upload):
- After
const buffer = await file.arrayBuffer(); - Add:
const dominantColor = await extractDominantColor(buffer); - Change response from
{ filename }to{ filename, dominantColor }
POST /from-url:
- In
src/server/services/image.service.ts, updatefetchImageFromUrlto also extract dominant color - After
await uploadImage(Buffer.from(buffer), filename, contentType); - Add:
const dominantColor = await extractDominantColor(buffer); - Change return from
{ filename, sourceUrl: url }to{ filename, sourceUrl: url, dominantColor } - Update
FetchImageResultinterface to includedominantColor: string | null
Import extractDominantColor in images.ts from ../services/image.service.
grep "dominantColor" src/server/routes/images.ts && grep "dominantColor" src/server/services/image.service.ts && echo "PASS" || echo "FAIL"
<acceptance_criteria>
POST /api/imagesresponse includesdominantColorfield (string or null)POST /api/images/from-urlresponse includesdominantColorfieldFetchImageResultinterface hasdominantColor: string | null- Dominant color extraction happens before the response is sent </acceptance_criteria>
Task 6: Update Zod schemas for new fields
- src/shared/schemas.ts Update `src/shared/schemas.ts`:createItemSchema — add after brand: z.string().optional():
dominantColor: z.string().nullable().optional(),
cropZoom: z.number().nullable().optional(),
cropX: z.number().nullable().optional(),
cropY: z.number().nullable().optional(),
createCandidateSchema — add after globalItemId: z.number().int().positive().optional():
dominantColor: z.string().nullable().optional(),
cropZoom: z.number().nullable().optional(),
cropX: z.number().nullable().optional(),
cropY: z.number().nullable().optional(),
upsertGlobalItemSchema — add after tags:
dominantColor: z.string().nullable().optional(),
cropZoom: z.number().nullable().optional(),
cropX: z.number().nullable().optional(),
cropY: z.number().nullable().optional(),
updateItemSchema and updateCandidateSchema already use .partial() so they inherit the new fields automatically.
grep -c "dominantColor" src/shared/schemas.ts | grep -q "3" && echo "PASS" || echo "FAIL"
<acceptance_criteria>
createItemSchemacontainsdominantColor,cropZoom,cropX,cropYfieldscreateCandidateSchemacontains the same 4 fieldsupsertGlobalItemSchemacontains the same 4 fields- All use
z.number().nullable().optional()for crop fields - All use
z.string().nullable().optional()for dominantColor </acceptance_criteria>
Task 7: Update storage service to return dominant color with image URLs
- src/server/services/storage.service.ts The `withImageUrl` and `withImageUrls` functions in `src/server/services/storage.service.ts` currently enrich records with `imageUrl`. They already pass through all record fields via spread operator, so `dominantColor`, `cropZoom`, `cropX`, `cropY` will automatically be included in the response when they exist on the record.No changes needed to storage.service.ts — the spread operator { ...record, imageUrl } already forwards all fields.
Verify this by confirming the return type T & { imageUrl: string | null } preserves all properties of T.
grep "...record" src/server/services/storage.service.ts && echo "PASS" || echo "FAIL"
<acceptance_criteria>
withImageUrlfunction uses spread operator{ ...record, imageUrl }which preserves dominantColor and crop fields from the source record- No changes needed — verify existing behavior is sufficient </acceptance_criteria>
<success_criteria>
- Sharp installed and importable
- 3 tables have dominantColor + crop fields
- Image upload extracts and returns dominant color
- Zod schemas accept new fields
- All existing tests pass </success_criteria>
<threat_model>
| Threat | Severity | Mitigation |
|---|---|---|
| Sharp buffer overflow via malformed image | Medium | Sharp handles this internally with libvips bounds checking; wrapped in try/catch returning null |
| DoS via large image processing | Low | Existing 5MB file size limit applies before Sharp processing |
| Stored XSS via dominantColor field | Low | Value is a hex color string extracted server-side, not user input; rendered as CSS backgroundColor |
| </threat_model> |
<must_haves>
- Sharp dependency installed
- dominantColor field on items, globalItems, threadCandidates
- Crop fields (cropZoom, cropX, cropY) on all 3 tables
- Upload endpoints return dominantColor
- Schema pushed to database </must_haves>