Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f7965cb
build: drop external dockerfile frontend directive
Mudit-Lal May 6, 2026
308f111
build(media-server): use standalone Dockerfile from sub-dir context
Mudit-Lal May 6, 2026
a6dc7c4
build: drop pnpm cache mount
Mudit-Lal May 6, 2026
c87c556
build(web-cluster): drop pnpm cache mount
Mudit-Lal May 6, 2026
a472acf
build(web-cluster): drop ENTRYPOINT, let Railway startCommand be the …
Mudit-Lal May 6, 2026
ee17754
build(web-cluster): env-var driven CMD for shard-manager vs runner
Mudit-Lal May 6, 2026
a401e13
build(web-cluster): split into runner Dockerfile (this) + shard-manag…
Mudit-Lal May 6, 2026
6cda587
build(web-cluster): add Dockerfile.shard-manager with hardcoded CMD
Mudit-Lal May 6, 2026
40343f2
fix(utils): publishConfig.exports points to dist for deployed builds
Mudit-Lal May 6, 2026
4333fec
fix(shard-manager): bind to 0.0.0.0 instead of localhost
Mudit-Lal May 6, 2026
6c74863
fix(shard-manager): RunnerAddress.make takes positional args
Mudit-Lal May 6, 2026
16c816a
fix(runner): allow RUNNER_HOST env override for advertised address
Mudit-Lal May 6, 2026
964620e
skip org email restriction for public (anyone-with-link) videos
Mudit-Lal May 6, 2026
d1e23a0
fix: hasActiveUpload check phase instead of existence
Mudit-Lal May 6, 2026
9bb8dcd
fix: get-status active upload check by phase not existence
Mudit-Lal May 6, 2026
4b4bde2
fix: treat upload as active only when phase=uploading AND uploaded < …
Mudit-Lal May 6, 2026
b60143f
fix(get-status): skip transcription only when upload truly in progress
Mudit-Lal May 6, 2026
ade28f2
fix(transcribe): allow transcription when phase=uploading but upload …
Mudit-Lal May 6, 2026
5ad3041
fix(transcribe): bypass workflow start() — call workflow fn directly
Mudit-Lal May 6, 2026
4a0c381
revert: use start() again now that VERCEL_URL will be set on Railway
Mudit-Lal May 6, 2026
60af444
fix(transcribe): replace workflow start() with inline direct execution
Mudit-Lal May 6, 2026
b179056
fix(transcribe): don't block transcription when phase=processing
Mudit-Lal May 6, 2026
61ae75d
bypass generateAiWorkflow start() with direct OpenAI call on Railway
Mudit-Lal May 6, 2026
7e6f5e1
treat QUEUED aiGenerationStatus as retriable on Railway
Mudit-Lal May 6, 2026
4fbd30c
bypass processVideoWorkflow start() with direct media server call
Mudit-Lal May 6, 2026
d74e65e
fix(embed): allow /embed/ routes on self-hosted instances
Mudit-Lal May 6, 2026
435eaf6
brand: replace Cap blue with Devalok brand color (#D33163)
Mudit-Lal May 6, 2026
f9ab257
brand: replace blue scale with Devalok pink in tailwind config and Bu…
Mudit-Lal May 6, 2026
1cce41b
fix(css): restore blockquote content unicode escape broken by brand c…
Mudit-Lal May 6, 2026
8116535
fix(proxy): allow static public files on self-hosted
Mudit-Lal May 6, 2026
69c5ace
fix(proxy): whitelist remaining authenticated and linked routes
Mudit-Lal May 6, 2026
a005fab
fix(proxy): whitelist /.well-known/ to unblock in-process workflows
Mudit-Lal May 6, 2026
0394476
revert: remove direct workflow bypasses now that start() works
Mudit-Lal May 6, 2026
39ce8b3
fix(transcribe): mark transcriptionStatus=PROCESSING before start()
Mudit-Lal May 6, 2026
0f2da37
feat(web): add PostHog ui_host for f.devalok.in proxy
Mudit-Lal Jul 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/media-server/railway.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build]
dockerfilePath = "apps/media-server/Dockerfile"
dockerfilePath = "Dockerfile.standalone"

[deploy]
startCommand = "bun run src/index.ts"
Expand Down
6 changes: 3 additions & 3 deletions apps/web-cluster/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ COPY . .
RUN corepack enable pnpm

RUN echo "inject-workspace-packages=true" >> .npmrc
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm i
RUN pnpm i

