mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
M1 of docs/plans/wardrobe-module.md — pure data layer + backend plumbing, zero UI (that's M2). A user can now hold a digital wardrobe per space: brand merch, club Trikots, family Kleiderschrank, team Kostüme, practice Dresscode, and personal closet all live as separate pools under the same Dexie tables, space-scoped like tags/scenes/agents after Phase 2c. Data model — two tables, no join: - wardrobeGarments (Dexie v41): single clothing items / accessories. Indexed on `category` + `createdAt` + `isArchived`. Encrypted: name/brand/color/size/material/tags/notes. Plaintext: category, mediaIds, counters, timestamps — all indexed or structural. `mediaIds[0]` is the primary photo used for try-on; additional ids are alternate views (back, detail) for M7. - wardrobeOutfits (Dexie v41): named compositions referencing garment ids. Encrypted: name/description/tags. Plaintext: garmentIds (FK array), occasion (closed enum — useful for undecrypted filtering), season, booleans, lastTryOn snapshot. - picture.images gains `wardrobeOutfitId?: string | null` as a plaintext back-reference. Try-on results land in the Picture gallery like any other generation; the outfit detail view queries them via this id rather than maintaining a third table. Space scope: - `wardrobe` added to all five explicit allowlists in shared-types/ spaces.ts (personal is wildcard, no edit needed). Each space type gets a one-line comment explaining the real-world use case. - App registry: `wardrobe` entry in shared-branding/mana-apps.ts with a rose→fuchsia gradient icon (T-shirt on hanger silhouette), color #e11d48, tier 'beta', status 'beta'. - Module registry: wardrobeModuleConfig imported + appended to MODULE_CONFIGS so SYNC_APP_MAP picks it up automatically. Backend: - MAX_REFERENCE_IMAGES bumped 4 → 8 in picture/generate-with- reference (plus the client-side default in ReferenceImagePicker). Justified with a comment: face + body + top + bottom + shoes + outerwear + 2 accessories = 8. Cost doesn't scale with ref count (OpenAI bills per output), so the bump is a pure capability expansion with no credit-side risk. - New POST /api/v1/wardrobe/garments/upload wraps uploadImageToMedia with app='wardrobe'. Registered under /api/v1/wardrobe in index.ts. Pattern 1:1 with the profile/me-images/upload endpoint; tier-gating falls out of wardrobe NOT being in RESOURCE_MODULES (tier='guest' works — consistent with picture's plain CRUD). Stores emit domain events (WardrobeGarmentAdded, WardrobeOutfitCreated, WardrobeOutfitTryOn, etc.) so later mana-ai missions can observe activity without polling. No UI in this commit. M2 (Garments-Grundlayer) wires the route + grid + upload-zone; M3 the Outfit composer; M4 the Try-On integration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
55 lines
1.7 KiB
TypeScript
55 lines
1.7 KiB
TypeScript
/**
|
|
* Wardrobe module — server endpoints.
|
|
*
|
|
* Thin wrapper around mana-media for garment photo uploads. Plan:
|
|
* docs/plans/wardrobe-module.md M1. No logic beyond tagging uploads
|
|
* as `app='wardrobe'` so a later `GET /api/v1/media?app=wardrobe&...`
|
|
* query can enumerate a user's garment pool without scanning every
|
|
* media reference.
|
|
*
|
|
* Try-on generation does NOT live here — it reuses the Picture
|
|
* module's POST /api/v1/picture/generate-with-reference endpoint
|
|
* with MAX_REFERENCE_IMAGES bumped to 8 so face + body + garments
|
|
* fit into one call.
|
|
*/
|
|
|
|
import { Hono } from 'hono';
|
|
import type { AuthVariables } from '@mana/shared-hono';
|
|
|
|
const routes = new Hono<{ Variables: AuthVariables }>();
|
|
|
|
// Same 10MB cap as the other photo-upload endpoints (profile me-images,
|
|
// picture uploads). Phone-camera PNG/HEIC routinely comes in under 6MB.
|
|
const MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
|
|
routes.post('/garments/upload', async (c) => {
|
|
const userId = c.get('userId');
|
|
const formData = await c.req.formData();
|
|
const file = formData.get('file') as File | null;
|
|
|
|
if (!file) return c.json({ error: 'No file' }, 400);
|
|
if (file.size > MAX_UPLOAD_BYTES) return c.json({ error: 'Max 10MB' }, 400);
|
|
|
|
try {
|
|
const { uploadImageToMedia } = await import('../../lib/media');
|
|
const buffer = await file.arrayBuffer();
|
|
const result = await uploadImageToMedia(buffer, file.name, {
|
|
app: 'wardrobe',
|
|
userId,
|
|
});
|
|
|
|
return c.json(
|
|
{
|
|
mediaId: result.id,
|
|
storagePath: result.id,
|
|
publicUrl: result.urls.original,
|
|
thumbnailUrl: result.urls.thumbnail,
|
|
},
|
|
201
|
|
);
|
|
} catch (_err) {
|
|
return c.json({ error: 'Upload failed' }, 500);
|
|
}
|
|
});
|
|
|
|
export { routes as wardrobeRoutes };
|