# FlatRender V2 — Session Handoff & Gotchas > Context captured 2026-06-05 so it survives a Claude-account/machine change. > The `~/.claude` memory is machine-local; this file is the portable copy. ## How to run (V2 stack) ```bash # Backend services + infra (Postgres + MinIO) — V2 compose, NOT the default docker-compose.yml docker compose -f docker-compose.v2.yml --env-file .env.v2 up -d # Rebuild one service after a code change: docker compose -f docker-compose.v2.yml --env-file .env.v2 build && \ docker compose -f docker-compose.v2.yml --env-file .env.v2 up -d # Services: identity-svc, content-svc, file-svc, studio-svc, render-svc, notification-svc, gateway, frontend ``` - **Open the app at `http://172.28.144.1:3000`** (the host LAN IP), NOT `localhost`. - Gateway: `:8088`. JWT secret + service creds live in `.env.v2`. - DB: single Postgres `flatrender`, one schema per service (`identity`, `content`, `studio`, `render`, `file`, …). User `postgres`. - DB migrations: `backend/db/migrations/NN_*.sql`, applied **once** by `scripts/init-db.sh` on first volume creation. New SQL files must be applied manually: `docker exec fr2-postgres psql -U postgres -d flatrender -f ...` (or inline `-c`). ## ⚠️ Critical gotchas (these have each cost hours) 1. **Localhost is VPN-hijacked.** EonVPN intercepts `127.0.0.1:*`. Curl/loopback to localhost returns junk/000 even when the app is up. Use the LAN IP `172.28.144.1`, or `docker exec wget -qO- http://:`. 2. **Non-secure browser context.** Because the app is served over `http://172.28.144.1` (plain HTTP, non-localhost), the browser is a **non-secure context** → `crypto.randomUUID`, `crypto.subtle`, clipboard, etc. are **undefined**. NEVER call them in client code — use `src/lib/uuid.ts` `uuid()` (falls back to `crypto.getRandomValues`). This silently broke the entire studio editor (`crypto.randomUUID is not a function`). 3. **Studio service binds camelCase JSON** (no snake_case naming policy, unlike identity/content which use `SnakeCaseLower`). Frontend→studio request bodies MUST be camelCase (`originalProjectId`, not `original_project_id`) or fields drop to defaults (Guid.Empty). Studio responses are camelCase too. 4. **EF Core global query filters** (`HasQueryFilter(DeletedAt==null)`) require `.IgnoreQueryFilters()` to see/revive soft-deleted rows (bit the AEP import apply). 5. **AE automation** runs on a Windows node via `node-agent.exe` (PULL model: agent dials render-svc with HMAC). Before each AE launch the agent kills stale AE processes + clears crash/Safe-Mode markers (`SCRPriorState.json` + registry `AppStates`) and launches AE with the project as an arg (bypasses the Home screen). Heavy expression-driven AEPs take >10min to open → FIX scans are now **binary-only** (`services/render/internal/aep/parse.go` `ParseNames` reads `frl_`/`frd_` names from the .aep RIFX directly; no AE). ## What this session built (commits 6e5efbd → 5b2617d) - **AE scan hardening:** binary FIX scanner (no AE, never hangs); kill stale AE + clear crash dialog before each launch. - **Profile = data-collection surface** (for future AI video gen, NO resume builder): full editable profile (avatar upload + slogan/about/company/website/country/birthdate/gender), `/api/profile` + user-scoped `/api/profile/upload`. Identity DTOs widened (no migration — columns existed). - **Role-aware nav:** `UserMenu` (avatar + dropdown; admins get Admin Panel link) replaces Sign-In when logged in; real avatars in dashboard sidebar + admin shell; `Avatar` primitive; `getNavUser()`. - **Template detail page** wired to real content (`fetchProject(slug)`; was hardcoded demo catalog → 404'd). - **"Use template" works end-to-end:** `StudioService.CreateProjectAsync` deep-copies the content template scene graph (scenes + content elements + scene colors + shared colors) into the editable studio project via one atomic cross-schema SQL copy (enum cols cast `::text`; temp `_scene_map`). `/api/projects` resolves container slug → published variant project. **Aspect-ratio picker** (16:9/1:1/9:16) on the detail page drives which variant is copied. ## ⏭️ NEXT UP — Studio↔Template binding EPIC (agreed priority) **Phase B DONE (commits a69bc62 B1, 47a4ced B2).** Edit→render binding works: - **B1 ✅** studio input edits persist to `saved_scene_contents` via studio-svc `PATCH /v1/saved-projects/{id}/contents` (Next `/api/projects/[id]/contents`); the persistence hook pushes edited values (bridged `c-` layers) on every save. - **B2 ✅** render-svc claim now includes `bindings` (GetRenderBindings = saved_scene_contents with non-empty value); node-agent `binder.go` emits a JSON bind-spec + downloads media, runs the data-driven `bind.jsx` via afterfx (sets Source Text, replaces footage) → saves `bound.aep` → aerender renders THAT. - **VERIFY (needs node-agent re-run):** re-run the updated `node-agent.exe`, edit a text input in the studio (wait for "saved"), render, confirm the MP4 shows the edited text. Colours (saved_shared_colors → spec.colors) + footage-item media (vs layer-source) are follow-ups. - Next: **A** admin preset stories (premade videos — model `preset_stories`/`preset_scenes` exists, no endpoints/UI; detail "ویدیوهای ساخته‌شده" is placeholder), and **C** AE single-frame scene snapshots (`scenes.snapshot_url` empty → node `aerender -s 0 -e 0`). Also smaller, still open: per-tier render **height** (render-svc r_height hardcoded 1080 + node ffmpeg scale), **FIX hides add-scene** (mode not plumbed into studio store), **Persian/Jalali date pickers** in admin, **admin/renders** pagination + video/output + user-name → profile link, deeper per-input controllers in the admin scene-inputs editor. ## Done follow-ups (this session) - Scene-graph copy now includes repeater children, characters/controllers, color-presets. - Admin CAN edit any user's full profile (Users → «پروفایل»). - Studio now shows ALL template inputs (contents→layers bridge). ## Debugging client-side behavior No Playwright/Puppeteer in the repo. To reproduce browser issues headlessly: `npm i puppeteer-core` in a temp dir, launch the user's Chrome (`C:\Program Files\Google\Chrome\Application\chrome.exe`), set the auth cookie via `page.setCookie({name:'fr_access', value:, url:'http://172.28.144.1:3000'})`, and listen on `page.on('pageerror')` / `console`. Mint a test admin JWT with the `.env.v2` `JWT_SECRET` (HS256; claims `sub`=real user id, `tenant_id`, `is_admin:"true"`, `role:"Admin"`, `iss:"flatrender-identity"`, `aud:"flatrender"`).