RUN pnpm run --filter=@cap/web-cluster build
RUN pnpm deploy --filter=@cap/web-cluster out
Expand All @@ -21,8 +21,8 @@ COPY --from=builder --chown=deno:deno /app/out /app

USER deno

ENTRYPOINT ["deno", "run", "--allow-all"]

EXPOSE 8080
EXPOSE 42069
EXPOSE 42169

CMD ["deno", "run", "--allow-all", "src/runner/index.ts"]
27 changes: 27 additions & 0 deletions apps/web-cluster/Dockerfile.shard-manager
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
FROM node:24-slim AS base
RUN corepack enable

FROM base AS builder
WORKDIR /app
COPY . .

RUN corepack enable pnpm

RUN echo "inject-workspace-packages=true" >> .npmrc
RUN pnpm i

RUN pnpm run --filter=@cap/web-cluster build
RUN pnpm deploy --filter=@cap/web-cluster out
RUN cd out && node scripts/post-deploy.ts

FROM denoland/deno:2.5.3 AS runner
WORKDIR /app

COPY --from=builder --chown=deno:deno /app/out /app

USER deno

EXPOSE 8080
EXPOSE 42069

CMD ["deno", "run", "--allow-all", "src/shard-manager.ts"]
36 changes: 22 additions & 14 deletions apps/web-cluster/src/cluster/container-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class EcsContainerMetadata extends Effect.Service<EcsContainerMetadata>()(
metadataUri: yield* Config.option(
Config.string("ECS_CONTAINER_METADATA_URI_V4"),
),
runnerHostOverride: yield* Config.option(
Config.string("RUNNER_HOST"),
),
};
}),
},
Expand Down Expand Up @@ -42,20 +45,25 @@ export class ContainerMetadata extends Effect.Service<ContainerMetadata>()(
{
effect: Effect.gen(function* () {
const containerMetadata = yield* EcsContainerMetadata;
const metadataUri = containerMetadata.metadataUri;
const ipAddress = yield* Option.match(metadataUri, {
onNone: () => Effect.succeed("0.0.0.0"),
onSome: (uri) =>
Effect.tryPromise({
try: async () => {
const response = await fetch(`${uri}/task`);
const data = await response.json();
return data.Containers[0].Networks[0].IPv4Addresses[0] as string;
},
catch: (error) => {
console.error("error", error);
return new FetchIpError();
},
const { metadataUri, runnerHostOverride } = containerMetadata;
const ipAddress = yield* Option.match(runnerHostOverride, {
onSome: (host) => Effect.succeed(host),
onNone: () =>
Option.match(metadataUri, {
onNone: () => Effect.succeed("0.0.0.0"),
onSome: (uri) =>
Effect.tryPromise({
try: async () => {
const response = await fetch(`${uri}/task`);
const data = await response.json();
return data.Containers[0].Networks[0]
.IPv4Addresses[0] as string;
},
catch: (error) => {
console.error("error", error);
return new FetchIpError();
},
}),
}),
});

Expand Down
4 changes: 4 additions & 0 deletions apps/web-cluster/src/shard-manager.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { RunnerAddress } from "@effect/cluster";
import {
NodeClusterShardManagerSocket,
NodeRuntime,
Expand All @@ -8,6 +9,9 @@ import { DatabaseLive, ShardDatabaseLive } from "./shared/database.ts";

NodeClusterShardManagerSocket.layer({
storage: "sql",
shardingConfig: {
shardManagerAddress: RunnerAddress.make("0.0.0.0", 8080),
},
}).pipe(
Layer.provide(ShardDatabaseLive),
Layer.provide(DatabaseLive),
Expand Down
4 changes: 1 addition & 3 deletions apps/web/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# syntax=docker.io/docker/dockerfile:1

FROM node:24-alpine AS base
RUN corepack enable

Expand All @@ -8,7 +6,7 @@ WORKDIR /app
COPY . .

RUN corepack enable pnpm
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm i --frozen-lockfile
RUN pnpm i --frozen-lockfile

ARG NEXT_PUBLIC_DOCKER_BUILD=true
ENV NEXT_PUBLIC_WEB_URL=http://localhost:3000
Expand Down
18 changes: 15 additions & 3 deletions apps/web/actions/videos/get-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { VideoMetadata } from "@cap/database/types";
import { serverEnv } from "@cap/env";
import { provideOptionalAuth, VideosPolicy } from "@cap/web-backend";
import { Policy, type Video } from "@cap/web-domain";
import { eq } from "drizzle-orm";
import { and, eq, or, sql } from "drizzle-orm";
import { Effect, Exit } from "effect";
import { startAiGeneration } from "@/lib/generate-ai";
import * as EffectRuntime from "@/lib/server";
Expand Down Expand Up @@ -60,7 +60,18 @@ export async function getVideoStatus(
const activeUpload = await db()
.select({ videoId: videoUploads.videoId })
.from(videoUploads)
.where(eq(videoUploads.videoId, videoId))
.where(
and(
eq(videoUploads.videoId, videoId),
or(
eq(videoUploads.phase, "generating_thumbnail"),
and(
eq(videoUploads.phase, "uploading"),
sql`${videoUploads.uploaded} < ${videoUploads.total}`,
),
),
),
)
.limit(1);

if (activeUpload.length > 0) {
Expand Down Expand Up @@ -124,7 +135,8 @@ export async function getVideoStatus(

const shouldTriggerAiGeneration =
video.transcriptionStatus === "COMPLETE" &&
!metadata.aiGenerationStatus &&
(!metadata.aiGenerationStatus ||
metadata.aiGenerationStatus === "QUEUED") &&
!metadata.summary &&
(serverEnv().GROQ_API_KEY || serverEnv().OPENAI_API_KEY);

Expand Down
1 change: 1 addition & 0 deletions apps/web/app/Layout/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function PostHogProvider({
if (!host) return undefined;
const base = {
api_host: host,
ui_host: "https://eu.posthog.com",
capture_pageview: false,
capture_pageleave: true,
bootstrap: initialBootstrap.current?.distinctID
Expand Down
3 changes: 2 additions & 1 deletion apps/web/app/api/videos/[videoId]/retry-ai/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export async function POST(
const canRetry =
!metadata.aiGenerationStatus ||
metadata.aiGenerationStatus === "ERROR" ||
metadata.aiGenerationStatus === "SKIPPED";
metadata.aiGenerationStatus === "SKIPPED" ||
metadata.aiGenerationStatus === "QUEUED";

if (!canRetry) {
return Response.json(
Expand Down
37 changes: 25 additions & 12 deletions apps/web/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,23 @@
@import "@radix-ui/colors/gray.css";
@import "@radix-ui/colors/gray-alpha.css";
@import "@radix-ui/colors/gray-dark.css";
@import "@radix-ui/colors/blue.css";
@import "@radix-ui/colors/crimson.css";
@import "@radix-ui/colors/crimson-dark.css";

@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
--primary: #005cb1;
--primary-2: #004c93;
--primary-3: #003b73;
--secondary: #2eb4ff;
--secondary-2: #1696e0;
--secondary-3: #117ebd;
--tertiary: #c5eaff;
--tertiary-2: #d3e5ff;
--tertiary-3: #e0edff;
--primary: #d33163;
--primary-2: #b8284f;
--primary-3: #912243;
--secondary: #f07ba0;
--secondary-2: #d33163;
--secondary-3: #b8284f;
--tertiary: #ffd5e2;
--tertiary-2: #ffdce8;
--tertiary-3: #ffe8f0;
--filler: #efefef;
--filler-2: #e4e4e4;
--filler-3: #e2e2e2;
Expand All @@ -32,6 +33,18 @@
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
--gradient-border-radius: 12px;
--blue-1: var(--crimson-1);
--blue-2: var(--crimson-2);
--blue-3: var(--crimson-3);
--blue-4: var(--crimson-4);
--blue-5: var(--crimson-5);
--blue-6: var(--crimson-6);
--blue-7: var(--crimson-7);
--blue-8: var(--crimson-8);
--blue-9: var(--crimson-9);
--blue-10: var(--crimson-10);
--blue-11: var(--crimson-11);
--blue-12: var(--crimson-12);
}

@media (prefers-color-scheme: dark) {
Expand Down Expand Up @@ -150,7 +163,7 @@ html {
:where(blockquote p:first-of-type):not(
:where([class~="not-prose"], [class~="not-prose"] *)
)::before {
content: "";
content: "\201c";
display: none;
}

Expand Down Expand Up @@ -381,7 +394,7 @@ label {
inset: 0;
padding: 1px;
border-radius: 100px;
background: linear-gradient(to bottom, #446fae 20%, transparent 90%);
background: linear-gradient(to bottom, #b8284f 20%, transparent 90%);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/s/[videoId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ export default async function ShareVideoPage(props: PageProps<"/s/[videoId]">) {
organizationId: sharedVideos.organizationId,
},
orgSettings: organizations.settings,
hasActiveUpload: sql`${videoUploads.videoId} IS NOT NULL`.mapWith(
hasActiveUpload: sql`(${videoUploads.phase} IN ('processing', 'generating_thumbnail') OR (${videoUploads.phase} = 'uploading' AND ${videoUploads.uploaded} < ${videoUploads.total}))`.mapWith(
Boolean,
),
owner: users,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/generate-ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export async function startAiGeneration(

return {
success: false,
message: "Failed to start AI generation workflow",
message: "Failed to start AI generation",
};
}
}
16 changes: 13 additions & 3 deletions apps/web/lib/transcribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,11 @@ export async function transcribeVideo(
}

const upload = await db()
.select({ phase: videoUploads.phase })
.select({
phase: videoUploads.phase,
uploaded: videoUploads.uploaded,
total: videoUploads.total,
})
.from(videoUploads)
.where(eq(videoUploads.videoId, videoId))
.limit(1);
Expand All @@ -122,6 +126,11 @@ export async function transcribeVideo(
`[transcribeVideo] Triggering transcription workflow for video ${videoId}`,
);

await db()
.update(videos)
.set({ transcriptionStatus: "PROCESSING" })
.where(eq(videos.id, videoId));

await start(transcribeVideoWorkflow, [
{
videoId,
Expand All @@ -135,7 +144,7 @@ export async function transcribeVideo(
message: "Transcription workflow started",
};
} catch (error) {
console.error("[transcribeVideo] Failed to trigger workflow:", error);
console.error("[transcribeVideo] Failed to start transcription:", error);

await db()
.update(videos)
Expand All @@ -144,7 +153,8 @@ export async function transcribeVideo(

return {
success: false,
message: "Failed to start transcription workflow",
message: "Failed to start transcription",
};
}
}

6 changes: 3 additions & 3 deletions apps/web/lib/video-processing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ export async function startVideoProcessingWorkflow({
rawFileKey,
bucketId,
processingMessage,
startFailureMessage,
mode,
forceRestart,
}: {
Expand Down Expand Up @@ -127,17 +126,18 @@ export async function startVideoProcessingWorkflow({
bucketId: bucketId as S3Bucket.S3BucketId | null,
},
]);
return "started";
} catch (error) {
const normalizedError =
error instanceof Error
? error
: new Error("Video processing could not start");
await setVideoProcessingError(
videoId,
startFailureMessage,
"Video processing failed to start",
normalizedError,
);
throw normalizedError;
}

return "started";
}
10 changes: 9 additions & 1 deletion apps/web/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export async function proxy(request: NextRequest) {
if (
!(
path.startsWith("/s/") ||
path.startsWith("/embed/") ||
path.startsWith("/middleware") ||
path.startsWith("/dashboard") ||
path.startsWith("/onboarding") ||
Expand All @@ -49,7 +50,14 @@ export async function proxy(request: NextRequest) {
path.startsWith("/invite") ||
path.startsWith("/self-hosting") ||
path.startsWith("/terms") ||
path.startsWith("/verify-otp")
path.startsWith("/privacy") ||
path.startsWith("/verify-otp") ||
path.startsWith("/admin") ||
path.startsWith("/dev") ||
path.startsWith("/messenger") ||
path.startsWith("/docs") ||
path.startsWith("/.well-known/") ||
/\.[a-z0-9]+$/i.test(path)
) &&
process.env.NODE_ENV !== "development"
)
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const buttonVariants = cva(
darkgradient:
"bg-gradient-to-t button-gradient-border from-[#0f0f0f] to-[#404040] shadow-[0_0_0_1px] hover:brightness-110 shadow-[#383838] text-gray-50 hover:bg-[#383838] disabled:bg-[#383838] border-transparent",
radialblue:
"text-gray-50 border button-gradient-border shadow-[0_0_0_1px] shadow-blue-400 disabled:bg-gray-1 border-0 [background:radial-gradient(90%_100%_at_15%_12%,#9BC4FF_0%,#3588FF_100%)] border-transparent hover:opacity-80",
"text-gray-50 border button-gradient-border shadow-[0_0_0_1px] shadow-[#F07BA0] disabled:bg-gray-1 border-0 [background:radial-gradient(90%_100%_at_15%_12%,#F07BA0_0%,#D33163_100%)] border-transparent hover:opacity-80",
transparent:
"bg-transparent text-gray-10 hover:underline transition-all duration-200 hover:text-gray-12",
},
Expand Down
Loading