Commit Graph

191 Commits

Author SHA1 Message Date
soroush.asadi 40fdcf280f feat(render): always-available, fully-cancel render controls
The backend cancel was solid (CancelJob/StopJob; the dev worker abandons a cancelled
job, a real node kills its process) — but the UI couldn't reach it: the render page
had NO cancel button, and the global progress pill's X only HID the pill (the job kept
running). So a render couldn't actually be stopped from where you watch it.

- Render page: a prominent «لغو رندر» button while a render is in flight (Queued or
  Running); cancelRender() calls /renders/:id/cancel and returns to config optimistically.
  The poll now also handles a `cancelled` status (when stopped from another surface).
- Global pill: the X now CANCELS the render (with confirm) instead of just hiding it —
  so any in-flight render is cancellable from any page.
- (Dashboard MyRenders already had a working cancel.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:31:56 +03:30
soroush.asadi 6814e64593 fix(studio): responsive scene-preview placeholders that fit the still
The hotspot overlay used fixed percentages on a w-full/h-auto image, so a 9:16 scene
ballooned vertically and the placeholders (tuned for landscape) floated off the image.

Now the still is CONTAIN-fit inside the measured area (portrait + landscape both fit,
no overflow) and the hotspot overlay is anchored to the fitted image rectangle, so
placeholders always track and scale with the image. Hotspot positions are aspect-aware
(tall vs wide) and clamped to stay on the still.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 10:51:12 +03:30
soroush.asadi a36e96d933 fix(templates): real scene count on template pages (was always 0)
The card + detail read template.sceneCount, but the API never sent one — so the
frontend mapper hardcoded sceneCount:0 for every DB-backed template.

- content-svc: ContainerSummaryResponse + ContainerDetailResponse now carry
  SceneCount. The list computes it with one grouped query (scenes per aspect project,
  max across aspects); the detail loads scenes and counts them.
- frontend: V2ContainerSummary.scene_count → AdminProject.sceneCount → the catalog
  card/detail (adminProjectToCatalogTemplate no longer hardcodes 0).

Verified on the live local API: fr-instagram-promo → 5, single-scene templates → 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 10:51:12 +03:30
soroush.asadi 21b6a30f08 feat(scripts): portable template import/export (bundles)
Move a template fully between environments (local → live): container, projects, all
scenes + editable fields, shared colours/layers, its categories & tags, and the asset
files.

- export_template.py <slug> → a self-contained bundle (template.json + assets/). One
  SQL query captures the whole tree as JSON; assets are resolved from template-media
  references and copied in. Source DB via PSQL env (default = local docker).
- import_template.py <bundle> → idempotent SQL (pipe to target psql). Replaces by slug
  via one cascading delete (all content.* FKs are ON DELETE CASCADE), recreates rows
  verbatim (UUIDs preserved → FKs intact), merges categories/tags BY SLUG so they line
  up across DBs. --assets-to copies media; docker cp / mc cp hints for remote.
- TEMPLATE_BUNDLES.md documents it.

Round-trip tested on fr-instagram-promo: DB → bundle → DB restores identical
3 projects / 15 scenes / 138 fields and field values.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 10:09:41 +03:30
soroush.asadi 7725c13771 feat(seed): add Instagram channel-promo template to FlatRender (local)
- scripts/seed_instagram_promo.py — dedicated seeder for the 5-scene FlexStory IG
  promo (the generic seeder only handles CharacterStory's 2-text-per-scene shape).
  Scenes keyed `<BlockId>__<n>` (render decodes the block from the key); each scene's
  content-elements are that block's real fields (Text + Media); colours as shared.
  render_remotion_comp = FlexStory-<asp> so GetFlexStoryProps routes it.
- public/template-media/InstagramPromo* — thumbnails, per-scene stills, preview MP4.

Applied to local fr2-postgres + fr2-frontend: container fr-instagram-promo (3 aspects,
15 scenes, 138 fields), served by the gateway and the /templates detail page (200).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:56:40 +03:30
soroush.asadi 38229185a7 feat(remotion): IG promo posts accept images AND video
- igkit: a Media component that detects video by extension and renders a frame via
  OffthreadVideo (muted), else an Img — so any post slot takes images or reels.
- IGProfile: the profile-page grid is now editable — 6 post media fields (was static
  colour placeholders); videos get a ▶ reel badge.
- IGFeed: post slots now accept video too; labels say «عکس/ویدیو».

Verified: a profile still with an image cell + a video cell + avatar image renders
both correctly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:37:34 +03:30
soroush.asadi 7ed2ccc414 feat(remotion): Instagram channel-promo template + taste system + design-quality kit
The reference-round workflow, run end to end for a real template:

Taste system (how we learn the user's taste, persisted):
- references/TASTE_PROFILE.md (living design contract) + references/README.md (the
  daily loop) + a "reference round" stage in docs/TEMPLATE_BRIEF.md (provide refs or
  I suggest+mock directions).

Design-quality before/after:
- HeroDemo — the fix recipe vs the faint default: layered-depth background, a proper
  big video type scale, and a bold composed focal object. (Backgrounds were naked,
  text too small, scenes had no objects.)
- YaldaSofreh3D + IGPromoDirections + IGProfileMock — reference-match proofs
  (low-poly 3D, 3 IG-promo style directions, the realistic IG-light page).

Instagram channel-promo template (the deliverable — a flexible 5-scene FlexStory):
- igkit + 5 blocks: IGIntro, IGProfile (realistic IG-light profile, scales to all
  aspects), IGFeed (post grid), IGStats (animated count-up), IGFollowCTA (Follow taps
  to "Following").
- FlexStory gains a `finish` toggle so the IG-light scenes render clean (no brand
  grade). INSTAGRAM_PROMO preset + 3 aspect comps in Root.

Verified: a still of every scene at 9:16 renders clean; full preview MP4 rendering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 09:16:31 +03:30
soroush.asadi 8c4bc2c626 feat(remotion): craft kit (stop-motion + paper-cut) + PaperCut block
The real visible quality leap — a handmade craft aesthetic code can't fake by being
smooth:
- craft.ts: useStopMotion (quantize the frame to "on twos/threes" + per-step jitter
  → choppy handmade motion), paperShadow (layered cast shadows for paper depth),
  PAPER_TEXTURE (procedural fibrous paper grain).
- PaperCut block: a layered paper-cut landscape — sun + 4 brand-coloured paper hills
  with real cast shadows + paper grain, rising into place on stop-motion timing with
  an idle wobble, + paper-cut title/subtitle. Re-flows 16:9/1:1/9:16.

Registry now has 13 blocks. Verified: warm Yalda render (fits the Persian/seasonal
moat) + a stop-motion demo clip showing the on-threes choppy rise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 07:26:01 +03:30
soroush.asadi b1a51cb01b feat(remotion): shared FinishPass cinematic grade (quality floor) + @remotion/lottie
The single highest-ROI quality lift — one finish applied at the FlexStory level
lifts all 12 blocks at once, no per-block change:
- GRADE_FILTER: a headless-safe colour grade (contrast/saturation/lift) applied as
  a CSS `filter` on the content root — backdrop-filter does NOT render in headless
  Chrome, so the grade lives on the content, not an overlay.
- FinishPass: split-tone (cool-shadows multiply + warm-highlights screen) + a soft
  brand duotone + top light-bloom, layered over each scene.
- Installed @remotion/lottie@4.0.290 (artist-made animations — next lever).

Verified: visible richer/graded look on CharacterScene + Slideshow, subtle enough
to suit the muted palette, consistent across blocks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 23:35:08 +03:30
soroush.asadi 8f34c3175f feat(remotion): +3 scene blocks (BarChart, Stomp, DeviceMockup) + catalog/toolchain docs
Unlocks the biggest catalog gaps by composition:
- BarChart: animated infographic bars (value + label, normalized, staggered grow).
- Stomp: punchy beat-synced typography — words slam in with overshoot + shake +
  accent impact bar (titles / fashion / openers).
- DeviceMockup: phone/browser frame holding the user's screenshot + title/caption
  (app / website promo); placeholder when no image.
Registry now has 12 blocks. All verified via FlexStory props-override stills.

docs: CATALOG_PLAN.md (the full template taxonomy + production map + build waves;
the Persian/Islamic occasions = the moat) and PREMIUM_TOOLCHAIN.md (the stop-motion/
paper-cut/premium tool plan; editable-backdrop architecture; Iran/OFAC reality).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 23:16:44 +03:30
soroush.asadi 866edbff8c feat(studio): scene-engine preview editor — scene image + clickable field hotspots
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>
2026-06-24 22:09:36 +03:30
soroush.asadi 055d8365fe feat(studio): per-scene loop plays on hover (scene.demo end-to-end)
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>
2026-06-24 21:46:07 +03:30
soroush.asadi e4fd936953 feat(seed): per-scene loop video + thumbnail for every scene
Each scene now carries both a still (content.scenes.image) AND a short ~1.5s LOOP
video (content.scenes.demo), so the studio scene cards show a looping preview, not
a static swatch.

- LOOP_SCENES = {CharacterStory, LogoMotion3D}: their scenes get a dedicated
  per-scene loop ({tid}-{asp}-c{n}-loop.mp4 / {tid}-{asp}-loop.mp4); other
  templates fall back to their full preview MP4.
- Renders 42 loops: LogoMotion3D ×3 (frames 30–74) + CharacterStory 13 scenes ×3
  aspects (frames (n-1)*90+20 … +64), each a 45-frame / 1.5s clip mid-scene.
- Seed sets image + snapshot_url + demo per scene; verified all 42 serve 200 and
  the DB wires each scene to its own still + loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 21:09:04 +03:30
soroush.asadi 825f25be55 fix(studio): lock the canvas for scene-engine templates (no drag/resize)
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>
2026-06-24 09:33:20 +03:30
soroush.asadi 4bac5154ed fix(seed): real per-scene images for every scene (were only colour swatches)
Scenes were seeded with just a scene_color_svg swatch — content.scenes.image /
snapshot_url were empty, so the studio/admin scene previews showed swatches, not
the actual scene. Now every scene gets a real rendered image:
- single-scene templates → their per-aspect thumbnail;
- multi-scene templates → one still per scene, captured at that scene's own frame.

Adds the 39 CharacterStory per-scene stills (13 scenes × 3 aspects), each rendered
at (sceneIndex*90 + 45). LogoMotion3D's single scene now points at its thumbnail.
Verified: DB image/snapshot_url populated, all per-scene images serve 200, and the
stills are distinct per scene (c7 = «یک مانع», kicker ۰۷/۱۳).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:07:48 +03:30
soroush.asadi de8849bd94 feat(remotion): +LogoMotion3D template (Tech/3D cinematic logo reveal)
First template built through the new flow (brief → quality-gate approval → build →
seed → deploy). Tech/3D logo motion: a 3D metallic card + radial light rays + lens
flare + bloom (genuine @remotion/three), with the user's uploaded logo composited
on the card as a reliable HTML <Img> (renders any SVG/PNG/data-URI; static camera
keeps it aligned), brand text + tagline, grain. Falls back to a branded play-mark
when no logo is set. Re-flows across 16:9/1:1/9:16.

- LogoMotion3D.tsx registered per aspect in Root.tsx.
- Seeded as fr-logo-motion-3d: text fields (brandText, tagline) + a logoUrl image
  upload field + the dark-tech palette (light text) + per-aspect previews.
- 3 thumbnails + 3 preview MP4s rendered, deployed; detail page + assets serve 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 00:13:26 +03:30
soroush.asadi 7394c5ce78 feat(remotion): +ProductShowcase block (phone/browser device mockup)
Adds the product/app-showcase template type the engine was missing: a 2.5D device
frame (rounded phone with notch, or a browser window with traffic-lights + URL bar)
holding an uploaded screenshot, with title/subtitle and the shared Three backdrop.
Fields: screenshot, title, subtitle, device (phone|browser). Registry now 9 blocks.
Verified via FlexStory props-override stills (both device modes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 20:18:06 +03:30
soroush.asadi a48633741e docs(remotion): audio sourcing catalog (CC0 music/SFX, Iran-aware)
From the audio-sourcing-sweep (45 sources verified). The load-bearing test —
can a paid SaaS render the audio into customers' MP4s AND vendor the file —
rules out almost all "royalty-free" libraries; only CC0/PD passes cleanly.

- USE (CC0, vendorable): FreePD (music; site dead → archive.org/details/freepd),
  Kenney.nl (SFX; the one clean-from-Iran source), Freesound-CC0, OpenGameArt-CC0.
- CAUTION: incompetech CC-BY (needs attribution pipeline), aggregators (verify
  per-track), Sonniss/Pixabay (render-input-only, never vendor raw).
- AVOID/reference-only: Mixkit/Uppbeat/Bensound/Envato/Zapsplat/… (clauses + OFAC).
- Persian = no clean CC0 bulk source → commission + self-CC0 long-term.

Real files need a VPN/non-Iran fetch (acquire-once-then-vendor makes the licence
perpetual); only the 4 self-authored ffmpeg stubs are vendored today. Firewall
rules mirror the illustration assets.json + check-assets guard (already scans audio).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 20:03:50 +03:30
soroush.asadi 3eab1056c8 feat(remotion): audio layer — self-authored music bed + transition SFX in FlexStory
Adds audio to the scene engine without any third-party/geo-blocked sourcing: the
beds + SFX are synthesized with ffmpeg, so they're license-free (CC0, self-authored)
and need no acquisition — the same play as self-authoring Lottie.

- public/audio/: music-ambient.mp3 (soft 3-tone pad, looped) + sfx-whoosh/pop/chime.
- FlexStory: optional music/musicVolume/sfx props (optional so the existing render
  binding needs no change). Renders <Audio loop> for the bed + a whoosh at each
  scene start and a chime on the final scene, driven by precomputed scene starts.
- check-assets: now also scans public/audio (+ lottie) with folder-prefixed keys;
  assets.json ledgers the 4 audio files (CC0 self-authored).

Verified: tsc clean; a 6s FlexStory render produces an MP4 with a real audio stream
(ffprobe: codec_type=audio). NOTE: these are placeholder/SFX-grade; a premium
curated music library (by vibe) is a separate sourcing sweep, and the studio music
picker → FlexStory `music` prop is a follow-up wiring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 17:31:19 +03:30
soroush.asadi c0d04fa855 feat(studio+render): wire theme picker → saved_shared_colors → FlexStory render
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>
2026-06-23 17:04:47 +03:30
soroush.asadi c1747167f3 feat(studio): Phase 4 v1 — FlexStory block-field editor
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>
2026-06-23 15:40:32 +03:30
soroush.asadi 383331e8f1 feat(remotion): +2 scene blocks — LogoReveal (logo motion) + StatCounter
Grows the scene-block library toward full template-type coverage:
- LogoReveal: premium logo-motion — spring scale-in + glint sweep over the logo
  (image upload or a branded play-mark placeholder) + brand text + tagline, on the
  shared 2.5D Three backdrop. Fields: logoUrl, brandText, tagline.
- StatCounter: animated count-up to a target (English-digit value → Persian
  display) + suffix + label. Fields: value, suffix, label.

Registry now has 8 blocks. Both verified via FlexStory props-override stills.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 15:05:15 +03:30
soroush.asadi 8582e956c9 feat(studio): theme picker — 4-color brand theme + curated preset swatches
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>
2026-06-23 14:56:14 +03:30
soroush.asadi 8ddca5647b feat(studio): Phase 3 — scene reorder + numeric duration + FIX/FLEXIBLE gating
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>
2026-06-23 14:18:00 +03:30
soroush.asadi f8ea9af3b6 feat(render): Phase 2 — FlexStory render passthrough + journey template seed
Closes the render boundary so a user's scene list (order, per-scene content,
per-scene duration, theme) actually drives the FlexStory engine — the one gap the
scene-engine mapping found.

- render-svc GetFlexStoryProps (db.go): structured per-scene query that groups
  saved_scene_contents BY scene (the flat GetRenderBindings union collides when
  scenes share keys like "title"), recovers blockId from the scene key
  ("<BlockId>__<n>"), and emits the FlexStory props object
  {scenes:[{blockId,durationSec,props}], accentColor, …}.
- render-svc Claim (internal.go): when the template is Remotion + comp starts with
  "FlexStory", send that object as a single "__flexprops__" binding (no protocol
  struct change).
- node-agent remotionProps (remotion.go): if "__flexprops__" is present, pass it
  to `remotion render --props` verbatim (it's the complete props object).
- scripts/seed_flexstory.py: seeds the CharacterJourney template (7 scenes, theme
  colours, FLEXIBLE) with blockId-encoded scene keys, so the studio's existing
  CopyTemplateGraphAsync copies them into saved_scenes with zero studio-svc change.

Both Go services compile; template is live in the catalog (detail 200, per-aspect
previews). End-to-end render verification needs a live Remotion render node.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:45:04 +03:30
soroush.asadi 2104dd3c84 feat(remotion): theme system + CharacterJourney pilot template
- src/scenes/themes.ts: 6 curated themes (the cohesion rail) — pick one, then
  tweak the 4 brand colors; every block derives its shades so a theme re-skins
  the whole video coherently (verified: same journey rendered in warm-editorial
  vs berry-pop by overriding only the 4 colors).
- src/scenes/presets.ts: CHARACTER_JOURNEY — the pilot template's scene list
  ("Idea → struggle → tool → win", 7 beats) as a FlexStory preset.
- briefs/character-journey.md: the filled Template Spec from the guided brief.
- Root.tsx: register CharacterJourney per aspect (FlexStory + the preset).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:19:51 +03:30
soroush.asadi d830c56ea0 feat(remotion): FlexStory scene engine — ordered editable scene-blocks (Phase 1)
Turns a template into an ordered list of editable scene blocks instead of one
monolithic composition — the foundation for the scene-based template engine
(all Renderforest-style types, per-scene editable duration, add/duplicate/
delete/reorder). Render-side only; backend wiring is Phase 2.

- src/scenes/types.ts: SceneInstance/BlockProps/SceneBlock + withDefaults/clamp.
- src/scenes/chrome.tsx: shared 2.5D Three.js backdrop (parallax camera, blobs,
  particles, optional 3D confetti) + grain/vignette/progress/kicker/transition.
- src/scenes/blocks/*: Core 6 blocks — TitleCard, CharacterScene (full room +
  vendored CC0 character behind a desk), ImageCaption, KineticQuote, Slideshow,
  OutroCTA — each with editable fields + its own duration range.
- src/scenes/registry.ts: the block registry (blockId -> block).
- src/compositions/FlexStory.tsx: the sequencer — stacks blocks in <Sequence>,
  clamps per-scene duration, and computes composition length dynamically via
  calculateMetadata (so add/delete/reorder/duration all flow to the render).
- StoryScenes.tsx: the 2.5D story proof this productizes; docs/TEMPLATE_BRIEF.md:
  the guided creator flow + Template Spec.

Verified: all 6 blocks render via FlexStory in 16:9/1:1/9:16; a custom props
override (reordered scenes, custom characters/durations/colors) renders correctly
and the total length tracks Σ per-scene durations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 07:45:57 +03:30
soroush.asadi fd364209e7 feat(coming-soon): hard-lock the live curtain; closable only on local/dev hosts
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>
2026-06-23 07:45:16 +03:30
soroush.asadi cb6512fee3 feat(remotion): asset-library catalog + Phase 0 (license firewall, @remotion/lottie, 30 CC0 characters)
- docs/ASSET_LIBRARY.md: curated catalog from the asset sweep (91 sources -> 62
  usable) + completeness-critic reality check; clean CC0/MIT tier, license/geo
  traps, and the 2.5D layered-scene plan (sky->room->furniture->device->character
  ->grain) to fix the "naked scene".
- deps: add @remotion/lottie@4.0.290 (runtime) + DiceBear (build-time devDep).
- scripts/gen-dicebear.mjs: generate 30 CC0 Open-Peeps characters OFFLINE (no
  runtime CDN) into public/illustrations/dicebear/ + a per-file assets.json ledger.
- scripts/check-assets.mjs: license-firewall CI guard — fails on any un-ledgered
  vendored asset.
- AssetSheet dev composition: proves vendored SVG -> staticFile() -> Remotion render
  (30 real characters render cleanly).
- NOTE: GitHub (Open Peeps/IRA/Notion git clones) + Gumroad (Lukasz) are geo-blocked
  headless here; those + Humaaans (Figma export) need a manual/mirror fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 18:59:03 +03:30
soroush.asadi a3152ee84f feat(remotion): premium CharacterStory template (13 flexible scenes) + fix detail-page SSR
- CharacterStory: refined flat-illustration character (gradient-shaded sweater,
  modern hair, calm minimal face), muted editorial palette (coral/teal/sand/navy),
  abstract environment (soft depth blobs, ground "stage", sparse particles,
  vignette + grain), scene-number kicker. Verified in 16:9/1:1/9:16 and all poses.
- seed: 13 editable scene cards (c1..c13, keys s{N}_title/s{N}_text) via new
  MULTISCENE path; per-aspect previews; muted defaults.
- assets: 3 thumbnails + 4 preview MP4s vendored into public/template-media.
- fix: load BrandedVideoPlayer (plyr-react) client-only via next/dynamic
  (ssr:false) — plyr touches `document` at import, which was 500-ing every
  template detail page during SSR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 16:58:48 +03:30
soroush.asadi 863b9503b3 fix(detail+docker): per-aspect template preview + Debian frontend base
CI/CD / CI · Web (tsc) (push) Successful in 1m17s
CI/CD / Deploy · full stack (push) Failing after 15s
- 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>
2026-06-21 23:04:04 +03:30
soroush.asadi 60759f35b4 polish(remotion): shiny titanium finish on AppShowcase3D phone
CI/CD / CI · Web (tsc) (push) Successful in 1m22s
CI/CD / Deploy · full stack (push) Failing after 14s
Polished-metal look: low-roughness (0.15) titanium + contrasty studio Environment
(light bases + bright softbox strips) so the rounded edges catch hot reflection
streaks that sweep as the phone rotates; shinier side buttons. Re-rendered all
aspects + preview, redeployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 22:00:26 +03:30
soroush.asadi 1795bc855b feat(remotion): premium 3D app-showcase template (AppShowcase3D)
CI/CD / CI · Web (tsc) (push) Successful in 1m19s
CI/CD / Deploy · full stack (push) Failing after 12s
- New @remotion/three template: titanium flagship phone (thin rim, glossy black
  glass, rounded-corner screen via ShapeGeometry, dynamic island, side buttons),
  light keynote studio (contact shadow + env reflections + DOF + soft bloom),
  film grain + entrance light-sweep. All 3 aspects re-flowed.
- Editable screenUrl (user app screenshot textured onto the screen via TextureLoader
  + delayRender), appName/tagline/cta, 4 colours (dark text on light bg).
- Add pick(wide,square,tall) helper to lib/aspect.ts (Tier-0 from the R&D).
- Seed: AppShowcase3D + per-template text colour; built with the flat-artist skill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 21:28:49 +03:30
soroush.asadi f83d657844 chore(skills+remotion): add flat-artist skill bundle; register 3D templates
CI/CD / CI · Web (tsc) (push) Successful in 1m19s
CI/CD / Deploy · full stack (push) Failing after 12s
- .claude/skills/flat-artist: the bundled FlatRender template-creation suite
  (orchestrator + 16 sub-skills + design/motion R&D), mirrors the Gitea AISkills repo.
- services/remotion Root.tsx/templates.tsx: register the 3D templates + Three3DTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 19:39:25 +03:30
soroush.asadi cb11c177a7 fix(ci): stop pulling Alpine packages from the geo-blocked CDN
CI/CD / CI · Web (tsc) (push) Successful in 1m21s
CI/CD / Deploy · full stack (push) Failing after 15s
The CI server can't reach dl-cdn.alpinelinux.org (TLS error) — only the Nexus
mirror is reachable, and it proxies Docker images, not apk packages.

- frontend: drop `apk add libc6-compat` (vestigial Next.js-template line; the
  deps stage only runs `npm ci` and the build/runtime stages never had it).
- 5 Go services (file/gateway/notification/payment/render): replace
  `apk add ca-certificates tzdata` with copying ca-certificates.crt from the
  golang builder stage + embedding tzdata via `go build -tags timetzdata`.
  No more apk -> no dependency on the Alpine CDN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 17:08:16 +03:30
soroush.asadi af3c73c560 feat(templates): branded Plyr video player for demos with download protection
CI/CD / CI · Web (tsc) (push) Successful in 1m24s
CI/CD / Deploy · full stack (push) Successful in 8m14s
- 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>
2026-06-21 16:44:55 +03:30
soroush.asadi 4f04f6bf75 feat(render+templates): Remotion engine, 16 branded templates (incl. 3D), seconds pricing, coming-soon
CI/CD / CI · Web (tsc) (push) Successful in 1m21s
CI/CD / Deploy · full stack (push) Failing after 20s
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>
2026-06-21 15:52:52 +03:30
soroush.asadi b9b91397b0 fix(deploy): configurable postgres host port (avoid 5432 conflict)
CI/CD / CI · Web (tsc) (push) Successful in 1m10s
CI/CD / Deploy · full stack (push) Successful in 3m58s
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>
2026-06-18 01:38:33 +03:30
soroush.asadi 6d79ddb8d1 feat(render): real progress %, ETA, and frequent preview during AE renders
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>
2026-06-18 01:18:54 +03:30
soroush.asadi 23d1fd8fb1 fix(payment): default broker host port to 1607 (8090 was allocated on the server)
CI/CD / CI · Web (tsc) (push) Successful in 1m10s
CI/CD / Deploy · full stack (push) Successful in 3m6s
2026-06-16 07:28:05 +03:30
soroush.asadi 376cdf6a1c feat(payment): route FlatRender plan purchases through the broker
CI/CD / CI · Web (tsc) (push) Successful in 1m10s
CI/CD / Deploy · full stack (push) Failing after 11m4s
- 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>
2026-06-16 00:34:45 +03:30
soroush.asadi ec51e87d2d feat(payment): standalone ZarinPal broker on pay.flatrender.ir
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>
2026-06-15 23:59:54 +03:30
soroush.asadi 896ce3dfa9 feat(render): plan-gate quality tiers — free=360p watermarked, paid=all
CI/CD / CI · Web (tsc) (push) Successful in 1m8s
CI/CD / Deploy · full stack (push) Successful in 3m8s
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>
2026-06-15 15:17:25 +03:30
soroush.asadi 468ae2ae97 docs(deploy): fix init-script path + add stale-volume reset note
CI/CD / CI · Web (tsc) (push) Successful in 1m10s
CI/CD / Deploy · full stack (push) Successful in 3m10s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:48:51 +03:30
soroush.asadi 1106c03feb docs(deploy): sync nginx/cert/DNS docs with the real working setup
CI/CD / CI · Web (tsc) (push) Successful in 1m12s
CI/CD / Deploy · full stack (push) Has been cancelled
Reflect what the live deploy actually required:
- cert must be NESTED under an already-mounted dir (/etc/ssl/soroushasadi/flatrender/)
  — mirror-nginx mounts cert dirs individually, so a fresh /etc/ssl/flatrender is
  invisible in the container.
- after a sed -i edit of the bind-mounted nginx.conf, restart (not reload) — inode swap.
- DNS: box is behind NAT (171.22.25.73 private; public via edge/CDN 185.239.1.100 or
  direct 31.171.101.x) — register the domain the same way the other sites enter.
- local SNI test command to verify routing bypassing DNS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:46:32 +03:30
soroush.asadi 514cd3705f ci(deploy): mount postgres init as a DIRECTORY (fix 'Is a directory')
CI/CD / CI · Web (tsc) (push) Successful in 1m8s
CI/CD / Deploy · full stack (push) Successful in 2m50s
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>
2026-06-12 21:39:15 +03:30
soroush.asadi c67d746004 ci: redeploy after resetting stale pgdata volume (fresh init + migrations)
CI/CD / CI · Web (tsc) (push) Successful in 1m8s
CI/CD / Deploy · full stack (push) Failing after 20s
2026-06-12 21:17:34 +03:30
soroush.asadi 072ac78b77 ci(deploy): pull minio -cpuv1 from Liara docker mirror (baseline CPU)
CI/CD / CI · Web (tsc) (push) Successful in 1m8s
CI/CD / Deploy · full stack (push) Failing after 20s
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>
2026-06-12 21:09:26 +03:30
soroush.asadi 0fefedbb86 ci(deploy): use minio -cpuv1 image for baseline-CPU server
CI/CD / CI · Web (tsc) (push) Successful in 1m8s
CI/CD / Deploy · full stack (push) Failing after 1m6s
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>
2026-06-12 20:39:19 +03:30
soroush.asadi 56e2202b5b ci(deploy): pin minio to pre-x86-64-v2 release (baseline CPU)
CI/CD / CI · Web (tsc) (push) Successful in 1m9s
CI/CD / Deploy · full stack (push) Failing after 1m4s
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>
2026-06-12 19:32:55 +03:30