Skip to content

feat(web): add PostHog ui_host for f.devalok.in proxy - #4

Open
Mudit-Lal wants to merge 35 commits into
prod-pinfrom
feat/posthog-fdevalok-proxy
Open

feat(web): add PostHog ui_host for f.devalok.in proxy#4
Mudit-Lal wants to merge 35 commits into
prod-pinfrom
feat/posthog-fdevalok-proxy

Conversation

@Mudit-Lal

Copy link
Copy Markdown

What

Add ui_host: "https://eu.posthog.com" to the web PostHog init. api_host already reads NEXT_PUBLIC_POSTHOG_HOST, being flipped to the shared Devalok first-party proxy (https://f.devalok.in → PostHog EU). ui_host keeps toolbar / session-replay links on the real EU app (the ingest proxy does not front the app surface).

Why

Adblockers that block posthog.com drop 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_HOSThttps://f.devalok.in on the Cap web service.

🤖 Generated with Claude Code

Mudit-Lal and others added 30 commits May 6, 2026 11:10
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.
Mudit-Lal and others added 5 commits May 6, 2026 22:58
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant