diff --git a/apps/mana/apps/web/src/lib/modules/profile/ListView.svelte b/apps/mana/apps/web/src/lib/modules/profile/ListView.svelte
index 6e5d0468e..5da06a545 100644
--- a/apps/mana/apps/web/src/lib/modules/profile/ListView.svelte
+++ b/apps/mana/apps/web/src/lib/modules/profile/ListView.svelte
@@ -211,6 +211,12 @@
+
goto('/profile/me-images')}>
+ Meine Bilder
+
+ Gesichts- und Ganzkörperbilder für KI-Bildgenerierung
+
+
(showEditModal = true)}>
Profil bearbeiten
@@ -365,7 +371,8 @@
}
.account-btn {
display: flex;
- align-items: center;
+ flex-direction: column;
+ align-items: flex-start;
width: 100%;
padding: 0.625rem 1rem;
border: 1px solid hsl(var(--color-border));
@@ -375,6 +382,12 @@
font-size: 0.8125rem;
cursor: pointer;
transition: background 0.15s;
+ text-align: left;
+ }
+ .account-btn-hint {
+ margin-top: 0.125rem;
+ font-size: 0.75rem;
+ color: hsl(var(--color-muted-foreground));
}
.account-btn:hover {
background: hsl(var(--color-surface-hover));
diff --git a/apps/mana/apps/web/src/lib/modules/profile/MeImagesView.svelte b/apps/mana/apps/web/src/lib/modules/profile/MeImagesView.svelte
new file mode 100644
index 000000000..cd07eb571
--- /dev/null
+++ b/apps/mana/apps/web/src/lib/modules/profile/MeImagesView.svelte
@@ -0,0 +1,194 @@
+
+
+
+
+
+
+
+
+
Meine Bilder
+
+
+ Hinterlege hier ein Gesichts- und ein Ganzkörper-Bild sowie weitere Referenzen. Die
+ Bildgenerierung nutzt diese später, um dich selbst zu visualisieren — etwa um Outfits, Brillen
+ oder Frisuren anzuprobieren.
+
+
+
+
+ Pro Bild entscheidest du mit dem "KI darf nutzen"-Schalter, ob es an den Bildgenerator
+ gesendet werden darf. Ohne diesen Schalter bleibt das Bild nur für dich sichtbar.
+
+
+
+
+ {#if uploadError}
+
+ {uploadError}
+
+ {/if}
+
+
+
+ ingestFiles(files, kind, slot)}
+ onToggleAi={handleToggleAi}
+ onDelete={handleDelete}
+ />
+ ingestFiles(files, kind, slot)}
+ onToggleAi={handleToggleAi}
+ onDelete={handleDelete}
+ />
+
+
+
+
+
+
+ Weitere Bilder
+
+ {#if extraImages.length > 0}
+
+ {extraImages.length}
+ {extraImages.length === 1 ? 'Bild' : 'Bilder'}
+
+ {/if}
+
+
+ {#if extraImages.length > 0}
+
+ {#each extraImages as img (img.id)}
+ handleToggleAi(img.id, v)}
+ onTogglePrimary={() => handleTogglePrimary(img)}
+ onDelete={() => handleDelete(img.id)}
+ />
+ {/each}
+
+ {/if}
+
+
+ ingestFiles(files, 'reference')}
+ />
+
+
+
diff --git a/apps/mana/apps/web/src/lib/modules/profile/api/me-images.ts b/apps/mana/apps/web/src/lib/modules/profile/api/me-images.ts
new file mode 100644
index 000000000..de8aa3892
--- /dev/null
+++ b/apps/mana/apps/web/src/lib/modules/profile/api/me-images.ts
@@ -0,0 +1,61 @@
+/**
+ * Client for `POST /api/v1/profile/me-images/upload` — the M1 endpoint
+ * that wraps mana-media (CAS dedup + thumbnails) with auth.
+ *
+ * Returns what the Dexie row needs: mediaId, storagePath, publicUrl,
+ * thumbnailUrl. Dimensions are read client-side so the call site can
+ * stamp width/height on the LocalMeImage without waiting for
+ * mana-media's async processing pass.
+ */
+
+import { getManaApiUrl } from '$lib/api/config';
+import { authStore } from '$lib/stores/auth.svelte';
+
+export interface UploadMeImageResult {
+ mediaId: string;
+ storagePath: string;
+ publicUrl: string;
+ thumbnailUrl?: string;
+}
+
+export async function uploadMeImageFile(file: File): Promise
{
+ const token = await authStore.getValidToken();
+ const formData = new FormData();
+ formData.append('file', file);
+
+ const response = await fetch(`${getManaApiUrl()}/api/v1/profile/me-images/upload`, {
+ method: 'POST',
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
+ body: formData,
+ });
+
+ if (!response.ok) {
+ const body = await response.json().catch(() => ({ error: `HTTP ${response.status}` }));
+ throw new Error(body.error || `Upload failed (${response.status})`);
+ }
+
+ return response.json() as Promise;
+}
+
+/**
+ * Read the natural dimensions of an image file client-side. mana-media
+ * also reports dimensions post-processing, but we want them synchronously
+ * so the Dexie row lands with `width` and `height` populated on first
+ * write — that lets the UI pick the right aspect-ratio tile immediately
+ * instead of re-flowing once the server catches up.
+ */
+export function readImageDimensions(file: File): Promise<{ width: number; height: number } | null> {
+ return new Promise((resolve) => {
+ const url = URL.createObjectURL(file);
+ const img = new Image();
+ img.onload = () => {
+ URL.revokeObjectURL(url);
+ resolve({ width: img.naturalWidth, height: img.naturalHeight });
+ };
+ img.onerror = () => {
+ URL.revokeObjectURL(url);
+ resolve(null);
+ };
+ img.src = url;
+ });
+}
diff --git a/apps/mana/apps/web/src/lib/modules/profile/components/MeImageSlotCard.svelte b/apps/mana/apps/web/src/lib/modules/profile/components/MeImageSlotCard.svelte
new file mode 100644
index 000000000..21a0b211f
--- /dev/null
+++ b/apps/mana/apps/web/src/lib/modules/profile/components/MeImageSlotCard.svelte
@@ -0,0 +1,103 @@
+
+
+
+
+
+ {title}
+ {#if image}
+
+ Primär
+
+ {/if}
+
+
+ {#if image}
+
+
+
+
+
+
+
+ KI darf nutzen
+ onToggleAi(image.id, v)}
+ size="sm"
+ />
+
+ onDelete(image.id)}
+ aria-label="Bild löschen"
+ title="Bild löschen"
+ class="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
+ >
+
+
+
+
+
+
+ onFiles(files, kind, slot)}
+ />
+
+ {:else}
+ onFiles(files, kind, slot)}
+ />
+ {/if}
+
diff --git a/apps/mana/apps/web/src/lib/modules/profile/components/MeImageTile.svelte b/apps/mana/apps/web/src/lib/modules/profile/components/MeImageTile.svelte
new file mode 100644
index 000000000..9301bc438
--- /dev/null
+++ b/apps/mana/apps/web/src/lib/modules/profile/components/MeImageTile.svelte
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+ {#if image.thumbnailUrl || image.publicUrl}
+
+ {/if}
+
+
+
+ {KIND_LABELS[image.kind] ?? image.kind}
+
+
+
+ {#if canBePrimary}
+
+
+
+ {/if}
+
+
+
+
+
+
+ KI darf nutzen
+ onToggleAi(v)} size="sm" />
+
+
+
+
+
+
diff --git a/apps/mana/apps/web/src/lib/modules/profile/components/MeImageUploadZone.svelte b/apps/mana/apps/web/src/lib/modules/profile/components/MeImageUploadZone.svelte
new file mode 100644
index 000000000..eb9437322
--- /dev/null
+++ b/apps/mana/apps/web/src/lib/modules/profile/components/MeImageUploadZone.svelte
@@ -0,0 +1,89 @@
+
+
+
+ fileInput?.click()}
+ ondrop={handleDrop}
+ ondragover={handleDragOver}
+ ondragleave={handleDragLeave}
+ class="group relative flex w-full flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed transition-colors
+ {variant === 'large' ? 'min-h-[220px] p-6' : 'min-h-[120px] p-4'}
+ {dragActive
+ ? 'border-primary bg-primary/5 text-primary'
+ : 'border-border text-muted-foreground hover:border-primary/60 hover:text-foreground'}
+ {disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}"
+>
+
+ {label}
+ {#if hint}
+ {hint}
+ {/if}
+
+
+
diff --git a/apps/mana/apps/web/src/routes/(app)/profile/me-images/+page.svelte b/apps/mana/apps/web/src/routes/(app)/profile/me-images/+page.svelte
new file mode 100644
index 000000000..425a3a4f4
--- /dev/null
+++ b/apps/mana/apps/web/src/routes/(app)/profile/me-images/+page.svelte
@@ -0,0 +1,12 @@
+
+
+
+ Meine Bilder · Mana
+
+
+
+
+
diff --git a/docs/plans/me-images-and-reference-generation.md b/docs/plans/me-images-and-reference-generation.md
index 51fc4278e..d93c36045 100644
--- a/docs/plans/me-images-and-reference-generation.md
+++ b/docs/plans/me-images-and-reference-generation.md
@@ -58,7 +58,7 @@ Jedes `meImage` hat ein `usage.aiReference: boolean` Flag. Default beim Upload:
```
┌─ Client (SvelteKit) ────────────────────────────────────┐
-│ /settings/me-images (Upload + Toggles) │
+│ /profile/me-images (Upload + Toggles) │
│ picture/GeneratorForm (Reference-Picker) │
│ Dexie: meImages (encrypted label/tags/kind) │
└──────┬──────────────────────────────────────────────────┘
@@ -205,7 +205,7 @@ Python/FastAPI-Seite bekommt einen `POST /edit` Endpoint, der IP-Adapter oder Pu
## UI: zwei Touchpoints
-### 1. `/settings/me-images` (neu)
+### 1. `/profile/me-images` (neu)
- 2 prominente Slots oben: **Gesicht** (quadratisch, 512×512 empfohlen) und **Ganzkörper** (portrait, min 1024 hoch)
- Darunter Grid für zusätzliche Referenzen (Drag-and-Drop, Multi-Select-Upload — Pattern aus `picture/ListView.svelte:165-217` klauen)
@@ -295,7 +295,7 @@ Soft-first/Hard-follow-up-Regel (siehe Memory):
- [ ] Sync-Schema registrieren
- [ ] Upload-Wrapper nutzt bestehenden `picture/upload`-Endpoint mit `app=me` (neuer Bucket `me-storage` in MinIO)
-- **M2 — UI Route `/settings/me-images`** (~1 Tag)
+- **M2 — UI Route `/profile/me-images`** (~1 Tag)
- [ ] Route + ModuleShell-Wrapping (wie andere Settings-Routen)
- [ ] Slot-Komponenten für Face/Fullbody, Grid für Reste
- [ ] Drag-and-Drop-Upload + Multi-File
@@ -342,7 +342,7 @@ Soft-first/Hard-follow-up-Regel (siehe Memory):
3. **OpenAI Ref-Image-Format**: Original-Format durchreichen (PNG/JPG/WEBP — OpenAI akzeptiert alle). Keine Server-Konvertierung.
4. **Credit-Kosten für Multi-Ref-Edits**: identisch zu `/generate`, pro Output-Bild, unabhängig von Reference-Anzahl.
5. **`profile.aiUsesReferenceImages`-Default**: `true` (globaler Panic-Kill-Switch; Pro-Bild-Opt-in ist die eigentliche Hürde).
-6. **Alter Avatar-Upload-Pfad**: bleibt in M1 unangetastet; M2 biegt `EditProfileModal` auf `/settings/me-images` um und räumt den toten Endpoint-Call weg.
+6. **Alter Avatar-Upload-Pfad**: bleibt in M1 unangetastet; M2 biegt `EditProfileModal` auf `/profile/me-images` um und räumt den toten Endpoint-Call weg.
## Verweise