DESIGN: ready-made card gallery (occasion-filtered) — own media becomes one option #28
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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:
This is DESIGN-FIRST (inventor)
Real open questions to resolve before coding — m wants an inventor on it:
galleryschema/table?). How served + cached.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)
DESIGN (inventor, shift-1) — design doc filed
Design doc:
docs/plans/issue-28-readymade-gallery.md(committed onmai/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.{token}/{rand}.extkey — never references. Card stays self-contained; #21 unguessable keys, layouts, flip, admin, cleanup all work with zero special-casing.postcardsbucket (gallery under agallery/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 topostcard_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 offlinecmd/seed-galleryfeeding 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
/galleryendpoint, all occasions. P3: mix gallery+own, baked-in-text cards, on-demand regenerate.4 open questions → going to m now (AskUserQuestion)
Decisions will be folded into the doc; then DESIGN READY FOR REVIEW. No implementation until m's go/no-go.
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.
IMPLEMENTED — Phase-1 MVP built + verified e2e
m gave the go; otto-head flipped me to coder. Built exactly per the design doc. Commit
341242con branchmai/cronus/issue-28-design-ready(pushed). Not merged/deployed — otto-head handles that.What shipped
010_gallery.sql:gallery_assets(multi-tagoccasions[]GIN,source,active,position), owner-safe additive CREATE, same shape as 004/006.store.Readon both backends (local + supabase) — read-then-copy on pick.internal/postcard/gallery.go:GalleryAsset+ repo (occasion filter withallgemeincatch-all, by-id pick preserving order, CRUD, idempotent upsert by storage_key).postcard_images({token}/{rand}per #21 — never references the shared asset), wired into the #15 live preview. Server enforces the exclusive source./admin/gallerymanager 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 thegallery/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 testall 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/adminauth + the singlepostcardsbucket under agallery/prefix./admin/galleryis under the existingPathPrefix(/admin)Traefik claim — no new routing needed.Deploy notes (otto-head)
go run ./cmd/seed-gallery(or/seed-gallerybinary) 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.gois not gofmt-clean in the repo (predates this work; not in my diff). Left as-is to keep this commit scoped — worth a one-linegofmt -win a separate cleanup.Deferred to #29 (per head): ImaGen as a live third compose option.
✅ Merged, deployed & seeded — gallery live on sendmy.cards
Merged
341242c(merge3e330e1), deployed. Migration 010 applied. Rancmd/seed-galleryagainst prod → 6 ImaGen starter images loaded (Geburtstag ×2, Weihnachten ×2, Allgemein ×2 multi-tagged).Verified live e2e on sendmy.cards:
postcards/{token}/{rand}.jpg(tokenized per #21), not thegallery/seed/key./admin/gallerylists + manages assets behind the existing admin auth (under the/adminTraefik 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.gonot gofmt-clean (predates #28) — worth a one-line cleanup sometime.