Replaces the misleading flat Konva canvas (for FLEXIBLE/Remotion templates) with a
real preview the user can edit against:
- ScenePreview shows the scene's rendered still (scene.image) centred, and overlays
labelled, clickable HOTSPOTS over each editable field (logo / text), positioned by
a layout heuristic tuned to our blocks (visual centred, text stacked below).
- Clicking a hotspot selects that field; BlockFieldForm highlights + scrolls to the
matching field (and focusing a field highlights its hotspot) — "click the logo to
edit it" works both ways.
- CanvasEditor branches to ScenePreview when isFlexStoryProject(); AE/Konva
templates keep the full editor.
Fixes: (1) clicking a scene now shows its real image centre-screen; (2)/(3) the logo
and text are visible placeholders you can click to edit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wires the per-scene loop video all the way to the scene card:
- studio-svc: SavedSceneResponse now includes Demo (was stored + copied but never
serialized); MapSceneResponse passes s.Demo.
- Scene type gains image?/demo?; parseScene reads them from the loaded scene data.
- SceneThumbnailBlock shows scene.image as the still and plays scene.demo (muted,
looped) on hover, resetting on mouse-leave.
Existing projects backfilled (saved_scenes.image/demo from content.scenes). Both
services rebuilt + deployed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
For FLEXIBLE (Remotion / FlexStory) templates the render uses fixed positions —
dragging or resizing a layer on the Konva canvas does nothing to the output, which
is confusing. Make the canvas a read-only PREVIEW for those projects: the Konva
Layer is listening=false (no drag/select/transform), the Transformer is hidden, and
the auto-thumbnail capture is skipped so the flat Konva snapshot can't overwrite the
real rendered per-scene image. Editing happens only through the field form
(BlockFieldForm). AE/Konva templates are unchanged. Gated on isFlexStoryProject().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the theme→render gap: the studio theme picker now actually drives a
FlexStory render's colours. GetFlexStoryProps reads saved_shared_colors by
element_key (accentColor/secondaryColor/backgroundColor/textColor), but the studio
only wrote the theme into scene_data — so the picker never reached the MP4.
- studio-svc: UpdateSharedColorsAsync upserts saved_shared_colors by (project,
element_key) + PATCH /v1/saved-projects/{id}/shared-colors endpoint +
UpdateColorsRequest/UpdateColorItem. Mirrors UpdateSceneContentsAsync. (dotnet
build: 0 errors.)
- gateway already wildcard-routes /v1/saved-projects/*path → studio-svc (no change).
- Next: /api/projects/[id]/colors route → gateway; project-api patchProjectColors
+ themeColorsFromSceneData (maps scene_data sceneAccentColor… → the colorSchema
keys); performSave best-effort pushes the 4 colours alongside contents.
Chain: theme picker → store → scene_data → performSave → patchProjectColors →
gateway → studio-svc upsert → saved_shared_colors → GetFlexStoryProps → render.
Verified: Next build + dotnet build both clean; theme presets render cohesively
across all 6 (incl. dark Midnight). End-to-end studio→render needs the live stack.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scene-engine (FLEXIBLE) projects now get a clean per-field content editor instead
of the Konva layer panel. The scoping confirmed content VALUES already flow to
saved_scene_contents via the existing `c-`-layer + updateLayer + autosave path —
so this is purely a cleaner presentation over the working save path, no new
persistence.
- isFlexStoryProject(chooseMode) helper (FLEXIBLE → scene engine).
- BlockFieldForm: renders one labelled field per content layer (label from
layer.name — the field's Persian label, already preserved from the content
title), text→textarea, image→upload; writes back via the unchanged
updateLayer(props) call. No Konva geometry/layer chrome.
- StudioSidebarContent: the "scenes" tool branches on chooseMode — FlexStory →
BlockFieldForm, AE/Konva → SceneEditSidebarContent (zero regression).
- i18n: componentsStudioSidebarBlockFieldForm in fa + en.
Verified `npm run build`. NOTE: preview stays the live Konva canvas for v1 (a true
@remotion/player embed is deferred — 8–12MB Three.js bundle). Remaining: confirm
the FlexStory render binder reads the 4 theme colours from scene_data (already
persisted) vs saved_shared_colors (would need a small colours endpoint).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends the studio's 2-color palette to the full 4-color brand theme
(accent / secondary / background / text) matching the Remotion SceneColors,
so the studio's colour state maps 1:1 to the scene engine.
- studio-store: add sceneSecondaryColor + sceneTextColor + their setters + an
applySceneTheme(accent,secondary,background,text) action (sets all four +
recolours canvas layers: bg→background, overlays→secondary, shapes→accent,
text→text explicitly); persist both new fields in hydrate + getSceneDataForSave.
- studio-scene-data: carry sceneSecondaryColor + sceneTextColor through
VideoPersistedSceneData / build / parse (with defaults).
- ColorsCustomTab: 6 one-click theme presets (Warm/Berry/Midnight/Ocean/Sunset/
Mono) + 4 manual colour inputs + Apply.
- i18n: secondaryColor/textColor/themePresets/applyTheme(+preset) in fa + en.
Verified with `npm run build`. NOTE: the theme persists in scene_data and
recolours the canvas; wiring the 4 colours all the way to a FlexStory render's
saved_shared_colors depends on the studio-svc shared-colour sync (a small
follow-up). Block-FIELD editing remains the Phase 4 follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wires the scene-list operations users asked for into the existing timeline
(model-agnostic — works for any scene, layer- or block-based):
- SceneThumbnailBlock: now sortable (@dnd-kit useSortable) with a left-edge grip
handle (listeners only on the handle so select/rename/resize still work); adds a
numeric per-scene duration input (commit on blur/Enter, clamped 1–30s) next to
the drag-resize; a `locked` prop makes it read-only.
- SceneThumbnailStrip: wraps the blocks in DndContext + SortableContext
(horizontal, 6px pointer-activation so clicks/resize aren't hijacked) and calls
the existing reorderScenes store action; gates add/browse + reorder/duplicate/
delete/duration behind isFixedSceneMode(chooseMode).
- studio-store: isFixedSceneMode() helper (single source for FIX vs FLEXIBLE).
- i18n: reorderScene / durationLabel / secondsUnit in fa + en.
Verified with `npm run build` (rules-of-hooks clean). NOTE: a THEME PICKER and
FlexStory block-FIELD editing are deferred — the studio editor is Konva-layer-
centric, so both need a FlexStory-aware editing path (a follow-up), not this phase.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The curtain was sessionStorage-dismissible everywhere. NODE_ENV can't tell the
live deploy from the local Docker site (both are prod builds), so gate on the
hostname instead: localhost + private LAN ranges (incl. 172.28.x) keep the
"view experimental (local)" close button; any public domain is hard-locked to
just the countdown. Starts the curtain up by default so the live site never
flashes a page before it mounts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
- BrandedVideoPlayer (plyr-react) plays template demo videos with FlatRender
blue branding (Plyr CSS vars) and NO download control.
- Download protection: no download button, native controls replaced, underlying
<video> gets controlsList="nodownload" + disablePictureInPicture, and the
right-click context menu is blocked.
- Template detail page uses it; gallery hover-preview cards get the same
nodownload / no-context-menu / no-PiP guards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Render engine
- Add Remotion (code-based) as a 2nd render engine alongside After Effects.
node-agent dispatches on Job.Engine; RunRemotion maps bindings -> --props,
renders native then ffmpeg-scales to the quality tier (aspect-preserving).
- content.projects.render_engine + render_remotion_comp (migration 32);
render-svc claim resolves engine and routes (skips .aep for Remotion).
- Admin TemplatesAdmin gains an engine picker + Remotion composition id field.
Template pack (services/remotion)
- 16 branded, Persian (Vazirmatn), color- and text-editable templates, each in
3 aspects (16:9 / 1:1 / 9:16): LogoMotion, Opener, InstaPromo, YouTubeIntro,
Slideshow, HappyBirthday, SalePromo, QuoteCard, EventInvite, Countdown,
GlitterReveal (editable logo image), NowruzGreeting (animated characters),
and 4 cinematic 3D templates via @remotion/three (Hero3D, Nowruz3D,
Birthday3D, Promo3D) with reflections + bloom/DOF/vignette.
- scripts/seed_remotion_templates.py seeds containers/projects/scenes/colors.
Pricing
- Rewrite /pricing to the seconds-based model (charge = length x resolution),
data-driven from /v1/plans, Toman, broker checkout.
Coming-soon
- Persian experimental-build overlay on all pages (launch date + countdown).
Fixes
- middleware matcher bypasses all static asset paths; catalog mapping passes
cover image + preview video so real thumbnails render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The render page already displayed progress/ETA/preview — but the node agent never
fed real data: aeRender used fake +5%/10s increments, discarded aerender stdout,
and pushed a preview only every 30s. (Plus the deployed agent predated even the
progress-reporting wiring.)
node-agent (aeRender):
- Capture aerender stdout; parse "(N):" current frame + "N frames"/"to N" total.
- Real percentage when total is known (5–90%, headroom for transcode/upload),
else a smooth time-asymptotic estimate that never sticks — message shows the
live frame number either way.
- Push a preview frame ~every 8s (was 30s) so the box fills in quickly.
render-svc:
- GET /v1/renders/:id/progress now computes eta_seconds from started_at + progress
(linear extrapolation) instead of returning null.
frontend:
- Thread eta_seconds → status route → render page; page prefers the server ETA and
falls back to the client-observed rate.
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>
Monetization gate for the template render flow:
- render-quality.ts: single source of truth (free -> 360p only +
watermark; pro/business -> 540p..4K, no watermark).
- /api/render: server-authoritative gate — rejects >360p for free
users with 403 quality_locked; passes a watermark flag through
createRenderJob -> /v1/renders (render-svc passthrough, wired later).
- /api/render/limits: GET endpoint exposing the user's allowed tiers
and watermark state to the studio.
- render page: locks higher tiers for free users (dashed + lock badge,
click routes to /pricing), clamps the selected resolution down,
shows the "free = 360p + watermark, upgrade" notice, and handles the
403 quality_locked response.
AI-video "no free preview" rule is a future hook (no AI gen yet).
Watermark rendering (ffmpeg drawtext on the node) is a follow-up.
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>
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 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>
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>
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>
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>
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>
The full-screen render page only transitioned to "completed" when status was
completed AND an outputUrl existed, so dev renders (which produce no export file)
polled forever at 100%. Now completion is driven by status alone; the download/
share buttons render only when a URL is present, otherwise a "dev render, no file"
note is shown. Same guard helps real renders whose export URL resolves a beat late.
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>
Concurrent-render ceiling (a user runs 1 render at a time unless granted more):
- Identity: TokenService emits max_renders claim from User.ParallelRenderingCeiling
- Identity: admin POST /v1/users/{id}/render-slots (AdminService.SetRenderSlotsAsync,
clamped 1..50) — gamification or admin raises a user's ceiling
- render-svc: middleware reads max_renders (default 1); CreateJob rejects with 409
active_render_limit when active jobs >= ceiling
- render-svc: db.CountActiveJobs + ListActiveJobs; GET /v1/renders/active returns
in-flight renders + can_start_new
Full-screen render page (replaces the modal):
- /studio/render/[projectId]: config (resolution/fps) → live preview + progress →
download; resumes this project's in-flight render on mount; blocks when another
render is active; reads ?preset=
- StudioTopBar export menu now navigates to the page; RenderModal deleted (dead)
App-wide minimal progress:
- GlobalRenderProgress pill mounted in the locale layout for authed users; polls
/api/render/active every 4s, shows thumbnail + step + % on every page, click →
the render page; hidden on the render page and when idle
Admin: UserActions gains a "concurrent render slots" control.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- admin-files: fetchFolders / createFolder / deleteFolder + FolderItem; fetchFiles
takes a folderId filter
- admin files upload route forwards target_folder_id so uploads land in the open folder
- FileManager: breadcrumb navigation, folder cards (open / delete), "+ new folder",
folder-scoped file listing + upload. Folders hidden while searching (search spans all)
Uses the file-svc folder API (GET/POST/DELETE /v1/folders, folder_id list filter)
that already existed but had no UI. "Pick from library" was already wired via
FilePicker in FileUploadField.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New /api/files/upload: generic user-scoped Browser→Next→MinIO upload
(presign → PUT → confirm), 200MB cap, image+video only, returns public URL
- image-editor-export: stageToBlob() + saveStageToCloud(); "Save to my account"
button in the Image Editor export popover
- Trimmer: "Save to my account" button uploads the trimmed clip blob
- i18n: saveToCloud/savingToCloud/savedToCloud/saveToCloudFailed in fa+en
(parity 1002/1002)
Connects the two client-side editors to V2 storage — output now lands in the
user's account instead of only a local download.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build now (and every studio/image editor id generation) called crypto.randomUUID,
which is undefined outside a secure context — so over http://<LAN-IP> (used because
localhost is VPN-hijacked) the click threw 'crypto.randomUUID is not a function' and
the spinner hung forever, never reaching the editor.
Add lib/uuid.ts (crypto.randomUUID → crypto.getRandomValues → Math.random fallback)
and use it in studio-store, image-editor-store, project-defaults, dev-mock-project.
Verified headless (Chrome over http://172.28.144.1): Build now → 201 → navigates to
/studio/video/<id> → editor renders Scene 1 with editable title/subtitle fields.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The detail page now loads a template's real published aspect variants (16:9/1:1/9:16)
from the content container and the preview chips select among them. Build now copies
the SELECTED variant's scene graph (passes that variant's content project UUID), not a
default. Selection is lifted to TemplateDetailContent and shared by the preview picker
and the build button; the preview box reflects the chosen aspect.
Verified on insta-promo (16:9 + a duplicated 1:1 variant): both chips render, and
building 1:1 copies the 1:1 project's scenes (1 scene, 6 fields).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build now created an EMPTY project: (1) the studio binds camelCase but the frontend
sent snake_case → original_project_id dropped to Guid.Empty; (2) CreateProjectAsync
never copied scenes. Now:
- saved-projects.ts sends camelCase (originalProjectId/copyDefaultValues).
- /api/projects resolves the container slug → first published variant content project.
- StudioService.CreateProjectAsync deep-copies the content scene graph (scenes +
content elements + scene colours + shared colours) into the new saved project via
one atomic cross-schema SQL copy (enum cols cast to text; temp scene-id map).
Verified: insta-promo → 1 scene, 6 content fields, 4 shared colours, loadable by the
studio editor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/templates/[id] only searched the hardcoded demo catalog, so real published
containers (e.g. insta-promo) 404'd even though the browser listed and linked them.
Now resolveTemplate() fetches the container by slug via fetchProject(), falling back
to the demo catalog, else notFound(). Page + generateMetadata made async (await params).
Also fix TemplateDetailBreadcrumb: it called server-only getTranslations while
rendered inside the client TemplateDetailContent tree (500 at request time) — switched
to the useTranslations hook. Was latent because demo pages were static-prerendered.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>