- Template detail page now shows the render matching the SELECTED aspect (poster +
preview video) instead of the 16:9 cover cropped into a 9:16/1:1 box. TemplateVariant
carries per-aspect image/previewVideo; fetchTemplateVariants + the detail page wire them.
- AppShowcase3D ships a distinct preview video per aspect (seed PERASPECT_VIDEO).
- Frontend Dockerfile: Alpine -> node:20-slim (glibc). Fixes next-swc ("ld-linux..."
load failure that broke `next build` once libc6-compat was removed) AND the original
CI Alpine-CDN issue. Healthcheck switched to node (slim has no wget).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fr2-postgres failed to start after another local project's postgres grabbed host
port 5432 during downtime. The internal stack always connects via postgres:5432 on
the docker network, so the published host port is only for external tooling — make
it ${PG_HOST_PORT:-5532} to avoid the clash. (Also recovered from a stale bind-mount
where scripts/init-db.sh had become a directory; current compose mounts
deploy/postgres-initdb/.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- identity: when FlatPay (broker) is configured, InitiateZarinPalAsync
routes through pay.flatrender.ir instead of calling ZarinPal directly;
new HandleBrokerCallbackAsync confirms the payment via the broker
inquiry API (authoritative, not trusting the redirect) and activates
the plan. New public endpoint GET /v1/payments/callback/broker
(already public at the gateway via /callback/*). Env-gated — empty
FlatPay__ApiKey keeps the legacy direct-ZarinPal path.
- broker: deliver webhooks inline on enqueue (best-effort) in addition
to the retry loop, so clients credit near-instantly (db.GetWebhook +
goroutine kick).
- compose + ENV_FILE: FlatPay__* for identity (FLATPAY_FLATRENDER_*).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A generic multi-client payment gateway so FlatRender, meezi.ir and
bargevasat.ir can all pay through ZarinPal's single verified callback
domain (pay.flatrender.ir).
New Go service services/payment (clones the notification skeleton +
vendored deps):
- migration 31_payment_broker.sql — `payment` schema: client_apps,
transactions, webhook_deliveries.
- ZarinPal v4 client ported from the proven identity PaymentService
(request.json -> StartPay -> verify.json; codes 100/101).
- client API: POST /v1/pay/request + /v1/pay/inquiry, authed by
X-Api-Key + HMAC body signature; GET /callback/zarinpal (the single
verified endpoint) verifies, then 302s the user back to the site's
return_url (signed) and fires a signed, retried webhook.
- per-client ZarinPal merchant override (default = shared merchant);
amount stored canonically in Rial, unit to ZarinPal env-configurable.
- admin API /v1/admin/* (FlatRender admin JWT): client-app CRUD +
key issue/rotate + transactions list.
Deploy wiring: payment-svc in docker-compose.v2.yml (host port 1607),
pay.flatrender.ir server block in mirror-nginx conf, ENV_FILE +
README updates (cert SAN + manual migration note).
Admin UI: src/components/admin/PaymentsAdmin.tsx (client apps with
one-time key reveal + rotate, transactions table) + /admin/payments
page + nav link + fa/en strings; pay-admin proxy route to payment-svc.
Docs/SDK: deploy/PAYMENTS.md (integration contract) + deploy/sdk/flatpay.js
(zero-dep Node client + webhook verifier) for meezi/any site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The single-file bind mount ./scripts/init-db.sh left a stale empty dir in the
reused act_runner workspace → mounted as a directory → migrations never ran →
empty schemas → backend 28P01/connection failures. Move the init script to
deploy/postgres-initdb/00-init.sh and mount the whole DIR at
/docker-entrypoint-initdb.d (robust, like the migrations dir). Deploy checkout
now 'git clean -ffd' to purge stale workspace dirs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
soroushasadi mirror only has cached x86-64-v2 minio builds; Liara
(docker-mirror.liara.ir) back-fills the -cpuv1 variants. Confirmed pullable +
runs on the server CPU. MINIO_REGISTRY defaults to Liara, MINIO_IMAGE_TAG to a
real -cpuv1 release; dev overrides both to plain Docker Hub :latest.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The server CPU lacks x86-64-v2 (started being required at minio RELEASE.2023-11-01).
MinIO publishes '-cpuv1' variants compiled for plain x86-64. Pin to
RELEASE.2025-09-07T16-13-09Z-cpuv1 — same release as local dev, runs on the old CPU.
Override via MINIO_IMAGE_TAG (dev = latest).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Server VPS CPU lacks x86-64-v2; newer minio:latest is built for it and crash-loops
with 'Fatal glibc error: CPU does not support x86-64-v2'. Pin to a 2024 release that
runs on baseline x86-64 (override with MINIO_IMAGE_TAG if a different tag is on the
mirror). Local dev stays on :latest via .env.v2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Server's mirror minio:latest is newer than dev's cached RELEASE.2025-09-07 and
dropped the bundled mc client, so 'mc ready local' failed → fr2-minio unhealthy →
up aborted. Switch to MinIO's curl liveness endpoint with an mc fallback so it
works across image versions; bump start_period 10s→20s, retries 5→8.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Docker Hub blocks Iran IPs (403), so 'docker compose up' couldn't pull the base
infra images on the server even though all service images built fine through the
mirror. Prefix them with ${INFRA_REGISTRY:-mirror.soroushasadi.com/} so they pull
through Nexus by default; set INFRA_REGISTRY= to use plain Docker Hub names.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The server's central mirror-nginx already owns 80/443 + manages TLS, so FlatRender
can't run its own Caddy there. Adapt the deploy to the host-port + reverse-proxy model:
- compose: Caddy moved behind `profiles: [edge]` (not started by default); frontend/
gateway/minio host ports are now EDGE_BIND + FRONTEND_PORT/GATEWAY_PORT/MINIO_PORT
(so they can avoid Gitea's :3000 etc.); postgres/render stay on HOST_BIND loopback.
- deploy/ENV_FILE.production.example: nginx model, pre-filled for flatrender.ir,
host ports 1600/1605/1610, no Caddy/ACME vars.
- deploy/mirror-nginx-flatrender.conf: ready-to-paste server blocks routing
flatrender.ir / api / storage → 171.22.25.73:{1600,1605,1610}.
- deploy/README.md: nginx integration + cert step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- .gitea/workflows/ci-cd.yml: frontend tsc check → self-hosted deploy job that
builds the full compose stack and brings it up behind Caddy. Locks
COMPOSE_PROJECT_NAME=flatrender (stable volumes), backs up the DB before each
deploy, health-waits gateway+frontend, no `down -v`.
- Route all package installs through mirror.soroushasadi.com:
frontend Dockerfile npm registry → NPM_REGISTRY build arg (Nexus default);
3× NuGet.Config (content/identity/studio) → HTTPS nuget-group (were a bare IP).
- Harden host ports: ${HOST_BIND:-0.0.0.0} prefix on postgres/minio/render/gateway/
frontend so prod (HOST_BIND=127.0.0.1) keeps them off the public internet — only
Caddy 80/443 is public. Dev (unset → 0.0.0.0) unchanged.
- render-svc MINIO_USE_SSL now env-driven (MINIO_HOST_USE_SSL) for HTTPS storage domain.
- deploy/ENV_FILE.production.example (the Gitea secret template) + deploy/README.md
(one-time setup + go-live checklist).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
C2 — real-AE scene snapshots on the node:
- node-agent: runner/snapshot.go RunSnapshot (aerender -comp <key> -s f -e f
→ findRenderedOutput → ffmpeg -frames:v 1 PNG); client ClaimSnapshot /
GetSnapshotUploadURL / ReportSnapshotResult / ReportSnapshotFail; snapshotLoop +
pollSnapshotOnce mirroring the scan loop (reuses the AE-exclusive lock).
- render-svc: GetSnapshotJobMeta + UploadURL handler presigns a PUT to the
public-read user-uploads bucket at snapshots/{project}/{scene}.png and returns a
permanent public_url (not the time-limited export presign); MINIO_UPLOAD_BUCKET +
MINIO_PUBLIC_URL config + compose env + /snapshot/:id/upload-url route.
Epic B — bind edited colours into the render:
- render-svc GetRenderBindings UNIONs studio.saved_shared_colors +
saved_scene_colors (type 'color') so the node writes them before render.
- node-agent binder.go routes type:"color" bindings into the bind-spec colors[]
array that bind.jsx already applies to the frshare colour layers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per-scene preview thumbnails for templates. Admin clicks "ساخت پیشنمایش
صحنهها" → one single-frame AE render per scene → content.scenes.snapshot_url
→ shown as a thumbnail in the admin scene list (and available to the studio).
- migration 30_render_snapshot_jobs.sql: render.snapshot_jobs (queued|running|
done|error, per scene, image_url).
- render-svc: db/snapshotjobs.go (EnqueueSceneSnapshots, List, Claim, SetResult
-> writes content.scenes.snapshot_url cross-schema, SetError); handlers/
snapshotjobs.go (admin POST/GET /v1/scene-snapshots/:project_id + node-facing
internal claim/result/fail); main.go routes; gateway route.
- devworker: RunSnapshots — fulfils snapshot jobs with a generated placeholder
PNG (data: URL, scene-key-tinted) so the flow is verifiable without an AE node.
Gated by RENDER_DEV_SNAPSHOTS (default off; never hijacks real render jobs).
- admin UI: ProjectScenes "generate snapshots" button (enqueue + poll + reload)
and a thumbnail (snapshot_url || image) per scene row.
Verified e2e via the dev mock: enqueue -> jobs run -> content.scenes.snapshot_url
populated -> scenes API returns it -> admin renders the thumbnail.
Remaining (C2): node-agent real-AE runner — claim snapshot, aerender -s0 -e0 ->
ffmpeg still -> upload to a PERMANENT URL (mirror file-svc, not the time-limited
export presign) -> post result. Needs a live AE node to build + verify.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Render — "stuck in Queued" fix:
- Jobs were created Queued and only a Windows AE node could claim them, so in the
dev stack (no node) they queued forever.
- New devworker package: in-process mock worker drives Queued jobs through the steps
with progress + live preview frames → Done. Enabled via RENDER_DEV_WORKER (default
true in compose; set false in prod where real nodes claim jobs).
- db: DevClaimNextQueued (atomic oldest-queued → Preparing) + UpdateJobStepProgress
- Verified live: a stuck job advanced Preparing→Done in ~10s with frontend polling.
Studio — predefined template structure:
- Projects are always copied from a template; structure is fixed. Users customise
existing layers, they don't add new ones.
- New studio-config flag ALLOW_ADD_LAYERS (false): StudioToolbar (add text/image/
video/shape) returns null; SceneEditSidebar "add text layer" button hidden.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Business rule: each user has a daily render limit. Admin-stop refunds the used
charge (not the user's fault); a user's own cancel does not.
- identity: ConsumeRenderChargeAsync / RefundRenderChargeAsync on DailyRemainRenderCount
with lazy daily reset (mig 24: daily_renders_reset_at). Convention: max=0 ⇒ UNLIMITED,
so existing 0/0 users keep rendering until an admin sets a real limit.
- identity InternalController (service-token): POST /v1/internal/render-charge/{consume,refund}
- render-svc: identityclient + on Create consume (block 429 when limit reached, fail-open
on identity outage); on admin Stop refund the job owner; user /cancel unchanged
- compose: IDENTITY_URL for render-svc, ServiceToken for identity-svc
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- /admin/files Media Library: drag-drop multi-upload, thumbnails, copy-URL, delete
- FileUploadField replaces raw URL inputs; new "image" field type in AdminResource;
wired into category image
- upload proxy /api/admin/files/upload: browser → Next → presigned PUT (server-side,
reaches minio:9000) → confirm → returns public URL
- user-uploads bucket is public-read; public base via NEXT_PUBLIC_MINIO_URL
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Frontend build args and runtime env no longer need NEXT_PUBLIC_SUPABASE_URL,
NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, STRIPE_SECRET_KEY,
STRIPE_WEBHOOK_SECRET — all replaced by V2 gateway. .env.v2.example updated to
reflect the current V2-only config.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PlansController had a class-level [Authorize] that gated the public
plans list, contradicting the gateway's optionalAuth on /plans. Mark
List/GetById [AllowAnonymous] and resolve tenant optionally so
anonymous callers receive global plans (purchase/current-plan stay
authenticated).
Frontend container stayed "unhealthy" because busybox wget resolves
localhost to IPv6 [::1] while the Next.js standalone server binds
IPv4 only. Use 127.0.0.1 in the healthcheck.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add full V2 architecture: identity, content, studio (.NET 10) and file,
render, notification, gateway (Go) services with vendored deps, plus DB
migrations, event/API contracts, and an init-db script.
Wire the Next.js frontend to the gateway: server-side JWT auth routes
(login/register/refresh/logout/me), gateway fetch helper, and session/
cookie/jwt helpers under src/lib.
Containerize the stack via docker-compose.v2.yml and per-service
Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and
MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via
next/font/local to avoid Google Fonts (geo-blocked).
Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>