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>
mirror.soroushasadi.com serves only cached images (node:20 resolved, golang:1.25
was 'not found' — too new to be cached, upstream can't back-fill). Point the Go
services' golang:1.25-alpine base at mirror.kargadan.ir per infra owner; alpine/
busybox/node/postgres/minio stay on soroushasadi (cached). GOPROXY already kargadan.
Docker Hub blocks Iran (403) on the BUILD base images too (golang/alpine/busybox/
node) once they fall out of cache. Prefix every Docker Hub FROM/COPY --from with
mirror.soroushasadi.com/ (MCR dotnet images are reachable, left as-is). Go builders
also set GOPROXY=mirror.kargadan.ir/repository/go-group/ + GOSUMDB=off so any module/
toolchain fetch avoids the geo-blocked proxy.golang.org.
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>
Lets an admin disable rendering when no render node is available — users can't
start new renders and see a localized "service unavailable until <date>" message.
- Admin → فارم رندر → موتور رندر (RenderEngineAdmin): on/off toggle + fa/en message
+ optional Jalali "until" date; saved as one `render_service` Website Setting
(jsonb) via /v1/settings — no backend change, no migration.
- lib/render-service.ts: fetchRenderServiceStatus (fail-open) + renderServiceMessage
(locale + appends the date).
- Enforcement: POST /api/render returns 503 {code:render_disabled, messages} when off;
studio render page reads GET /api/render/service on mount → disables "شروع رندر"
and shows the banner, and handles the 503 on click.
- i18n: appAdminLayout.renderEngine (fa+en, parity 1045/1045). tsc + next build clean.
Verified: disabled setting → /api/render/service returns enabled:false.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the "desktop only" gate on phones with real mobile editing layouts.
Shared:
- BottomSheet (mobile slide-up panel) hosting the desktop side-docks on phones.
- Side panels made width-fluid (w-full on mobile, fixed on md+): StudioSidebarContent,
ImageEditorRightPanel.
Video Studio (VideoStudioMobileLayout):
- Canvas fills the viewport; the vertical tool dock becomes a scrollable bottom bar;
each tool's panel + the timeline open as bottom sheets. Exported MAIN_DOCK_ITEMS.
Image Editor (ImageEditorMobileLayout):
- Canvas fills the viewport; toolbar → scrollable bottom bar; Adjust/Filters/Layers
panel + shape picker open as bottom sheets. Exported IMAGE_TOOLS/IMAGE_SHAPES.
- Touch editing: Stage now handles onTouchStart/Move/End (draw, select, move) with
touch-action:none; draw-tool stroke works with a finger. Pointer handlers widened
to MouseEvent | TouchEvent.
i18n: added timeline/preview/panels keys (fa+en, parity verified). Full next build +
tsc clean. (Studio is auth-gated — verify editing on a device.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PricingCompareTable: wide 4-col table is hidden on mobile; new tab-per-plan card
view (Lite/Pro/Business) so pricing fits a phone. Extracted PricingCompareValueInline.
- Dashboard: sidebar becomes an off-canvas drawer on mobile (hamburger top bar +
overlay, closes on navigation) via DashboardSidebarDrawer; static column on lg+.
RTL/LTR safe (max-lg: transforms avoid the lg:/rtl: specificity trap).
- AdminResource: search/add row stacks on mobile (w-full sm:w-52), tables scroll
horizontally (overflow-x-auto + min-w) instead of clipping.
- Templates: added a mobile category chip row (lg:hidden) since the category
sidebar is desktop-only; exported VIDEO_SIDEBAR_CATEGORY_IDS.
- Hero: CTAs full-width on mobile, auto width on sm+.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A colour preset is a setup for ALL of a project's shared colours, but the editor
forced the admin to add each colour one by one. Now:
- "+ پریست جدید" pre-fills one item per shared colour (seeded from each colour's
default), so a new preset is a complete colour setup out of the box.
- New "+ همهٔ رنگهای مشترک" button back-fills placeholders for any shared colours
missing from an existing preset (or after new shared colours are added).
Frontend-only change in ProjectScenes.tsx PresetsTab.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The homepage is now driven by a `home_layout` Website Setting (jsonb) instead of a
hardcoded section stack — zero backend changes, no migration.
- lib/home-layout.ts: section catalog + saved-layout merge + locale-aware config
reader (`<field>_fa`/`<field>_en`) + public fetchHomeLayout() (falls back to
defaults when unset/unreachable).
- app/[locale]/page.tsx: renders ordered, enabled sections from the layout, passing
per-section content overrides.
- sections (Hero/Products/Templates/HowItWorks/Pricing/Testimonials/FAQ): accept an
optional `config` prop overriding heading/subtitle/CTA, locale-aware, default-safe.
- new HomeSlides + HomeEvents sections render the previously-orphaned admin Slides
(/v1/slides) and Home Events (/v1/home-events) data.
- admin: HomeSectionsManager (toggle on/off, ↑/↓ reorder, per-section FA/EN content
editor) at /admin/home, saved via the existing /v1/settings upsert; nav item + i18n.
Verified: a saved layout overrides Hero/Pricing headings and reorders sections;
removing it reverts to the default homepage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Idempotent seed (services/content/migrations/002_seed_blog_learn_pages.sql):
one Blog article, one Learn tutorial, and the 7 static Page rows
(about/contact/careers/privacy/terms/cookies/help) so the new public sections
render with real content and the admin "Pages" section has editable rows.
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>
"Use this example" now actually fills the new project, not just stores a ref.
- studio-svc: CreateProjectAsync applies the chosen preset story's saved values
after the template-graph copy. ApplyPresetValuesAsync reads
content.preset_stories.scenes_spa = { values: {contentKey:value},
colors: {elementKey:hex} } and overlays them onto studio.saved_scene_contents
(by key) + saved_shared_colors/saved_scene_colors (by element_key, is_selected).
Keys are globally unique (AE convention) so key-only matching is safe.
Malformed scenes_spa is skipped (defaults kept). Runs in the create tx.
- admin UI: ProjectPresetStories raw scenes_spa textarea replaced with a
structured PresetValueEditor — loads each preset scene's content elements +
the project's shared colours and renders a type-aware input per item
(text/textarea/number, media→upload, fill/color→colour). Serializes to
scenes_spa {values,colors}; parses it back on edit.
Verified e2e: authored a preset with values+colour → used it → the new
project's saved_scene_contents + saved_shared_colors carry the preset values
(which the B2 render binder then writes into AE).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Epic A — admins author premade example videos per template; users pick one
on the template detail page to start a pre-filled project.
Backend (content-svc):
- PresetStory DTOs + PresetStoryService (admin CRUD + public published-only
filter via role check + soft-delete) + PresetStoriesController (/v1/preset-stories)
- DI registration; gateway route /v1/preset-stories (optionalAuth, public read)
Frontend:
- ProjectPresetStories admin authoring UI (name/description/demo upload/published/
sort + scene picker with order+duration + advanced scenes_spa); «ویدیوهای نمونه»
button + modal in ProjectsAdmin
- TemplateDetailExamples renders real published stories (image/video preview,
hover → "use this example" → creates a pre-bound project), falls back to
placeholders when none; selected aspect's variant id keys the fetch
- public /api/preset-stories route; preset_story_id plumbed through
createProjectFromTemplate + projects POST route; usePreset i18n (fa+en)
Verified: full CRUD via gateway (public hides unpublished); creating a project
with presetStoryId persists selected_preset_story_id on the saved project.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When adding a scene in the admin scene editor, its duration is now pulled
from the After Effects project automatically (scene key = comp name).
frontend (ProjectScenes):
- the new/edit scene form quick-scans the project .aep for comp names +
durations and offers a "pick composition" dropdown that fills key, title
and default duration in one click
- the key field gains a datalist of comp names; typing a key that matches a
comp auto-fills the length (only when empty, never clobbering a manual value)
- an inline "AEP duration: Ns — insert" hint next to the duration field
- graceful states when no .aep is uploaded / scan fails
render-svc (aep.durationFromCdta): fix the composition-duration offset.
The duration rational lives at cdta offset 44 (numerator) / 48 (time base)
on AE 2024/2026, not 32/36 (previous guess) or 40/44 (boltframe reference,
older builds). Made it version-robust: read the time base from the framerate
dividend (offsets 4/8) and accept whichever offset places the time base right
after the numerator. Verified against a real project — render comp frfinal
parses to 15.02s (matches project_duration_sec 15.00).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The V2 scene-inputs editor only exposed ~15 of the content model's
~40 fields. Restore full parity with the legacy admin controller.
content-svc:
- SaveContentElementRequest + ContentElementResponse widened to the
complete field set (text/font, direction/RTL, media, advanced, DP)
- ApplyElement / ToElementResponse map every field 1:1
(Enum.TryParse for JustifyKind + AiInputType)
frontend (SceneInputsEditor):
- common fields up top; an "advanced" toggle reveals grouped sections:
Text and Font, Direction (RTL/LTR), Media, Advanced, Design-Presets (DP)
- editing an element loads the full field set; rows show font/hidden badges
- nullable numbers sent as null, enums as named values (snake_case body)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The node's onProgress callback only LOGGED — it never POSTed, so render_progress stayed
0 and step stayed Preparing (no bar, no ETA). Add render-svc POST
/v1/internal/render/jobs/{id}/progress (UpdateJobProgress: set render_progress + bump
step Queued/Preparing→Rendering once >0) + client UpdateProgress + wire onProgress to
post it (8s best-effort timeout, AE-CPU/DB-starvation tolerant). Preview already posts;
real-frame preview is epic C.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
THE bug behind 'nothing changed': parseScene required a STRING id, but the V2
relational scene assembly serializes numeric ids (scene 23, content 136) — so every
template scene was rejected → 0 scenes → studio fell back to its default 2-layer
title/subtitle. Coerce numeric ids to string. Verified: insta-promo project now
parses 1 scene / 6 layers (frl_c1t1-t4 text + frl_c1m1/m2 media).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add react-multi-date-picker + a reusable PersianDateInput (displays Jalali, stores
Gregorian ISO so DTOs are unchanged). Replace admin date inputs: UserProfileEdit
birthdate + CRM date-range filters.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Template copy now carries choose_mode from the content project → studio store gets
chooseMode; AddSceneMenu returns null for FIX/MusicVisualizer. Admin ProjectScenes
hides '+ صحنهٔ جدید' (shows an 'scenes defined in AE' note) for fixed modes. Verified
choose_mode=FIX flows end-to-end. (Visible admin nav link added earlier.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
r_height was hardcoded 1080. render-svc now derives r_height from the resolution
(ResolutionHeight) on job create; node-agent ffmpeg downscales to the tier height
(scale=-2:H). Quality picker now actually changes output size.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Edits previously never reached the MP4 (the node rendered template defaults). Now:
- render-svc claim includes the saved input values as bindings (GetRenderBindings →
saved_scene_contents with non-empty value).
- node-agent: new binder.go emits a JSON bind-spec + downloads media locally, runs the
pre-existing data-driven bind.jsx via afterfx (sets text layers' Source Text, replaces
media footage), saves a bound.aep next to the template, then aerender renders THAT.
- 12-min timeout + fresh-AE + done-marker polling (mirrors scan). Non-fatal: on bind
failure the job still renders template defaults.
Verified binding data flows (edited frl_c1t1/frl_c1t2 → claim bindings). Live MP4
verification needs the updated node-agent.exe re-run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Studio edits previously went only to edit_state; the render binds saved_scene_contents,
so edits never reached the MP4. Add studio-svc PATCH /v1/saved-projects/{id}/contents
(update saved_scene_contents.value/value_file_id by content key, ExecuteSqlInterpolated
for null-safe params) + Next /api/projects/[id]/contents route + persistence hook pushes
edited values (from bridged c-<key> layers) alongside the scene_data save. Verified
text persists incl. UTF-8 Persian (9 chars/17 bytes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The studio parser required scene.layers; a template-created project's scene_data
carries content-elements (scene.contents), so every scene parsed to null and the
editor fell back to the default 2-layer title/subtitle scene. Now parseScene bridges
contents → editable layers (Text→text, Media→image), so all of a scene's inputs
appear (e.g. c1 → 6: 4 text + 2 media). Scene name/duration also read V2 fields.
Remaining studio↔template epic (separate): edit→content-element→AE-render binding,
real AE scene-preview thumbnails, and FIX-mode (hide add-scene).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Homepage TemplateGallery card called the old Supabase createVideoProject which
failed → fell back to /templates. Now it routes to /templates/{slug} (detail),
where the user picks aspect/style and starts the project. Removed dead handler/imports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-scene inputs (content elements) editor existed only on /admin/templates
(SceneColorEditor). /admin/projects → «صحنهها» used the older ProjectScenes which
had no inputs panel, so admins couldn't see/edit a scene's inputs there. Export
SceneInputsEditor and add an «ورودیها» expander per scene row in ProjectScenes
(GET/POST/PUT/DELETE /v1/scene-elements, 15 element types). Verified c1 → 6 inputs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
next.config had no images.remotePatterns, so the optimizer rejected every remote
URL with HTTP 400 → all MinIO-hosted images (avatars, template art) showed broken.
Add remotePatterns derived from NEXT_PUBLIC_MINIO_URL + dev hosts (172.28.144.1/
localhost/minio :9000). Verified /_next/image → 200 image/jpeg.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Identity: admin-only PATCH /v1/users/{id} (reuses UpdateMeAsync) + POST {id}/avatar.
Admin Users panel: «پروفایل» modal to view/edit name/slogan/about/company/website/
country/national-code/birthdate/gender/avatar for any user. Verified admin→other-user edit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends CopyTemplateGraphAsync: repeater children flatten into saved_scene_contents
(repeater_item_key/index via repeater_items); scene characters+controllers and color
presets+items copied, correlated by (new scene, original-id/sort) since studio tables
lack original-id columns. studio character.key is a uuid → store original char id.
No regression on templates without these (copy 0 rows). All enum cols cast ::text.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Uploading an .aep/.zip in the template editor only set content.projects.aep_file_url
(a user-uploads reference) — it never copied the file to templates/{id}/ where the
render node-agent's claim looks. Result: uploaded templates weren't renderable.
attachAep now also POSTs /v1/template-bundles/{project_id} {source_url} after saving
the reference, which server-side-copies the file into templates/{id}/(bundle.zip|
template.aep). Uploading a template now makes it immediately renderable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
THE bug behind "AEPFilePath is required for real AE render": CreateJob inserted
original_project_id = saved_project_id (VALUES $3,$3), so the claim looked for the
render bundle at templates/{saved_project_id}/ — which never exists. The bundle
lives at templates/{TEMPLATE_id}/. Now original_project_id is resolved from
studio.saved_projects.original_project_id (the template the project was built from).
(Direct-SQL test renders masked this by setting the template id explicitly.)
Also harden the node-agent: Run() falls back to mock render when AEPFilePath is
empty even if AE is installed (previously hard-errored), so a missing/un-promoted
template degrades gracefully instead of failing the job.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After a CPU-heavy AE render+transcode the orchestrator/DB can be briefly slow;
the 15s client timeout made the post-render output-upload-url call fail and the
finished MP4 was dropped (completed without export). Bumped client timeout to 60s
and retry the upload-URL call up to 4× with backoff so a finished render's output
is never lost to a transient stall.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aerender can't reliably write H.264 directly in modern AE — it renders the
project's output module (Lossless AVI/MOV) and ignores the .mp4 extension,
producing a multi-GB .avi the agent then failed to find/upload.
- findRenderedOutput(): locate the file aerender actually wrote (output.avi/.mov/.mp4)
- transcodeToMP4(): ffmpeg → H.264 yuv420p + AAC + faststart; drops the lossless
intermediate. ffmpeg located via $FFMPEG_PATH, beside the agent exe, or PATH.
- Graceful fallback: if ffmpeg is missing/fails, upload the raw render so the job
still delivers a (large but valid) file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The output-upload-url 500 was NOT the enum cast — it was:
1. INSERT referenced original_project_id; the exports table column is project_id
2. file_type/create_type literals were lowercase ('video'/'render') but the
export_file_type/export_create_type enums are PascalCase ('Video'/'Render')
Verified the corrected INSERT against the live schema. Now real renders produce
a downloadable export instead of completing with export=nil.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scene content elements (the editable Text/Media/Color/… inputs inside a scene)
had no CRUD — only AEP-import created them, so admins couldn't define or edit
them. Added full management:
content-svc:
- SceneElementsController: GET/POST/PUT/DELETE /v1/scene-elements?scene_id=
- SceneColorService: Get/Create/Update/DeleteContentElementAsync
- ContentElementResponse + SaveContentElementRequest (key, title, type,
default_value, hint, position, text-box/font/media flags)
gateway: route /v1/scene-elements/*path → content
frontend: SceneColorEditor scenes tab → per-scene "ورودیها" expander with full
add/edit/delete of inputs (15 element types: Text/Media/Color/Number/…)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three bugs surfaced bringing up a real After Effects node (verified: AE 2026
claimed + ran, but produced no usable output):
1. aerender got no -comp/-rqindex → "output argument ignored", nothing rendered.
- Claim now returns comp_name from content.projects.render_aep_comp (e.g. "frfinal")
via new Store.GetTemplateCompName; threaded through ClaimedJob → runner.Job →
aerender args (`-comp <name>`, or `-rqindex 1` fallback when unknown).
2. CreateExportForJob INSERT passed render_quality as a bare param into an enum
column → 500 ("output-upload-url HTTP 500"), so completed renders had no export.
- Cast $8::render.render_quality (+ explicit casts for file_type/create_type enums).
3. flatrender-exports bucket didn't exist → uploads would fail anyway.
- render-svc now MakeBucket(exports, templates) idempotently at startup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>