feat: one-click join + live pod presence

Join UX: click a member's name to join directly, or type a name and "Join"
(adds to roster + joins) — removes the redundant dropdown step. "Add" still
adds to the roster without joining.

Presence: new GET /api/presence (LiveKit RoomServiceClient) reports who's
connected per pod. The pod list polls it and shows a "N in room" badge plus a
green dot on members currently connected, so people can see active pods and jump in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kartikeya
2026-06-27 16:46:47 -07:00
parent 8271188cf1
commit 0f3b55a4f9
5 changed files with 146 additions and 49 deletions
+39
View File
@@ -0,0 +1,39 @@
import { RoomServiceClient } from 'livekit-server-sdk';
import { env } from '../env.js';
function isConfigured(): boolean {
return !!env.LIVEKIT_URL && !env.LIVEKIT_URL.includes('REPLACE_ME');
}
let client: RoomServiceClient | null = null;
function svc(): RoomServiceClient {
if (!client) {
const httpUrl = env.LIVEKIT_URL.replace(/^wss:/, 'https:').replace(/^ws:/, 'http:');
client = new RoomServiceClient(httpUrl, env.LIVEKIT_API_KEY, env.LIVEKIT_API_SECRET);
}
return client;
}
/**
* Who is currently connected in each pod's LiveKit room, as display names,
* keyed by pod id (= room name). Empty rooms are omitted. Returns {} if
* LiveKit isn't configured or the API call fails (presence is best-effort).
*/
export async function getPresence(): Promise<Record<string, string[]>> {
if (!isConfigured()) return {};
const out: Record<string, string[]> = {};
const rooms = await svc().listRooms();
await Promise.all(
rooms
.filter((r) => r.numParticipants > 0)
.map(async (r) => {
try {
const ps = await svc().listParticipants(r.name);
out[r.name] = ps.map((p) => p.name || p.identity);
} catch {
out[r.name] = [];
}
}),
);
return out;
}
+10
View File
@@ -17,6 +17,7 @@ import {
removeMember,
seedDefaultPods,
} from './pods/store.js';
import { getPresence } from './livekit/rooms.js';
import type { InterventionOutcome } from '@podman/shared';
const app = express();
@@ -72,6 +73,15 @@ app.get('/api/memory/stats', async (_req, res) => {
}
});
// Live presence: who is currently connected in each pod's LiveKit room.
app.get('/api/presence', async (_req, res) => {
try {
res.json(await getPresence());
} catch (e) {
res.status(500).json({ error: (e as Error).message });
}
});
// --- Pods CRUD (Mongo-backed) ---
app.get('/api/pods', async (_req, res) => {
try {