feat: harden podman deployment orchestration

This commit is contained in:
Yahya Alhinai
2026-06-28 01:44:26 +00:00
parent 5185845090
commit c818d5081e
37 changed files with 1796 additions and 273 deletions
+95 -58
View File
@@ -9,6 +9,7 @@ import {
SparklesIcon,
UsersIcon,
WifiIcon,
ShieldCheckIcon,
} from 'lucide-react';
import type { Pod, PodInput } from '@podman/shared';
import { joinPod } from './lib/pod.js';
@@ -233,34 +234,42 @@ export default function App() {
return (
<div className="min-h-screen bg-background text-foreground">
<div className="mx-auto flex min-h-screen w-full max-w-7xl flex-col gap-6 px-4 py-4 sm:px-6 lg:px-8">
<header className="flex flex-col gap-5 border-b pb-5">
<div className="mx-auto flex min-h-screen w-full max-w-[1440px] flex-col gap-6 px-4 py-4 sm:px-6 lg:px-8">
<header className="sticky top-0 z-10 -mx-4 flex flex-col gap-5 border-b bg-background/86 px-4 pb-5 pt-2 backdrop-blur-xl sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div className="flex min-w-0 items-center gap-3">
<div className="grid size-10 place-items-center rounded-xl bg-primary text-sm font-medium text-primary-foreground">
<div className="grid size-10 place-items-center rounded-lg bg-primary text-sm font-semibold text-primary-foreground shadow-sm">
PM
</div>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<h1 className="text-3xl font-semibold tracking-tight">PodMan</h1>
<h1 className="text-[1.95rem] font-semibold leading-none tracking-tight">
PodMan
</h1>
<Badge variant={podManOnline ? 'default' : 'secondary'}>
<CircleDotIcon data-icon="inline-start" />
{podManOnline ? 'online' : 'standby'}
</Badge>
</div>
<p className="text-sm text-muted-foreground">
Quiet coordination for live engineering rooms.
Live engineering rooms, team memory, and intervention routing.
</p>
</div>
</div>
<Button variant="outline" onClick={() => void refresh()} disabled={loading}>
<RefreshCwIcon data-icon="inline-start" />
Refresh
</Button>
<div className="flex items-center gap-2">
<Badge variant="outline" className="h-8 rounded-lg px-3">
<ShieldCheckIcon data-icon="inline-start" />
Privacy-limited
</Badge>
<Button variant="outline" onClick={() => void refresh()} disabled={loading}>
<RefreshCwIcon data-icon="inline-start" />
Refresh
</Button>
</div>
</div>
<div className="grid gap-2 sm:grid-cols-4">
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
<StatPill icon={WifiIcon} label="Live" value={fmt.format(liveTotal)} />
<StatPill icon={RadioTowerIcon} label="Rooms" value={fmt.format(activeRooms)} />
<StatPill icon={UsersIcon} label="Roster" value={fmt.format(totalMembers)} />
@@ -292,55 +301,74 @@ export default function App() {
</CardContent>
</Card>
) : (
<main className="flex min-w-0 flex-1 flex-col gap-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0">
<h2 className="text-lg font-medium">Workspaces</h2>
<p className="text-sm text-muted-foreground">
Join a room, publish your screen, and let PodMan watch for overlap.
<main className="grid min-w-0 flex-1 gap-5 lg:grid-cols-[minmax(0,1fr)_360px]">
<section className="flex min-w-0 flex-col gap-4">
<div className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
<div className="min-w-0">
<p className="text-xs font-medium uppercase text-muted-foreground">Workspaces</p>
<h2 className="text-xl font-semibold tracking-tight">Active pods</h2>
</div>
<p className="max-w-xl text-sm leading-6 text-muted-foreground">
Join the room that matches your current workstream.
</p>
</div>
</div>
{loading ? (
<div className="grid gap-4 xl:grid-cols-2">
<PodSkeleton />
<PodSkeleton />
</div>
) : pods.length ? (
<div className="grid gap-4 xl:grid-cols-2">
{pods.map((pod) => (
<PodCard
key={pod.id}
pod={pod}
busy={pending.has(pod.id)}
presence={presence[pod.id] ?? []}
onJoin={handleJoin}
onAddAndJoin={handleAddAndJoin}
onAddMember={handleAddMember}
onRemoveMember={handleRemoveMember}
onUpdate={handleUpdate}
onDelete={handleDelete}
{loading ? (
<div className="grid gap-4 xl:grid-cols-2">
<PodSkeleton />
<PodSkeleton />
</div>
) : pods.length ? (
<div className="grid gap-4 xl:grid-cols-2">
{pods.map((pod) => (
<PodCard
key={pod.id}
pod={pod}
busy={pending.has(pod.id)}
presence={presence[pod.id] ?? []}
onJoin={handleJoin}
onAddAndJoin={handleAddAndJoin}
onAddMember={handleAddMember}
onRemoveMember={handleRemoveMember}
onUpdate={handleUpdate}
onDelete={handleDelete}
/>
))}
</div>
) : (
<Empty className="min-h-[360px] rounded-lg border bg-card">
<EmptyHeader>
<EmptyMedia variant="icon">
<SparklesIcon />
</EmptyMedia>
<EmptyTitle>No pods yet</EmptyTitle>
<EmptyDescription>Create the first room for this team.</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<CreatePodForm busy={pending.has('new')} onCreate={handleCreate} compact />
</EmptyContent>
</Empty>
)}
</section>
<aside className="flex flex-col gap-4">
<CreatePodForm busy={pending.has('new')} onCreate={handleCreate} />
<Card>
<CardHeader>
<CardTitle>Operating brief</CardTitle>
<CardDescription>Current coordination signals.</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<BriefLine label="Notification default" value="Card" />
<BriefLine label="Escalation" value="Hermes voice only when urgent" />
<BriefLine label="Memory events" value={fmt.format(latestActivity)} />
<BriefLine
label="Live people"
value={liveNames.length ? liveNames.join(', ') : 'None'}
/>
))}
<CreatePodForm busy={pending.has('new')} onCreate={handleCreate} />
</div>
) : (
<Empty className="min-h-[360px] border">
<EmptyHeader>
<EmptyMedia variant="icon">
<SparklesIcon />
</EmptyMedia>
<EmptyTitle>No pods yet</EmptyTitle>
<EmptyDescription>
Create the first pod to start a LiveKit room and coordination loop.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<CreatePodForm busy={pending.has('new')} onCreate={handleCreate} compact />
</EmptyContent>
</Empty>
)}
</CardContent>
</Card>
</aside>
</main>
)}
</div>
@@ -358,18 +386,27 @@ function StatPill({
value: string;
}) {
return (
<div className="flex items-center gap-3 rounded-xl border bg-card px-3 py-2">
<div className="grid size-8 place-items-center rounded-lg bg-muted">
<div className="flex min-h-16 items-center gap-3 rounded-lg border bg-card/90 px-3 py-2 shadow-sm">
<div className="grid size-8 place-items-center rounded-md bg-muted">
<Icon className="size-4 text-muted-foreground" />
</div>
<div className="min-w-0">
<p className="text-xs text-muted-foreground">{label}</p>
<p className="text-xs font-medium uppercase text-muted-foreground">{label}</p>
<p className="text-base font-medium">{value}</p>
</div>
</div>
);
}
function BriefLine({ label, value }: { label: string; value: string }) {
return (
<div className="flex items-start justify-between gap-4 rounded-md bg-muted/45 px-3 py-2">
<span className="text-sm text-muted-foreground">{label}</span>
<span className="max-w-44 text-right text-sm font-medium">{value}</span>
</div>
);
}
function PodSkeleton() {
return (
<Card>
+4 -4
View File
@@ -50,10 +50,10 @@ export function CreatePodForm({
if (!open) {
return (
<button
className="group flex min-h-64 flex-col items-center justify-center gap-3 rounded-xl border border-dashed bg-card p-6 text-center transition hover:bg-muted/30"
className="group flex min-h-64 flex-col items-center justify-center gap-3 rounded-lg border border-dashed bg-card p-6 text-center shadow-sm transition hover:bg-muted/30 hover:ring-1 hover:ring-black/10"
onClick={() => setOpen(true)}
>
<span className="grid size-9 place-items-center rounded-lg bg-muted transition group-hover:bg-background">
<span className="grid size-9 place-items-center rounded-md bg-muted transition group-hover:bg-background">
<PlusIcon className="size-4 text-muted-foreground" />
</span>
<span className="text-sm font-medium">New pod</span>
@@ -68,7 +68,7 @@ export function CreatePodForm({
<Card className={cn(compact && 'w-full border-0 shadow-none ring-0')}>
<CardHeader>
<CardTitle>New pod</CardTitle>
<CardDescription>Keep the room name short and specific.</CardDescription>
<CardDescription>Create a focused room.</CardDescription>
</CardHeader>
<CardContent>
<FieldGroup>
@@ -117,7 +117,7 @@ export function CreatePodForm({
Cancel
</Button>
)}
<Button onClick={submit} disabled={busy || !name.trim()}>
<Button className="min-w-20" onClick={submit} disabled={busy || !name.trim()}>
Create
</Button>
</CardFooter>
+11 -5
View File
@@ -84,10 +84,12 @@ export function PodCard({
return (
<>
<Card className="min-h-64 transition duration-200 hover:-translate-y-0.5 hover:ring-foreground/15">
<Card className="min-h-64 transition duration-200 hover:-translate-y-0.5 hover:shadow-[0_2px_6px_rgba(0,0,0,0.06),0_28px_70px_rgba(0,0,0,0.06)] hover:ring-black/15">
<CardHeader>
<CardTitle className="truncate">{pod.name}</CardTitle>
<CardDescription className="truncate">{pod.repo || 'No repository set'}</CardDescription>
<CardTitle className="truncate text-[1.05rem]">{pod.name}</CardTitle>
<CardDescription className="truncate font-mono text-xs">
{pod.repo || 'No repository set'}
</CardDescription>
<CardAction>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -121,7 +123,7 @@ export function PodCard({
{pod.description || 'Focused workspace for live engineering coordination.'}
</p>
</div>
<Badge variant={active ? 'default' : 'secondary'}>
<Badge variant={active ? 'default' : 'secondary'} className="rounded-md">
{active ? `${presence.length} live` : 'quiet'}
</Badge>
</div>
@@ -175,7 +177,11 @@ export function PodCard({
<VideoIcon data-icon="inline-start" />
Join
</Button>
<Button onClick={() => submitMember(true)} disabled={busy || !newMember.trim()}>
<Button
className="min-w-28"
onClick={() => submitMember(true)}
disabled={busy || !newMember.trim()}
>
Add and join
</Button>
</CardFooter>
+16 -14
View File
@@ -185,8 +185,8 @@ export function PodView({
return (
<div className="min-h-screen bg-background text-foreground">
<div className="mx-auto flex min-h-screen w-full max-w-7xl flex-col gap-5 px-4 py-4 sm:px-6 lg:px-8">
<header className="flex items-center justify-between gap-4 border-b pb-4">
<div className="mx-auto flex min-h-screen w-full max-w-[1440px] flex-col gap-5 px-4 py-4 sm:px-6 lg:px-8">
<header className="sticky top-0 z-10 -mx-4 flex flex-col gap-3 border-b bg-background/86 px-4 pb-4 pt-2 backdrop-blur-xl sm:-mx-6 sm:px-6 md:flex-row md:items-center md:justify-between lg:-mx-8 lg:px-8">
<div className="flex min-w-0 items-center gap-3">
<Tooltip>
<TooltipTrigger asChild>
@@ -200,13 +200,15 @@ export function PodView({
<div className="min-w-0">
<div className="flex items-center gap-2">
<h1 className="truncate text-xl font-semibold tracking-tight">{team.name}</h1>
<Badge variant={room ? 'default' : 'secondary'}>{room ? 'live' : 'local'}</Badge>
<Badge variant={room ? 'default' : 'secondary'} className="rounded-md">
{room ? 'live' : 'local'}
</Badge>
</div>
<p className="truncate text-sm text-muted-foreground">{team.repo}</p>
<p className="truncate font-mono text-xs text-muted-foreground">{team.repo}</p>
</div>
</div>
<div className="flex items-center gap-2">
<div className="grid grid-cols-2 gap-2 sm:flex sm:items-center">
<Button variant="outline" onClick={toggleBeat} disabled={!room}>
<Volume2Icon data-icon="inline-start" />
{playingBeat ? 'Stop audio' : 'Test audio'}
@@ -244,8 +246,8 @@ export function PodView({
<Card className="flex-1">
<CardHeader>
<CardTitle>Room</CardTitle>
<CardDescription>Only the signals that matter right now.</CardDescription>
<CardTitle>Room state</CardTitle>
<CardDescription>People and media currently visible to PodMan.</CardDescription>
</CardHeader>
<CardContent>
{participants.length === 0 ? (
@@ -278,9 +280,9 @@ export function PodView({
<Card>
<CardHeader>
<CardTitle>Intervention</CardTitle>
<CardDescription>PodMan stays quiet until the signal is useful.</CardDescription>
<CardDescription>Card first, voice only for urgent escalation.</CardDescription>
<CardAction>
<Badge variant={active ? 'default' : 'secondary'}>
<Badge variant={active ? 'default' : 'secondary'} className="rounded-md">
{active ? 'active' : 'clear'}
</Badge>
</CardAction>
@@ -288,7 +290,7 @@ export function PodView({
<CardContent>
{active ? (
<div className="flex flex-col gap-4">
<div className="rounded-xl border bg-muted/30 p-3">
<div className="rounded-lg border bg-muted/35 p-3">
<p className="text-sm leading-6">{active.message}</p>
</div>
<div className="flex items-center justify-between gap-3 text-sm">
@@ -306,8 +308,8 @@ export function PodView({
</EmptyMedia>
<EmptyTitle>No collision detected</EmptyTitle>
<EmptyDescription>
Share your screen when ready. The agent will surface only meaningful
overlap.
Share your screen when ready. PodMan will stay quiet until there is a useful
signal.
</EmptyDescription>
</EmptyHeader>
</Empty>
@@ -372,7 +374,7 @@ function Participant({ participant }: { participant: PInfo }) {
return (
<div
className={cn(
'flex min-h-16 items-center justify-between gap-3 rounded-xl border bg-muted/20 px-3 transition',
'flex min-h-16 items-center justify-between gap-3 rounded-lg border bg-muted/20 px-3 transition',
participant.speaking && 'bg-muted',
)}
>
@@ -386,7 +388,7 @@ function Participant({ participant }: { participant: PInfo }) {
<p className="text-xs text-muted-foreground">{participant.isLocal ? 'you' : 'remote'}</p>
</div>
</div>
<Badge variant={participant.speaking ? 'default' : 'secondary'}>
<Badge variant={participant.speaking ? 'default' : 'secondary'} className="rounded-md">
{participant.speaking ? 'speaking' : 'connected'}
</Badge>
</div>
+5 -6
View File
@@ -5,7 +5,7 @@ import { Slot } from 'radix-ui';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
@@ -23,14 +23,13 @@ const buttonVariants = cva(
size: {
default:
'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
xs: "h-6 gap-1 rounded-md px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-md px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
icon: 'size-8',
'icon-xs':
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
'icon-sm':
'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
"size-6 rounded-md in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
'icon-sm': 'size-7 rounded-md in-data-[slot=button-group]:rounded-md',
'icon-lg': 'size-9',
},
},
+3 -3
View File
@@ -12,7 +12,7 @@ function Card({
data-slot="card"
data-size={size}
className={cn(
'group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
'group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-lg bg-card py-(--card-spacing) text-sm text-card-foreground shadow-[0_1px_2px_rgba(0,0,0,0.04),0_20px_60px_rgba(0,0,0,0.04)] ring-1 ring-black/[0.07] [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg',
className,
)}
{...props}
@@ -25,7 +25,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
<div
data-slot="card-header"
className={cn(
'group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)',
'group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-lg px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)',
className,
)}
{...props}
@@ -77,7 +77,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
<div
data-slot="card-footer"
className={cn(
'flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)',
'flex items-center rounded-b-lg border-t bg-muted/35 p-(--card-spacing)',
className,
)}
{...props}
+25 -23
View File
@@ -7,33 +7,33 @@
:root {
color-scheme: light;
--background: oklch(0.985 0 0);
--foreground: oklch(0.145 0 0);
--background: oklch(0.986 0.002 247.84);
--foreground: oklch(0.17 0.004 255.9);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--card-foreground: oklch(0.17 0.004 255.9);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.18 0 0);
--popover-foreground: oklch(0.17 0.004 255.9);
--primary: oklch(0.19 0.004 255.9);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.96 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.955 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.955 0 0);
--accent-foreground: oklch(0.205 0 0);
--secondary: oklch(0.956 0.003 247.86);
--secondary-foreground: oklch(0.26 0.004 255.9);
--muted: oklch(0.962 0.003 247.86);
--muted-foreground: oklch(0.52 0.006 255.9);
--accent: oklch(0.955 0.004 247.86);
--accent-foreground: oklch(0.24 0.004 255.9);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--border: oklch(0.905 0.004 247.86);
--input: oklch(0.905 0.004 247.86);
--ring: oklch(0.62 0.012 255.9);
--chart-1: oklch(0.67 0.12 250);
--chart-2: oklch(0.68 0.11 155);
--chart-3: oklch(0.72 0.14 80);
--chart-4: oklch(0.63 0.16 20);
--chart-5: oklch(0.55 0.11 290);
--radius: 0.5rem;
--sidebar: oklch(0.986 0.002 247.84);
--sidebar-foreground: oklch(0.17 0.004 255.9);
--sidebar-primary: oklch(0.24 0.004 255.9);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
@@ -47,6 +47,8 @@ body {
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(247, 248, 250, 0.98)), var(--background);
}
button,
+5 -1
View File
@@ -1,6 +1,10 @@
import type { InterventionOutcome, Pod, PodInput } from '@podman/shared';
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8787';
const BACKEND_URL =
import.meta.env.VITE_BACKEND_URL ||
(import.meta.env.DEV || ['localhost', '127.0.0.1'].includes(window.location.hostname)
? 'http://localhost:8787'
: '');
export interface MemoryStats {
observations: number;
+5 -1
View File
@@ -1,6 +1,10 @@
import { Room, RoomEvent } from 'livekit-client';
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8787';
const BACKEND_URL =
import.meta.env.VITE_BACKEND_URL ||
(import.meta.env.DEV || ['localhost', '127.0.0.1'].includes(window.location.hostname)
? 'http://localhost:8787'
: '');
/** Token + LiveKit URL minted by the backend. */
export async function fetchPodToken(