mAi: #7 - cloud-sync to Supabase Storage + imagen.images

Every successful imagen generate now (a) uploads the PNG to the private
imagen-generated bucket and (b) inserts a row into imagen.images, the
data plane the flexsiebels owner-mode viewer reads from.

Schema, RLS, indexes, bucket and PostgREST exposure landed via four
applied migrations on msupabase: imagen_schema_init,
imagen_schema_grants, imagen_storage_policies, imagen_pgrst_expose
(authenticator role-level ALTER + reload). Owner UUID for m:
ac6c9501-3757-4a6d-8b97-2cff4288382b — documented in the config sample.

Code: new internal/cloud/ package mirroring the internal/usage/ shape.
PostgREST POST against the imagen schema (Accept-Profile + Content-
Profile headers), Storage upload via PUT with x-upsert, retry on 5xx /
transport but not 4xx, owner_user_id required (the column is NOT NULL
and the read-side RLS policy needs it).

Wiring in cmd/imagen/generate.go: --no-cloud flag, output.cloud_sync
config knob (auto|on|off mirroring --preview), $IMAGEN_CLOUD_SYNC env
override. The hook reads the just-written PNG + sidecar from disk and
calls cloud.Sync; failures emit "imagen: cloud sync: <err>" to stderr
without changing exit code, so a Supabase blip never loses the artefact.
output.Outputs grew Date/Slug/Seed fields so storage_path mirrors the
local filename's prefix exactly (no UTC-vs-local drift).

Config: owner_user_id field added; sample comment points at the
auth.users lookup. imagen config validate warns on stderr when
cloud_sync is on/auto but owner_user_id is empty.

Tests: cloud_test.go covers happy path, retry-on-5xx, no-retry-on-4xx,
missing-owner-uuid, missing-date-or-slug, signed URL, and the partial-
success case where the upload landed but the DB insert failed.
generate_test.go covers the precedence chain for cloud-sync mode
resolution. Build + tests clean across the tree.

Real smoke against mRock: generation through flux-schnell-local writes
the local PNG + sidecar AND uploads to imagen-generated/2026-05-11/...
AND inserts into imagen.images. Signed URL round-trips the same bytes.
--no-cloud verified to skip both Storage and DB.
This commit is contained in:
mAi
2026-05-11 01:51:09 +02:00
parent 2d5896e27d
commit e22f286024
10 changed files with 1013 additions and 4 deletions

View File

@@ -15,6 +15,10 @@ import (
// Config is the top-level shape of imagen.yaml.
type Config struct {
DefaultBackend string `yaml:"default_backend"`
// OwnerUserID is m's auth.users.id on msupabase. The cloud-sync writer
// uses it to populate imagen.images.owner_user_id (NOT NULL, owns RLS).
// Empty disables DB inserts even when cloud_sync is on.
OwnerUserID string `yaml:"owner_user_id"`
Output OutputConfig `yaml:"output"`
Backends map[string]BackendSpec `yaml:"backends"`
}
@@ -29,6 +33,11 @@ type OutputConfig struct {
// Empty / unset is treated as "auto". $IMAGEN_PREVIEW and the
// --preview/--no-preview flags override this in turn.
Preview string `yaml:"preview"`
// CloudSync controls whether successful generations also upload to
// Supabase Storage and insert into imagen.images. Tri-state mirroring
// Preview: "auto" (default — on when SUPABASE_URL + SUPABASE_SERVICE_KEY
// are set), "on" (errors if env unset), "off". --no-cloud overrides.
CloudSync string `yaml:"cloud_sync"`
}
// BackendSpec is one entry under `backends:`. Type identifies the adapter;
@@ -88,6 +97,11 @@ func (c *Config) Validate() error {
default:
return fmt.Errorf("output.preview = %q (must be auto|on|off)", c.Output.Preview)
}
switch c.Output.CloudSync {
case "", "auto", "on", "off":
default:
return fmt.Errorf("output.cloud_sync = %q (must be auto|on|off)", c.Output.CloudSync)
}
for name, spec := range c.Backends {
if name == "" {
return errors.New("empty backend name")
@@ -107,6 +121,11 @@ const Sample = `# imagen.yaml — config for the imagen CLI.
default_backend: flux-schnell-local
# Owner UUID for the cloud-sync row in imagen.images. Look up via:
# SELECT id FROM auth.users WHERE email = '<your-supabase-email>';
# Empty disables imagen.images inserts even when cloud_sync is on.
owner_user_id: ""
output:
directory: ~/Pictures/imagen
naming: "{date}-{slug}-{seed}.png"
@@ -116,6 +135,13 @@ output:
# on: always preview (errors outside a tmux session).
# off: never preview (use this for batch / CI callers).
preview: auto
# Sync the PNG to Supabase Storage (bucket: imagen-generated) and insert
# a row into imagen.images. Reads SUPABASE_URL + SUPABASE_SERVICE_KEY
# from env (same as mai.imagen_usage cost-tracking).
# auto (default): on iff env is configured AND owner_user_id is set.
# on: always upload (errors if env or owner_user_id is missing).
# off: never upload. --no-cloud also forces off per-call.
cloud_sync: auto
backends:
flux-schnell-local: