From fc3752bbe93df1ebd467af3c4c547b7ae5cca812 Mon Sep 17 00:00:00 2001
From: Kartikeya <176560021+karti-ai@users.noreply.github.com>
Date: Sat, 27 Jun 2026 14:56:04 -0700
Subject: [PATCH] feat(frontend): dev mock-join fallback when LiveKit
unconfigured
If the backend returns the placeholder LiveKit URL, skip the real connect and
screen capture and drop into the post-join state (with a DEV MODE banner) so
the connected UI is developable without LiveKit creds or HTTPS. Real joins
also guard getDisplayMedia behind isSecureContext.
Co-Authored-By: Claude Opus 4.8
---
frontend/src/App.tsx | 19 ++++++++++++++++---
frontend/src/lib/pod.ts | 39 +++++++++++++++++++++++++++++----------
2 files changed, 45 insertions(+), 13 deletions(-)
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 60063a4..1c5d8b2 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -8,6 +8,8 @@ export default function App() {
const team = useMemo(() => TEAMS.find((t) => t.id === teamId) ?? TEAMS[0]!, [teamId]);
const [member, setMember] = useState(team.members[0]!);
const [room, setRoom] = useState(null);
+ const [joined, setJoined] = useState(false);
+ const [devMode, setDevMode] = useState(false);
const [error, setError] = useState(null);
const [connecting, setConnecting] = useState(false);
@@ -22,7 +24,10 @@ export default function App() {
setConnecting(true);
try {
const identity = `${member}-${Math.random().toString(36).slice(2, 7)}`;
- setRoom(await joinPod(team.id, identity, member));
+ const result = await joinPod(team.id, identity, member);
+ setRoom(result.room);
+ setDevMode(result.mode === 'dev');
+ setJoined(true);
} catch (err) {
setError((err as Error).message);
} finally {
@@ -39,12 +44,20 @@ export default function App() {
- {room ? (
+ {joined ? (
{member} connected to “{team.name}”.
- Sharing screen + mic. PodMan is watching.
+
+ {room ? 'Sharing screen + mic. PodMan is watching.' : 'Pod joined.'}
+
+ {devMode && (
+
+ DEV MODE — LiveKit not configured, so screen capture is off. Set LIVEKIT_* in the
+ backend .env (and serve over HTTPS) for a real join.
+
+ )}
) : (
diff --git a/frontend/src/lib/pod.ts b/frontend/src/lib/pod.ts
index 7fdcdd5..dbabd7b 100644
--- a/frontend/src/lib/pod.ts
+++ b/frontend/src/lib/pod.ts
@@ -2,7 +2,7 @@ import { Room, RoomEvent } from 'livekit-client';
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8787';
-/** Ask the backend for a LiveKit token to join a pod. */
+/** Token + LiveKit URL minted by the backend. */
export async function fetchPodToken(
podId: string,
identity: string,
@@ -17,22 +17,41 @@ export async function fetchPodToken(
return res.json();
}
+/** True once a real LiveKit server is configured (not the placeholder). */
+export function isLiveKitConfigured(url: string | undefined): boolean {
+ return !!url && !url.includes('REPLACE_ME');
+}
+
+export type JoinResult = { mode: 'live'; room: Room } | { mode: 'dev'; room: null };
+
/**
- * Join a pod: connect to the LiveKit room and publish screen + mic so PodMan
- * can watch. Returns the connected Room.
+ * Join a pod. When LiveKit is configured we connect for real and publish
+ * screen + mic. Otherwise we fall back to a dev mock join so the post-join UI
+ * is developable without LiveKit creds / HTTPS.
*/
-export async function joinPod(podId: string, identity: string, name: string): Promise {
+export async function joinPod(podId: string, identity: string, name: string): Promise {
const { token, url } = await fetchPodToken(podId, identity, name);
+
+ if (!isLiveKitConfigured(url)) {
+ console.warn('[podman] LiveKit not configured — dev mock join (no screen capture)');
+ return { mode: 'dev', room: null };
+ }
+
const room = new Room({ adaptiveStream: true, dynacast: true });
room.on(RoomEvent.Disconnected, () => console.log('[podman] disconnected'));
-
await room.connect(url, token);
- const screen = await navigator.mediaDevices.getDisplayMedia({ video: true });
- for (const track of screen.getTracks()) {
- await room.localParticipant.publishTrack(track);
+ // Screen capture needs a secure context (HTTPS or localhost). Guard so a
+ // non-secure origin doesn't hard-crash the join.
+ if (window.isSecureContext && navigator.mediaDevices?.getDisplayMedia) {
+ const screen = await navigator.mediaDevices.getDisplayMedia({ video: true });
+ for (const track of screen.getTracks()) {
+ await room.localParticipant.publishTrack(track);
+ }
+ await room.localParticipant.setMicrophoneEnabled(true);
+ } else {
+ console.warn('[podman] insecure context — screen capture skipped (needs HTTPS)');
}
- await room.localParticipant.setMicrophoneEnabled(true);
- return room;
+ return { mode: 'live', room };
}