feat(web): add PostHog ui_host for f.devalok.in proxy - #4
Open
Mudit-Lal wants to merge 35 commits into
Open
Conversation
Railway's Buildkit rejects `# syntax=docker.io/docker/dockerfile:1`. Default frontend works fine for this multi-stage build.
Railway sub-directory deploys (rootDirectory=apps/media-server) need the standalone Dockerfile whose COPY paths are relative to apps/media-server, not the root Dockerfile that expects repo-root paths.
Railway's Railpack builder rejects `--mount=type=cache,id=pnpm,...` without a cacheKey prefix on the id. Dropping the cache mount; build is slower but correct.
Same Railway Railpack constraint as apps/web/Dockerfile — id=pnpm without cacheKey prefix is rejected.
…full command Railway's startCommand exec's the literal string and does NOT append to a Docker ENTRYPOINT. With `ENTRYPOINT ["deno", "run", "--allow-all"]` Railway tries to run the startCommand as a file path, getting "permission denied". Removing ENTRYPOINT means startCommand becomes the CMD via shell, so `deno run --allow-all src/runner/index.ts` works as expected.
Railway's startCommand fights with multi-word commands (treats whole string
as executable path). Switching strategy: hardcode CMD in Dockerfile to invoke
deno + ${CLUSTER_ENTRY}. Each service sets CLUSTER_ENTRY env var (default =
runner) so no startCommand override needed.
…er Dockerfile Railway's startCommand override mechanism keeps fighting multi-word commands on this Deno-based image. Hardcoding the CMD per Dockerfile sidesteps the fight entirely. This Dockerfile is now Runner-only; ShardManager gets its own Dockerfile.shard-manager.
Variant of apps/web-cluster/Dockerfile whose only difference is the final CMD points at src/shard-manager.ts instead of src/runner/index.ts.
post-deploy.ts merges publishConfig into the deployed package.json. Without publishConfig.exports, the deployed package keeps `exports: "./src/index.ts"` and Deno tries to load TS source from node_modules, hitting ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING and crashing the runner. Mirroring the pattern @cap/database uses.
Effect Cluster's NodeClusterShardManagerSocket layerSocketServer binds to config.shardManagerAddress (default localhost:8080). With Cap upstream's SST infra they set SHARD_MANAGER_HOST=0.0.0.0 env var to override, but that mapping isn't reliably resolving on Railway. Hardcoding the bind address in shardingConfig works regardless of env-var conventions.
Previous attempt passed an object, which the Schema constructor reads as the host value (an object) → fails NonEmptyString check. Correct call: RunnerAddress.make(host, port).
When ECS metadata isn't available (e.g. on Railway), ipAddress defaults to 0.0.0.0. Runner then registers with the shard manager as 0.0.0.0:42069 and the shard manager can't reach it (0.0.0.0 from one container is its own loopback). Allow RUNNER_HOST env to override — set to cap-workflow-runner.railway.internal on Railway.
VideosPolicy was enforcing allowedEmailDomain even when video.public=true, blocking anonymous viewers despite the owner setting "Anyone with link". Public=true is an explicit per-video override; email restriction should only apply to private videos. Password protection (verifyPassword) is preserved for public videos.
Videos with completed uploads had videoUploads records persisting, making hasActiveUpload = true forever and blocking transcription. Only treat as active when phase is uploading/processing/generating_thumbnail.
Same bug as page.tsx: checking if any videoUploads row exists blocked transcription for completed uploads. Now only blocks when phase is uploading/processing/generating_thumbnail.
…total
Cap desktop app never updates phase from 'uploading' to 'complete' even
when the upload finishes (uploaded == total). All three upload guards
were permanently blocking transcription for every desktop-app upload.
Fix: active upload = phase IN ('processing','generating_thumbnail')
OR (phase = 'uploading' AND uploaded < total).
phase='uploading' with uploaded==total means the upload is done but Cap desktop app never advanced the phase. Add uploaded<total guard so completed uploads aren't treated as active.
…complete Same root-cause fix: Cap desktop never updates phase to 'complete'. Select uploaded+total and only block when uploaded < total.
workflow@4.2.0-beta.73 start() uses VERCEL_URL for self-callbacks, which doesn't exist on Railway. start() resolves silently without executing any steps, leaving transcriptionStatus = null forever. Fix: set transcriptionStatus = PROCESSING immediately (stops polling loop), then fire-and-forget transcribeVideoWorkflow() as a plain async call. The "use workflow"/"use step" directives don't prevent direct invocation — they only add step-endpoint registration at build time.
workflow package enforces start() — direct invocation throws at runtime. VERCEL_URL=cap.devalok.in env var added to Railway so start() can construct the self-callback URL for /.well-known/workflow/v1/step.
workflow@4.2.0-beta.73 start() silently no-ops on Railway (needs VERCEL_URL for self-callbacks, not set by default). Direct invocation also blocked — the compiler wraps the fn to throw. Fix: copy the transcription steps from workflows/transcribe.ts into a plain runTranscriptionDirect() with no "use workflow"/"use step" directives. Called fire-and-forget after setting PROCESSING in the DB.
startVideoProcessingWorkflow sets phase="processing" then calls start(processVideoWorkflow) which silently no-ops on Railway. Phase stays "processing" forever, blocking transcription indefinitely. Remove "processing" from blocked phases. rawFileKey is set before the workflow starts, so runTranscriptionDirect falls back to raw-upload.webm.
start(generateAiWorkflow) dispatches to Effect Cluster runner via RPC but runner never executes it (same issue as transcribeVideoWorkflow). Implement runAIGenerationDirect() that fetches VTT from S3, calls Groq/OpenAI inline, and saves results to DB — all in the Cap Web process without the workflow runner. startAiGeneration() now calls runAIGenerationDirect() fire-and-forget instead of start().
start() workflow dispatch leaves status as QUEUED forever since Runner never executes. Allow QUEUED in shouldTriggerAiGeneration so the browser poll auto-retriggers direct generation. Remove QUEUED from startAiGeneration early- return guard so it actually re-runs instead of returning "already in progress". Also allow QUEUED in retry-ai canRetry for manual retries.
start(processVideoWorkflow) dispatches to Effect Cluster runner via RPC but runner never executes it — same issue as transcription and AI generation. Implement runVideoProcessingDirect() that calls media server POST /video/process directly, polls for completion, saves metadata to DB, and cleans up the upload row and raw S3 file. This fixes result.mp4 generation and thumbnail creation for raw recorder uploads on Railway.
proxy.ts whitelist excludes /embed/, so any request to /embed/:videoId redirects to /login on self-hosted (NEXT_PUBLIC_IS_CAP !== "true"). Add /embed/ to the allowed paths so embed iframes load without auth. Backport of upstream PR CapSoftware#1415.
Replace the full blue color system with Devalok brand pink: - Swap @radix-ui/colors/blue.css for crimson + dark variant - Remap --blue-1..12 to var(--crimson-1..12) so all bg-blue-N Tailwind utilities automatically resolve to crimson - Update --primary/secondary/tertiary custom props to brand colors - Update inner-play-button-border-two gradient from #446fae to brand Brand colors from shilp-sutra: brand=#D33163, brandDark=#B8284F
…tton Override standard blue-50/900 shades with brand pink scale (H342), update blue-transparent rgba, remap radialblue gradient stops and shadow to brand.
…ommit Our globals.css edit mangled content: "“"; (CSS string containing U+201C) into content: """; (three ASCII quotes), causing a runaway unclosed string that PostCSS didn't detect until line 851. Use CSS escape \201c instead of the literal UTF-8 byte sequence to avoid re-encoding issues.
/google.svg and other /public/* assets matched the proxy matcher and got redirected to /login because they didn't match any whitelist prefix. Skip the redirect for any path with a file extension.
Add /privacy (linked from login footer), /admin, /dev, /messenger, and /docs to the self-hosted proxy whitelist. These routes have their own auth checks; the proxy was forcing 307 to /login even for authenticated users, making them unreachable.
workflow npm package on non-Vercel hosts uses world-local, which
dispatches step/flow execution by HTTP POSTing to
http://localhost:$PORT/.well-known/workflow/v1/{flow,step}. Our
self-hosted proxy redirected ALL non-whitelisted paths to /login,
so the self-callback hit a 307 instead of the workflow handler,
making start() silently no-op for transcribeVideoWorkflow,
generateAiWorkflow, processVideoWorkflow, and importLoomVideoWorkflow.
This is the underlying cause of the issue we worked around in
60af444, 7e6f5e1, and 4fbd30c. With this fix, those direct bypasses
can be reverted in a follow-up if desired.
The /.well-known/ proxy whitelist (a005fab) was the actual fix for workflow execution. Restore upstream behavior: - transcribe.ts: replace runTranscriptionDirect() with start(transcribeVideoWorkflow). Restore 'processing' phase to upload-blocking guard. - generate-ai.ts: replace runAIGenerationDirect() with start(generateAiWorkflow). Restore QUEUED to early-return block. - video-processing.ts: replace runVideoProcessingDirect() with start(processVideoWorkflow). Restore upstream error handling. QUEUED-as-retriable in retry-ai/route.ts and get-status.ts is left in place — harmless with working workflows since QUEUED is transient.
Without this, the get-status poll endpoint sees null transcriptionStatus and triggers transcribeVideo() repeatedly while the workflow's first step (validateVideo, which sets PROCESSING) hasn't executed yet. Result: ~10 parallel transcribe workflow runs for the same video. startAiGeneration already follows this pattern (sets QUEUED before start). Mirror it here to debounce poll-driven triggers.
api_host already comes from NEXT_PUBLIC_POSTHOG_HOST (flipped to the shared Devalok first-party proxy https://f.devalok.in in prod). Add ui_host so the PostHog toolbar / session-replay links resolve to the real EU app, which the ingest proxy does not front. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rs2hzKzJf9ar1yCwVcBi9o
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Add
ui_host: "https://eu.posthog.com"to the web PostHog init.api_hostalready readsNEXT_PUBLIC_POSTHOG_HOST, being flipped to the shared Devalok first-party proxy (https://f.devalok.in→ PostHog EU).ui_hostkeeps toolbar / session-replay links on the real EU app (the ingest proxy does not front the app surface).Why
Adblockers that block
posthog.comdrop events; the first-party proxy recovers them.Gate
Single tab-indented, double-quoted, comment-free property, compliant with the repo Biome config. CI Biome/lint is authoritative (biome not installed in this checkout).
Deploy
Flip Railway
NEXT_PUBLIC_POSTHOG_HOST→https://f.devalok.inon the Cap web service.🤖 Generated with Claude Code