Facebook resumable upload START/FINISH phases require multipart/form-data
(not application/x-www-form-urlencoded). Add full raw response logging for
each phase to diagnose any remaining issues (subcode, etc).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace simple multipart upload (which fails with error 6000 on large
files) with the 3-phase Resumable Upload API (start → transfer → finish).
This is the recommended Facebook approach for files > ~50 MB and is much
more reliable regardless of file size.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add slow zoom/pan (Ken Burns) effect to each image slide, cycling through
8 deterministic presets per image index for natural variety
- CapCut-style captions: semi-transparent pill background + yellow glow on
active word; background/text now only visible while voice is speaking
- Fix Facebook error 6000: pass Node.js Buffer directly instead of unsafe
ArrayBuffer pool-slice conversion which could corrupt the upload payload
- Fix TypeScript error: replace u-flag emoji regex with BMP surrogate pairs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remotion's headless Chromium cannot reliably fetch http://localhost
inside Docker due to network sandboxing. Instead of HTTP URLs, convert
all local /uploads/... files to data:image/...;base64,... before passing
to inputProps. This is reliable in all environments (Docker, dev, CI).
Audio files remain as HTTP URLs since they are loaded via Web Audio API
which has different network handling.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
req.file.buffer is undefined with diskStorage — must read the temp file
from req.file.path using fs.readFileSync instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Logo stored as relative /uploads/logos/... was resolving to Remotion's
bundle server (port 3000) instead of the app (port 5555).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remotion's headless Chromium runs inside the same container as the app,
so it must use http://localhost:PORT to fetch local media files.
The Docker service hostname (socialflow-app) is only reachable from
other containers on the Docker network, not from within the container itself.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remotion's headless Chromium cannot load relative /uploads/... paths.
Library images were passed as-is, causing "Error loading image" at render time.
Now resolves them using INTERNAL_APP_URL (Docker) or the request host (dev).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Images uploaded between the local storage switch and the relative URL
fix have http://hostname/uploads/... stored in DB, which is blocked by
CSP. POST /api/media/fix-urls (admin only) updates all affected rows
to use relative /uploads/... paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Media files were stored with absolute http://localhost:5555/... URLs,
which got blocked by the Content Security Policy when the app runs at
https://socialflow.fnancy.fr. Now stores relative /uploads/... paths
and resolves to absolute URLs only when needed by external APIs
(Facebook, FFmpeg internal service).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Media is now stored directly on the server disk (no external service).
Files are served via Express static middleware at /uploads/*.
Cleanup rules:
- Videos deleted immediately after successful publish to Facebook/Instagram
- Videos older than 7 days purged daily by cron job
- Images older than 30 days purged daily by cron job
Changes:
- server/services/minio.ts: replaced S3 client with local fs read/write
- server/index.ts: added static serving for /uploads/media, /logos, /stories
- server/services/media-purge.ts: new rules (images 30d, videos 7d)
- server/services/scheduler.ts: delete local videos after successful publish
- docker-compose.yml: add media_uploads, logos_uploads, stories_uploads volumes
- settings UI: removed credentials card, shows local storage info
- package.json: removed googleapis (unneeded)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Store endpointUrl and publicUrl in cloudinary_config DB table so
MinIO connection can be fully configured from the app settings
without needing environment variables.
- DB migration: adds endpoint_url and public_url columns
- Schema: adds endpointUrl/publicUrl to cloudinaryConfig table
- MinIO service: reads endpoint from DB (falls back to MINIO_ENDPOINT env)
- buildMinioUrl: accepts optional publicUrl override from DB config
- All routes updated to pass config.publicUrl to buildMinioUrl
- Settings UI (desktop + mobile): adds Endpoint URL and Public URL fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Create server/services/minio.ts: S3-compatible service (same interface as
cloudinaryService) using @aws-sdk/client-s3 and MINIO_ENDPOINT env var
- Replace all cloudinaryService imports with minioService across routes.ts,
routes/reels.ts, routes/remotion.ts, services/media-purge.ts
- Replace Cloudinary logo URL pattern (res.cloudinary.com/...) with
buildMinioUrl() helper throughout all server routes
- GET /api/cloudinary/config now returns a logoUrl field (full MinIO URL)
so clients never need to build the URL themselves
- Update settings pages (desktop + mobile): labels changed to MinIO
(Bucket Name, Access Key, Secret Key)
- Update image-editor.tsx: remove Cloudinary URL transformation, use logoUrl
- Update media-utils.ts: simplify getVideoThumbnailUrl (no URL transforms)
- Add MINIO_ENDPOINT and MINIO_PUBLIC_URL to docker-compose.yml + .env.example
- DB table reuse: cloudinary_config.cloud_name = bucket, api_key = access key,
api_secret = secret key — no migration needed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New POST /api/remotion/publish endpoint: uploads MP4 to Cloudinary,
creates post/media records, publishes or schedules per-page as a Reel
- Desktop and mobile pages show page selector + DateTimePicker + description
after video generation, matching the Reels publication workflow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parses the generated TTS audio file with music-metadata to get the real
duration before computing word timings, eliminating desync between text
overlay and voice playback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Video duration forced to 25-30s (content 22-27s + 3s ending)
- Logo watermark (bottom-right) visible throughout content section
- Ending slide: logo centered + store name with spring animation (3s)
- Text overlay now shows only the active spoken word (true TikTok sync)
- Server fetches logo from Cloudinary config and store name from user's pages
- Player preview also shows logo/storeName via /api/cloudinary/config + /api/pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Convert relative music track URLs to absolute before passing to Remotion renderer
- Reverts Dockerfile from Debian-slim back to Alpine + gcompat/libc6-compat for lighter image
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>