diff --git a/apps/mana/apps/web/src/lib/data/database.ts b/apps/mana/apps/web/src/lib/data/database.ts index cd92e299a..d4e4f3562 100644 --- a/apps/mana/apps/web/src/lib/data/database.ts +++ b/apps/mana/apps/web/src/lib/data/database.ts @@ -992,6 +992,25 @@ db.version(41).stores({ wardrobeOutfits: 'id, createdAt, isFavorite, isArchived', }); +// v42 — Index `images.wardrobeOutfitId` + add `images.wardrobeGarmentId` +// index for fast Wardrobe back-reference queries. Until now Try-On +// results in `picture.images` carried a back-ref to `wardrobeOutfits` +// but no index — `useOutfitTryOns` fell back to a linear filter across +// every image in the active space. Solo-Garment Try-Ons (M4.1) didn't +// have a back-ref at all; the result ended up in the Picture gallery +// and was unfindable from the garment detail page. +// +// Both fields are plaintext FKs to Wardrobe rows in the same space. +// Index both together so outfit-detail and garment-detail queries are +// single-field equality lookups (O(log n)) instead of table scans, and +// add the symmetric `wardrobeGarmentId` so Solo-Try-Ons have the same +// first-class back-reference as Outfit-Try-Ons. Invariant (enforced in +// the write path): at most one of the two is set per row. +db.version(42).stores({ + images: + 'id, isFavorite, isPublic, isArchived, prompt, updatedAt, wardrobeOutfitId, wardrobeGarmentId', +}); + // ─── Sync Routing ────────────────────────────────────────── // SYNC_APP_MAP, TABLE_TO_SYNC_NAME, TABLE_TO_APP, SYNC_NAME_TO_TABLE, // toSyncName() and fromSyncName() are now derived from per-module diff --git a/apps/mana/apps/web/src/lib/modules/picture/queries.ts b/apps/mana/apps/web/src/lib/modules/picture/queries.ts index c6d027e2f..d83c383dd 100644 --- a/apps/mana/apps/web/src/lib/modules/picture/queries.ts +++ b/apps/mana/apps/web/src/lib/modules/picture/queries.ts @@ -50,6 +50,7 @@ export function toImage(local: LocalImage): Image { referenceImageIds: local.referenceImageIds ?? undefined, generationMode: local.generationMode ?? undefined, wardrobeOutfitId: local.wardrobeOutfitId ?? undefined, + wardrobeGarmentId: local.wardrobeGarmentId ?? undefined, createdAt: local.createdAt ?? new Date().toISOString(), updatedAt: local.updatedAt ?? new Date().toISOString(), }; diff --git a/apps/mana/apps/web/src/lib/modules/picture/types.ts b/apps/mana/apps/web/src/lib/modules/picture/types.ts index 324e06be4..1f484e3e6 100644 --- a/apps/mana/apps/web/src/lib/modules/picture/types.ts +++ b/apps/mana/apps/web/src/lib/modules/picture/types.ts @@ -52,6 +52,14 @@ export interface LocalImage extends BaseRecord { * an extra table. Plaintext — it's an FK. */ wardrobeOutfitId?: string | null; + /** + * Back-reference to `wardrobeGarments.id` when this image was produced + * by the solo-garment try-on flow (M4.1). Symmetric to + * wardrobeOutfitId — pictures are at most one of the two. Lets the + * garment detail view list its own try-ons without scanning by + * `referenceImageIds` containment. + */ + wardrobeGarmentId?: string | null; } export interface LocalBoard extends BaseRecord { @@ -122,6 +130,7 @@ export interface Image { referenceImageIds?: string[]; generationMode?: ImageGenerationMode; wardrobeOutfitId?: string; + wardrobeGarmentId?: string; createdAt: string; updatedAt: string; } diff --git a/apps/mana/apps/web/src/lib/modules/wardrobe/api/try-on.ts b/apps/mana/apps/web/src/lib/modules/wardrobe/api/try-on.ts index 25b6d294e..627392b96 100644 --- a/apps/mana/apps/web/src/lib/modules/wardrobe/api/try-on.ts +++ b/apps/mana/apps/web/src/lib/modules/wardrobe/api/try-on.ts @@ -303,11 +303,13 @@ export async function runGarmentTryOn(params: RunGarmentTryOnParams): Promise(async () => { + if (!garmentId) return []; + const locals = await scopedForModule('picture', 'images') + .and((row) => row.wardrobeGarmentId === garmentId) + .toArray(); + const visible = locals + .filter((row) => !row.deletedAt && !row.isArchived) + .sort((a, b) => (b.createdAt ?? '').localeCompare(a.createdAt ?? '')); + const decrypted = await decryptRecords('images', visible); + return decrypted.map(toImage); + }, [] as Image[]); +} + +/** + * All outfits in the active space that include the given garment, + * newest first. Used on the garment detail page to render a "Teil + * dieser Outfits"-Strip so the user can jump back into any outfit + * they've built around the item — each outfit's own `lastTryOn` + * snapshot provides the thumbnail without another image lookup. + */ +export function useOutfitsContainingGarment(garmentId: string | null) { + return useLiveQueryWithDefault(async () => { + if (!garmentId) return []; + const locals = await scopedForModule( + 'wardrobe', + 'wardrobeOutfits' + ).toArray(); + const visible = locals + .filter( + (row) => !row.deletedAt && !row.isArchived && (row.garmentIds ?? []).includes(garmentId) + ) + .sort((a, b) => (b.createdAt ?? '').localeCompare(a.createdAt ?? '')); + const decrypted = await decryptRecords('wardrobeOutfits', visible); + return decrypted.map(toOutfit); + }, [] as Outfit[]); +} diff --git a/apps/mana/apps/web/src/lib/modules/wardrobe/views/DetailGarmentView.svelte b/apps/mana/apps/web/src/lib/modules/wardrobe/views/DetailGarmentView.svelte index b748bb170..96767dd38 100644 --- a/apps/mana/apps/web/src/lib/modules/wardrobe/views/DetailGarmentView.svelte +++ b/apps/mana/apps/web/src/lib/modules/wardrobe/views/DetailGarmentView.svelte @@ -7,7 +7,7 @@