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>
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>
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>
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 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>
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>
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>
- /dashboard/renders: user's own render jobs (live status + progress bar + cancel)
and finished exports (thumbnail + size/duration + download); bilingual fa/en
- server lib my-renders.ts (user-scoped /v1/renders + /v1/exports via session JWT)
- user action routes: POST /api/renders/[id]/cancel, GET /api/exports/[id]/download
(presigned URL)
- dashboard sidebar: "رندرهای من / My Renders" nav item
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- AdminShell: the rtl:/ltr: translate variants ([dir] selector) out-specified
lg:translate-x-0, so the sidebar stayed off-screen on desktop and the mobile
drawer couldn't open. Pin physically right + plain translate-x-full/0; content
uses lg:mr-60.
- /admin now redirects to /admin/stats (overview) instead of /admin/nodes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- AdminShell: fixed RTL sidebar with grouped nav (نمای کلی / محتوا / رشد و ارتباطات
/ کاربران و مالی / فارم رندر / سیستم), active-link highlighting via usePathname,
sticky header showing the current section, mobile drawer with hamburger + overlay
- layout: build the grouped nav and render via AdminShell
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The admin render queue called the user-scoped /v1/renders (so it only showed the
admin's own jobs) and parsed items/total instead of data/meta (→ always empty).
- render-svc: GET /v1/admin-renders (admin) → ListAllJobs across users, optional
?status= filter; gateway-wired
- admin renders page now fetches /v1/admin-renders and reads data/meta correctly
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- render-svc: admin-scoped store (ListAllExports / GetExportByIDAny /
SoftDeleteExportAny) + GET/DELETE/download-url under /v1/admin-exports
(admin-gated; separate prefix so it routes to render, not identity's /admin)
- gateway: /v1/admin-exports/* → render
- admin /admin/exports: paginated table of every rendered export with thumbnail,
type/quality, size, duration, dimensions, produce + expiry dates; download
(presigned URL) and delete
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Push a font once → every node installs it → admin sees per-node status.
- render-svc: font_requests + node_fonts tables (mig 25); admin GET/POST/DELETE
/v1/node-fonts (with per-node status matrix); internal (HMAC) GET pending +
POST status for node-agents
- node-agent: fontSyncLoop polls pending fonts every 60s, downloads, installs
(Windows Fonts dir + registry / macOS / linux fc-cache), reports Installed/Failed
- gateway: /v1/node-fonts/* → render
- admin /admin/node-fonts: upload a .ttf/.otf → install on all nodes; per-node
Installed/Pending/Failed badges + counts + delete
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The render-queue cancel button used the owner-scoped /cancel (WHERE user_id=…),
so an admin couldn't stop another user's job. Added:
- render-svc: POST /v1/renders/:job_id/stop (admin-gated) → store.StopJob cancels
any in-progress job regardless of owner and frees the assigned node
- admin: render-queue button now "توقف" → /api/admin/renders/{id}/stop (with confirm)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Nodes page: "+ افزودن نود" opens a full-screen form (name, region, IP, worker
port, AE version, node kind, RAM, CPU, priority) → POST /v1/nodes
- current_ae_version is now a dropdown (2025…2020, matching the ae_version DB
enum) instead of free text; node_kind is a dropdown (Shared/Dedicated/Spot)
- new POST /api/admin/nodes proxy route (forwards body; admin-gated). The backend
POST /v1/nodes existed but had no UI — you couldn't define nodes before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- content-svc: GET /v1/projects (browse/search all projects across containers,
paginated, admin) returning template name/slug + AE status; project_assets
table (mig 23) + entity; GET/POST/DELETE /v1/projects/{id}/assets
- /admin/projects: searchable, paginated list of every renderable project with
thumbnail, template, aspect/resolution, AE-file + publish status
- ProjectAssets component: list/upload/delete named footage/image/audio/font
files per project (reused in the projects page; AE file upload alongside)
- nav + fa/en "Projects" label
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Final legacy-admin items:
- identity GET /v1/admin/plan-statistics (active/total users + revenue per plan
from user_plans); surfaced as a breakdown table in /admin/stats
- NodesTable: wire Restart + Close-AE actions (backend already supported them) via
new proxy routes; was only drain/release before
Full DivineGateWeb legacy-admin parity achieved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the remaining legacy-admin gaps:
- Users «مدیریت» modal: create personal discount or affiliate code (owner_user_id +
owner_profit_percentage on existing /v1/discounts), and view the user's saved
projects ("videos") via new admin GET /v1/saved-projects/by-user/{id} (studio)
- Internal routes admin (/admin/routes): CRUD on content.internal_routes
(RoutesController + CmsService + gateway /v1/routes/*)
- Security: lock identity UsersController Search + Ban to [Authorize(Roles="Admin")]
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- /admin/music: list / upload / delete studio audio tracks (content-svc
GET/POST/DELETE /v1/music) — fills the legacy music-library gap
- fix: CRM analytics coerced query-bound dates to UTC (Npgsql timestamptz
rejects Kind=Unspecified) — endpoint was returning 400
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- /admin/discounts: list + create discount codes (kind, value, max uses, expiry)
via /v1/discounts (backend has no edit/delete API yet)
- /admin/settings: key/value site settings with upsert + secret flag. The value
column is jsonb, so values are JSON-encoded on save / decoded for display
- nav links + fa/en labels
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>
Token auto-refresh (middleware):
- Proactively refresh fr_access when < 120s remain — no more silent 15-min kick
- Inlines /v1/auth/refresh call in middleware, stamps new cookies on response
- /admin/* protected: is_admin JWT claim required, else redirect /dashboard
- apiFetch() (src/lib/api/fetch.ts): client-side 401 → auto-refresh → retry;
de-duplicates concurrent refresh calls; redirects to /auth on failure
Studio → Render V2 wiring:
- scenes[] no longer sent to POST /api/render (V2 render-svc fetches project
from Studio service via saved_project_id directly)
- renderRequestSchema.scenes is now optional
- RenderModal uses apiFetch for auto-refresh on 401 during polling
Admin panel (/admin/*):
- Admin layout: server-side is_admin guard + top nav (Nodes, Render Queue)
- /admin/nodes: lists all nodes from GET /v1/nodes with status badges,
heartbeat age, slot usage, tags; Drain (PATCH status=Draining) + Release actions
- /admin/renders: render job table with step filter tabs; progress bars,
error messages, Retry + Cancel per-row actions; polls GET /v1/renders
- API proxy routes: /api/admin/nodes/:id/drain|release,
/api/admin/renders/:id/retry|cancel — all validate is_admin in JWT before proxying
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
render-svc:
- db.UpdateJobPreview(): writes base64 PNG to render_jobs.image_preview_b64
(only on active jobs; Done/Failed/Cancelled rows ignored)
- POST /v1/internal/render/jobs/:job_id/preview — node agent endpoint
- Route registered under /v1/internal (nodeAuth)
node-agent:
- runner.PreviewFn callback type alongside ProgressFn
- runner.preview.go: GeneratePreviewB64(percent, quality, resolution)
— pure stdlib (image/png + encoding/base64), no external deps
— 320×180 dark frame with animated progress bar + colored indicator dots
- mock render: pushes a preview frame at every step (5→95%)
- real AE render: pushes a preview frame every 30s
- client.UpdatePreview(): POST /v1/internal/render/jobs/:job_id/preview
- main.go: onPreview callback wires client.UpdatePreview() into runner.Run()
frontend:
- render-jobs.ts: RenderJobRow.preview_b64 field; read from progress endpoint
- status/route.ts: previewB64 included in JSON response
- RenderModal: aspect-ratio preview pane during polling — shows spinner until
first frame arrives, then live-updates with each poll (every 3s);
step label overlaid as badge bottom-right
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>