DESIGN: ready-made card gallery (occasion-filtered) — own media becomes one option #28

Closed
opened 2026-06-18 10:55:34 +00:00 by mAi · 4 comments
Collaborator

Intent (m, PWA)

m: „Vielleicht können wir die eigenen Bilder/Videos als nur eine Option anbieten und sonst auch fertige Bilder / Grußkarten anbieten? Die müssten wir aus einer Galerie auswählen können, die dann nach dem Anlass gefiltert ist.“

So the compose flow shifts from upload-only to a choice:

  • eigene Medien (today's upload — images/video) becomes one option, AND
  • fertige Bilder / Grußkarten pickable from a curated gallery, filtered by occasion (reuse the #10 occasion taxonomy: Geburtstag, Weihnachten, Einladung, …).

This is DESIGN-FIRST (inventor)

Real open questions to resolve before coding — m wants an inventor on it:

  1. Where do the ready-made assets come from? The core decision. Options: licensed stock; AI-generated via the in-house ImaGen (we have it — FLUX on mRock); m-curated uploads; a seeded starter set. Licensing/quality/effort tradeoffs. (ImaGen is attractive: on-brand, free, occasion-themed generation on demand.)
  2. Data model: a gallery-asset entity (image URL + occasion tag(s) + title/credit + active flag). Where stored (Supabase Storage bucket? a separate gallery schema/table?). How served + cached.
  3. Compose UX: how the 'eigene Medien | aus Galerie wählen' choice presents; the occasion filter; how a picked gallery image becomes the card's image; can you combine a gallery background + own text; interaction with layouts (single/gallery/flip) + the live preview (#15).
  4. Curation/admin: how the gallery is populated + managed (extend the #19 admin? a seed script? ImaGen pipeline?).
  5. Scope of MVP vs later (e.g. start with a small seeded set for 2-3 occasions, expand later).

Constraints to respect

Mobile-first, German + Umlaute, renamable (BASE_URL). Coexists with everything shipped (layouts, video, broadcast, slugs+lifecycle, password+tokenized media, occasions, Anrede, admin). Owner-safe migrations if schema added.

Process

Inventor designs first → otto-head reviews → m go/no-go → then flip to coder. Do NOT start implementation before m approves the design. Inventor: produce a design doc (docs/plans/) + an issue summary, end with 'DESIGN READY FOR REVIEW'.

DoD (design phase)

  • A design doc covering: asset source decision (with recommendation), data model, compose UX, curation/admin, MVP scope + phasing. Filed in docs/plans/ + summarized here. Then parked for m's go/no-go.
## Intent (m, PWA) m: *„Vielleicht können wir die eigenen Bilder/Videos als nur eine Option anbieten und sonst auch fertige Bilder / Grußkarten anbieten? Die müssten wir aus einer Galerie auswählen können, die dann nach dem Anlass gefiltert ist.“* So the compose flow shifts from *upload-only* to a choice: - **eigene Medien** (today's upload — images/video) becomes **one** option, AND - **fertige Bilder / Grußkarten** pickable from a **curated gallery**, **filtered by occasion** (reuse the #10 occasion taxonomy: Geburtstag, Weihnachten, Einladung, …). ## This is DESIGN-FIRST (inventor) Real open questions to resolve before coding — m wants an inventor on it: 1. **Where do the ready-made assets come from?** The core decision. Options: licensed stock; **AI-generated via the in-house ImaGen** (we have it — FLUX on mRock); m-curated uploads; a seeded starter set. Licensing/quality/effort tradeoffs. (ImaGen is attractive: on-brand, free, occasion-themed generation on demand.) 2. **Data model**: a gallery-asset entity (image URL + occasion tag(s) + title/credit + active flag). Where stored (Supabase Storage bucket? a separate `gallery` schema/table?). How served + cached. 3. **Compose UX**: how the 'eigene Medien | aus Galerie wählen' choice presents; the occasion filter; how a picked gallery image becomes the card's image; can you combine a gallery background + own text; interaction with layouts (single/gallery/flip) + the live preview (#15). 4. **Curation/admin**: how the gallery is populated + managed (extend the #19 admin? a seed script? ImaGen pipeline?). 5. **Scope of MVP** vs later (e.g. start with a small seeded set for 2-3 occasions, expand later). ## Constraints to respect Mobile-first, German + Umlaute, renamable (BASE_URL). Coexists with everything shipped (layouts, video, broadcast, slugs+lifecycle, password+tokenized media, occasions, Anrede, admin). Owner-safe migrations if schema added. ## Process **Inventor designs first → otto-head reviews → m go/no-go → then flip to coder.** Do NOT start implementation before m approves the design. Inventor: produce a design doc (docs/plans/) + an issue summary, end with 'DESIGN READY FOR REVIEW'. ## DoD (design phase) - A design doc covering: asset source decision (with recommendation), data model, compose UX, curation/admin, MVP scope + phasing. Filed in docs/plans/ + summarized here. Then parked for m's go/no-go.
Author
Collaborator

DESIGN (inventor, shift-1) — design doc filed

Design doc: docs/plans/issue-28-readymade-gallery.md (committed on mai/cronus/issue-28-design-ready).

Grounded against live code

  • store.PublicURL(key) derives a URL from any storage key → a gallery asset is just a stored object; rendering needs no new render code.
  • Admin-delete (#19) removes objects by their stored keys → a referenced shared gallery key would be destroyed when a card is deleted. So a gallery pick COPIES bytes into a fresh per-card {token}/{rand}.ext key — never references. Card stays self-contained; #21 unguessable keys, layouts, flip, admin, cleanup all work with zero special-casing.
  • Occasion picker sits before the media field → the gallery auto-filters to the chosen occasion.
  • Live preview (#15) + single public postcards bucket (gallery under a gallery/ prefix) reused.

Core recommendation — asset source

ImaGen (FLUX on mRock) as the primary feed, admin-upload always available, source-agnostic storage. Generation is offline/batch (seed script or /imagine), never in the compose path — a sender is never blocked on mRock. On-brand, occasion-themed, zero licensing/attribution, free. Stock APIs were rejected (mandatory attribution + external dependency + generic look).

Data model

One additive idempotent migration 010_gallery.sql: gallery_assets(id, storage_key, content_type, title, credit, source, occasions text[], active, position, created_at). occasions text[] multi-tag (GIN) so one picture = one row across occasions; allgemein = catch-all shown under every occasion. No change to postcard_images.

Compose UX

A source toggle wraps the existing media field: ◉ Eigene Medien | ○ Aus Galerie wählen. The gallery grid is occasion-pre-filtered, tap-to-select, multi-pick, feeds the same live preview. Text/Anrede/Von unchanged — a gallery image is the card's picture, the sender's words sit with it as today.

Curation/admin

Extends #19 /admin/admin/gallery: list, active toggle, multi-tag edit, delete, upload. ImaGen batch is an offline cmd/seed-gallery feeding the same table (generate→inactive→m approves→active).

MVP phasing

P1: migration + repo + source toggle + occasion grid + copy-on-pick + admin gallery + a seeded starter set + E2E. P2: ImaGen batch cmd, lazy /gallery endpoint, all occasions. P3: mix gallery+own, baked-in-text cards, on-demand regenerate.

4 open questions → going to m now (AskUserQuestion)

  1. Asset source (rec ImaGen+upload). 2. Karten-Art: background-image+own-text (rec) vs complete designed card. 3. Mix gallery+own per card vs exclusive source (rec exclusive for MVP). 4. Which occasions to seed first (rec Geburtstag+Weihnachten+Allgemein).

Decisions will be folded into the doc; then DESIGN READY FOR REVIEW. No implementation until m's go/no-go.

## DESIGN (inventor, shift-1) — design doc filed Design doc: `docs/plans/issue-28-readymade-gallery.md` (committed on `mai/cronus/issue-28-design-ready`). ### Grounded against live code - `store.PublicURL(key)` derives a URL from **any** storage key → a gallery asset is just a stored object; rendering needs **no** new render code. - Admin-delete (#19) removes objects by their stored keys → a *referenced* shared gallery key would be **destroyed** when a card is deleted. **So a gallery pick COPIES bytes into a fresh per-card `{token}/{rand}.ext` key — never references.** Card stays self-contained; #21 unguessable keys, layouts, flip, admin, cleanup all work with zero special-casing. - Occasion picker sits **before** the media field → the gallery **auto-filters** to the chosen occasion. - Live preview (#15) + single public `postcards` bucket (gallery under a `gallery/` prefix) reused. ### Core recommendation — asset source **ImaGen (FLUX on mRock) as the primary feed, admin-upload always available, source-agnostic storage.** Generation is **offline/batch** (seed script or `/imagine`), never in the compose path — a sender is never blocked on mRock. On-brand, occasion-themed, zero licensing/attribution, free. Stock APIs were rejected (mandatory attribution + external dependency + generic look). ### Data model One additive idempotent migration `010_gallery.sql`: `gallery_assets(id, storage_key, content_type, title, credit, source, occasions text[], active, position, created_at)`. `occasions text[]` multi-tag (GIN) so one picture = one row across occasions; `allgemein` = catch-all shown under every occasion. **No change to `postcard_images`.** ### Compose UX A source toggle wraps the existing media field: **◉ Eigene Medien | ○ Aus Galerie wählen**. The gallery grid is occasion-pre-filtered, tap-to-select, multi-pick, feeds the same live preview. Text/Anrede/Von unchanged — a gallery image is the card's *picture*, the sender's words sit with it as today. ### Curation/admin Extends #19 `/admin` → `/admin/gallery`: list, active toggle, multi-tag edit, delete, **upload**. ImaGen batch is an offline `cmd/seed-gallery` feeding the same table (generate→inactive→m approves→active). ### MVP phasing P1: migration + repo + source toggle + occasion grid + copy-on-pick + admin gallery + a seeded starter set + E2E. P2: ImaGen batch cmd, lazy `/gallery` endpoint, all occasions. P3: mix gallery+own, baked-in-text cards, on-demand regenerate. ### 4 open questions → going to m now (AskUserQuestion) 1. Asset source (rec ImaGen+upload). 2. Karten-Art: background-image+own-text (rec) vs complete designed card. 3. Mix gallery+own per card vs exclusive source (rec exclusive for MVP). 4. Which occasions to seed first (rec Geburtstag+Weihnachten+Allgemein). Decisions will be folded into the doc; then **DESIGN READY FOR REVIEW**. No implementation until m's go/no-go.
Author
Collaborator

m: GO (PWA, 2026-06-18). Design approved — proceed to implementation (same worker cronus, inventor→coder).

Asset source confirmed: ImaGen-generated feed + admin-upload, source-agnostic (as recommended). Starter set: seed a few ImaGen generations (m: „für den Anfang nehmen wir einfach ein paar Generationen“) across a couple of occasions — keep it small for MVP.

Scope = the design doc's Phase-1 MVP (gallery_assets table + migration 010, gallery/ storage prefix, store.Read/copy, compose source toggle 'Eigene Medien | Aus Galerie' occasion-filtered, copy-on-pick into postcard_images, live-preview integration, /admin/gallery with upload, seed a few ImaGen assets, e2e).

Deferred to #29 (m's enhancement): ImaGen as a direct, live third compose option ('Bild generieren') — the sender enters a prompt and generates on the fly. The design intentionally kept live generation OUT of the request path (FLUX latency = seconds–minutes), so this needs its own async UX (generate→poll→use) and is a fast-follow, not part of this MVP.

**m: GO (PWA, 2026-06-18).** Design approved — proceed to implementation (same worker cronus, inventor→coder). **Asset source confirmed:** ImaGen-generated feed + admin-upload, source-agnostic (as recommended). **Starter set: seed a few ImaGen generations** (m: „für den Anfang nehmen wir einfach ein paar Generationen“) across a couple of occasions — keep it small for MVP. **Scope = the design doc's Phase-1 MVP** (gallery_assets table + migration 010, gallery/ storage prefix, store.Read/copy, compose source toggle 'Eigene Medien | Aus Galerie' occasion-filtered, copy-on-pick into postcard_images, live-preview integration, /admin/gallery with upload, seed a few ImaGen assets, e2e). **Deferred to #29 (m's enhancement):** ImaGen as a **direct, live third compose option** ('Bild generieren') — the sender enters a prompt and generates on the fly. The design intentionally kept live generation OUT of the request path (FLUX latency = seconds–minutes), so this needs its own async UX (generate→poll→use) and is a fast-follow, not part of this MVP.
Author
Collaborator

IMPLEMENTED — Phase-1 MVP built + verified e2e

m gave the go; otto-head flipped me to coder. Built exactly per the design doc. Commit 341242c on branch mai/cronus/issue-28-design-ready (pushed). Not merged/deployed — otto-head handles that.

What shipped

  • Migration 010_gallery.sql: gallery_assets (multi-tag occasions[] GIN, source, active, position), owner-safe additive CREATE, same shape as 004/006.
  • store.Read on both backends (local + supabase) — read-then-copy on pick.
  • Domain internal/postcard/gallery.go: GalleryAsset + repo (occasion filter with allgemein catch-all, by-id pick preserving order, CRUD, idempotent upsert by storage_key).
  • Compose: exclusive source toggle (Eigene Medien | Aus Galerie), occasion-filtered grid (embedded JSON, client-filtered, re-filters live on occasion change), copy-on-pick into postcard_images ({token}/{rand} per #21 — never references the shared asset), wired into the #15 live preview. Server enforces the exclusive source.
  • /admin/gallery manager behind the existing admin auth: upload, edit tags/title/credit, active toggle, delete (server-rendered confirm). Linked from the card admin bar.
  • cmd/seed-gallery: idempotent seeder, embeds 6 ImaGen-generated starter images (FLUX on mRock) across Geburtstag + Weihnachten + Allgemein.

The load-bearing guarantee (verified)

Deleting a card removes only its own copy → catalogue stays intact. Deleting a gallery asset never breaks cards already composed from it. E2E proved: seed → compose-via-gallery → render (img is /uploads/{token}/{rand}.jpg, not the gallery/seed/ key) → admin manage → delete card leaves all 6 seed objects → exclusivity guard (upload-source ignores stray gallery picks → 0 images) → empty gallery submit = clean 400.

Quality

go build / go vet / go test all green. Unit tests (occasion-tag normalize, source normalize, HasOccasion) + render tests (compose gallery section, no-gallery PE degrade, admin gallery + confirm). No new env vars; rides on existing /admin auth + the single postcards bucket under a gallery/ prefix. /admin/gallery is under the existing PathPrefix(/admin) Traefik claim — no new routing needed.

Deploy notes (otto-head)

  1. Merge + deploy as usual. 2. After deploy, run go run ./cmd/seed-gallery (or /seed-gallery binary) once against prod env to load the starter set — idempotent, safe to re-run. 3. Ongoing curation via /admin/gallery.

Tech-debt flag (pre-existing, untouched)

internal/server/media_key_test.go is not gofmt-clean in the repo (predates this work; not in my diff). Left as-is to keep this commit scoped — worth a one-line gofmt -w in a separate cleanup.

Deferred to #29 (per head): ImaGen as a live third compose option.

## IMPLEMENTED — Phase-1 MVP built + verified e2e m gave the go; otto-head flipped me to coder. Built exactly per the design doc. Commit [`341242c`](https://mgit.msbls.de/m/postcards/commit/341242cabfb921326a081a6282ac05e56555eeef) on branch `mai/cronus/issue-28-design-ready` (pushed). **Not merged/deployed** — otto-head handles that. ### What shipped - **Migration** `010_gallery.sql`: `gallery_assets` (multi-tag `occasions[]` GIN, `source`, `active`, `position`), owner-safe additive CREATE, same shape as 004/006. - **`store.Read`** on both backends (local + supabase) — read-then-copy on pick. - **Domain** `internal/postcard/gallery.go`: `GalleryAsset` + repo (occasion filter with `allgemein` catch-all, by-id pick preserving order, CRUD, idempotent upsert by storage_key). - **Compose**: exclusive **source toggle** (Eigene Medien | Aus Galerie), occasion-filtered grid (embedded JSON, client-filtered, re-filters live on occasion change), **copy-on-pick** into `postcard_images` (`{token}/{rand}` per #21 — never references the shared asset), wired into the #15 live preview. Server **enforces** the exclusive source. - **`/admin/gallery`** manager behind the existing admin auth: upload, edit tags/title/credit, active toggle, delete (server-rendered confirm). Linked from the card admin bar. - **`cmd/seed-gallery`**: idempotent seeder, embeds **6 ImaGen-generated** starter images (FLUX on mRock) across **Geburtstag + Weihnachten + Allgemein**. ### The load-bearing guarantee (verified) Deleting a **card** removes only its own copy → catalogue stays intact. Deleting a **gallery asset** never breaks cards already composed from it. E2E proved: seed → compose-via-gallery → render (img is `/uploads/{token}/{rand}.jpg`, **not** the `gallery/seed/` key) → admin manage → delete card leaves all 6 seed objects → exclusivity guard (upload-source ignores stray gallery picks → 0 images) → empty gallery submit = clean 400. ### Quality `go build` / `go vet` / `go test` all green. Unit tests (occasion-tag normalize, source normalize, HasOccasion) + render tests (compose gallery section, no-gallery PE degrade, admin gallery + confirm). No new env vars; rides on existing `/admin` auth + the single `postcards` bucket under a `gallery/` prefix. `/admin/gallery` is under the existing `PathPrefix(/admin)` Traefik claim — no new routing needed. ### Deploy notes (otto-head) 1. Merge + deploy as usual. 2. After deploy, run `go run ./cmd/seed-gallery` (or `/seed-gallery` binary) once against prod env to load the starter set — idempotent, safe to re-run. 3. Ongoing curation via `/admin/gallery`. ### Tech-debt flag (pre-existing, untouched) `internal/server/media_key_test.go` is not gofmt-clean in the repo (predates this work; not in my diff). Left as-is to keep this commit scoped — worth a one-line `gofmt -w` in a separate cleanup. Deferred to #29 (per head): ImaGen as a *live* third compose option.
Author
Collaborator

Merged 341242c (merge 3e330e1), deployed. Migration 010 applied. Ran cmd/seed-gallery against prod → 6 ImaGen starter images loaded (Geburtstag ×2, Weihnachten ×2, Allgemein ×2 multi-tagged).

Verified live e2e on sendmy.cards:

  • Compose has the source toggle (Eigene Medien | Aus Galerie), occasion-filtered.
  • Picked a Geburtstag gallery asset → card renders a copy at postcards/{token}/{rand}.jpg (tokenized per #21), not the gallery/seed/ key.
  • The shared seed asset stays 200 after; deleting the test card leaves all 6 catalog assets intact — the copy-don't-reference guarantee holds.
  • /admin/gallery lists + manages assets behind the existing admin auth (under the /admin Traefik claim, no new routing).

build/vet/test green. Inventor→coder gate executed cleanly (design → m go/no-go → same worker flipped to coder). Not closing — m closes.

Deferred to #29: ImaGen as a live third compose option.
Minor pre-existing tech-debt flagged by the worker: internal/server/media_key_test.go not gofmt-clean (predates #28) — worth a one-line cleanup sometime.

## ✅ Merged, deployed & seeded — gallery live on sendmy.cards Merged `341242c` (merge `3e330e1`), deployed. Migration 010 applied. Ran `cmd/seed-gallery` against prod → **6 ImaGen starter images** loaded (Geburtstag ×2, Weihnachten ×2, Allgemein ×2 multi-tagged). **Verified live e2e on sendmy.cards:** - Compose has the source toggle (Eigene Medien | Aus Galerie), occasion-filtered. - Picked a Geburtstag gallery asset → card renders a **copy** at `postcards/{token}/{rand}.jpg` (tokenized per #21), **not** the `gallery/seed/` key. - The shared seed asset stays 200 after; deleting the test card leaves all **6** catalog assets intact — the copy-don't-reference guarantee holds. - `/admin/gallery` lists + manages assets behind the existing admin auth (under the `/admin` Traefik claim, no new routing). build/vet/test green. Inventor→coder gate executed cleanly (design → m go/no-go → same worker flipped to coder). Not closing — m closes. _Deferred to #29:_ ImaGen as a live third compose option. _Minor pre-existing tech-debt flagged by the worker:_ `internal/server/media_key_test.go` not gofmt-clean (predates #28) — worth a one-line cleanup sometime.
mAi closed this issue 2026-06-23 15:48:04 +00:00
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: m/postcards#28
No description provided.