enable mp4 download, smart Stage 4 cache, default Apify live in prod
Closes the last gaps so the operator app is end-to-end usable in production.
Server:
- routes/reports.ts: GET /api/reports/:id/dashboard[/<file>] serves files
out of the report's brief outputs/ tree (HTML bundle, dataset_v2.json,
any covers referenced relatively). Auth-gated by team viewer role.
Path-traversal guarded.
- index.ts: two new route patterns (with and without trailing path).
Client:
- routes/reports/detail.tsx: "Open dashboard" is a target=_blank anchor at
/api/reports/:id/dashboard/, "Download" same URL with download attribute.
No more dead SPA-internal link.
Pipeline polish (the four open items from the smoke test):
- stage_10_build.ts: covers are now downscaled via ffmpeg (240px / q=6)
before base64 inlining. Hard ceiling per cover 60 KB; falls back to the
original only if it already fits. Honours V3 brief's ≤3 MB HTML bundle.
- lib/apify_client.ts: post-run cost is re-polled with backoff (0/5/15/30s)
instead of a single read. TIKTOK_COMMENTS reports $0 immediately and
$5+ later — without this the soft cap can't fire on it.
- stage_2_pass1_scrape.ts: shouldDownloadVideos:true (and shouldDownloadCovers:true)
by default so videoMeta.downloadAddr is populated for Stage 4 frame
extraction. Disable with DISABLE_VIDEO_DOWNLOADS=true if the budget is
tight.
- stage_4_pass2_enrich.ts: Stage 5 backfill candidates aren't in the
transcripts/comments cache. New loadOrFetchActor() reads what's cached,
identifies missing ids, fetches just those from Apify, and merges back
into the cache. Backfill no longer drops every candidate.
Production defaults:
- .env.example: APIFY_LIVE_APPROVED=true (commented; operators can flip
to false for dry-runs).
- cutover-in-place.sh: sets APIFY_LIVE_APPROVED=true if not already in .env
after the migration step, so a fresh prod cutover doesn't accidentally
dry-run.
62/62 unit tests pass; tsc + vite build green; bundle 269 kB.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>