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 }; }