feat: Mongo-backed Pods with full CRUD + member management
Pods are persisted in MongoDB and CRUD'd via /api/pods (list/create/get/ update/delete + add/remove member). Shared Pod reshaped to a persisted entity (members: string[], description, timestamps) + PodInput. Backend seeds Demo/ Frontend/Backend Pods on first run (Squad -> Pod rename). Frontend replaces hardcoded teams with live data: PodCard (member chips add/remove, inline edit, delete, join) + CreatePodForm; App fetches+mutates via API. Removes teams.ts/ TeamCard; gitignores test artifacts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -39,3 +39,4 @@ coverage/
|
|||||||
# Ramis
|
# Ramis
|
||||||
.remember/
|
.remember/
|
||||||
.claude/
|
.claude/
|
||||||
|
.playwright-mcp/
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { MongoClient, type Db, type Collection } from 'mongodb';
|
import { MongoClient, type Db, type Collection } from 'mongodb';
|
||||||
import type { EngineerContext, Collision, Intervention, InterventionOutcome } from '@podman/shared';
|
import type {
|
||||||
|
EngineerContext,
|
||||||
|
Collision,
|
||||||
|
Intervention,
|
||||||
|
InterventionOutcome,
|
||||||
|
Pod,
|
||||||
|
} from '@podman/shared';
|
||||||
import { env } from '../env.js';
|
import { env } from '../env.js';
|
||||||
|
|
||||||
let clientPromise: Promise<MongoClient> | null = null;
|
let clientPromise: Promise<MongoClient> | null = null;
|
||||||
@@ -19,6 +25,7 @@ export async function getDb(): Promise<Db> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PodCollections {
|
export interface PodCollections {
|
||||||
|
pods: Collection<Pod>;
|
||||||
observations: Collection<EngineerContext>;
|
observations: Collection<EngineerContext>;
|
||||||
collisions: Collection<Collision>;
|
collisions: Collection<Collision>;
|
||||||
interventions: Collection<Intervention>;
|
interventions: Collection<Intervention>;
|
||||||
@@ -28,6 +35,7 @@ export interface PodCollections {
|
|||||||
export async function collections(): Promise<PodCollections> {
|
export async function collections(): Promise<PodCollections> {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
return {
|
return {
|
||||||
|
pods: db.collection<Pod>('pods'),
|
||||||
observations: db.collection<EngineerContext>('observations'),
|
observations: db.collection<EngineerContext>('observations'),
|
||||||
collisions: db.collection<Collision>('collisions'),
|
collisions: db.collection<Collision>('collisions'),
|
||||||
interventions: db.collection<Intervention>('interventions'),
|
interventions: db.collection<Intervention>('interventions'),
|
||||||
@@ -41,6 +49,7 @@ export async function initMemory(): Promise<void> {
|
|||||||
await db.command({ ping: 1 });
|
await db.command({ ping: 1 });
|
||||||
const c = await collections();
|
const c = await collections();
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
c.pods.createIndex({ id: 1 }, { unique: true }),
|
||||||
c.observations.createIndex({ podId: 1, observedAt: -1 }),
|
c.observations.createIndex({ podId: 1, observedAt: -1 }),
|
||||||
c.observations.createIndex({ engineerId: 1 }),
|
c.observations.createIndex({ engineerId: 1 }),
|
||||||
c.collisions.createIndex({ podId: 1, detectedAt: -1 }),
|
c.collisions.createIndex({ podId: 1, detectedAt: -1 }),
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import type { Pod, PodInput } from '@podman/shared';
|
||||||
|
import { collections } from '../memory/db.js';
|
||||||
|
|
||||||
|
const NO_ID = { projection: { _id: 0 } } as const;
|
||||||
|
|
||||||
|
function slugify(name: string): string {
|
||||||
|
return name
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Trim, drop empties, de-dupe case-insensitively while preserving order. */
|
||||||
|
function cleanMembers(members: unknown): string[] {
|
||||||
|
if (!Array.isArray(members)) return [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const out: string[] = [];
|
||||||
|
for (const raw of members) {
|
||||||
|
const name = String(raw).trim();
|
||||||
|
const key = name.toLowerCase();
|
||||||
|
if (name && !seen.has(key)) {
|
||||||
|
seen.add(key);
|
||||||
|
out.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function now(): string {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listPods(): Promise<Pod[]> {
|
||||||
|
const c = await collections();
|
||||||
|
return c.pods.find({}, NO_ID).sort({ createdAt: 1 }).toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPod(id: string): Promise<Pod | null> {
|
||||||
|
const c = await collections();
|
||||||
|
return c.pods.findOne({ id }, NO_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uniqueSlug(base: string): Promise<string> {
|
||||||
|
const c = await collections();
|
||||||
|
const root = base || 'pod';
|
||||||
|
let slug = root;
|
||||||
|
let n = 2;
|
||||||
|
while (await c.pods.findOne({ id: slug })) slug = `${root}-${n++}`;
|
||||||
|
return slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPod(input: PodInput): Promise<Pod> {
|
||||||
|
const name = (input.name ?? '').trim();
|
||||||
|
if (!name) throw new Error('name is required');
|
||||||
|
const c = await collections();
|
||||||
|
const ts = now();
|
||||||
|
const pod: Pod = {
|
||||||
|
id: await uniqueSlug(slugify(name)),
|
||||||
|
name,
|
||||||
|
repo: (input.repo ?? '').trim(),
|
||||||
|
description: input.description?.trim() || undefined,
|
||||||
|
members: cleanMembers(input.members),
|
||||||
|
createdAt: ts,
|
||||||
|
updatedAt: ts,
|
||||||
|
};
|
||||||
|
await c.pods.insertOne({ ...pod });
|
||||||
|
return pod;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updatePod(id: string, patch: PodInput): Promise<Pod | null> {
|
||||||
|
const set: Partial<Pod> = { updatedAt: now() };
|
||||||
|
if (patch.name !== undefined) {
|
||||||
|
const name = patch.name.trim();
|
||||||
|
if (!name) throw new Error('name cannot be empty');
|
||||||
|
set.name = name;
|
||||||
|
}
|
||||||
|
if (patch.repo !== undefined) set.repo = patch.repo.trim();
|
||||||
|
if (patch.description !== undefined) set.description = patch.description.trim() || undefined;
|
||||||
|
if (patch.members !== undefined) set.members = cleanMembers(patch.members);
|
||||||
|
const c = await collections();
|
||||||
|
const updated = await c.pods.findOneAndUpdate(
|
||||||
|
{ id },
|
||||||
|
{ $set: set },
|
||||||
|
{ returnDocument: 'after', projection: { _id: 0 } },
|
||||||
|
);
|
||||||
|
return updated ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePod(id: string): Promise<boolean> {
|
||||||
|
const c = await collections();
|
||||||
|
const r = await c.pods.deleteOne({ id });
|
||||||
|
return r.deletedCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setMembers(id: string, members: string[]): Promise<Pod | null> {
|
||||||
|
const c = await collections();
|
||||||
|
const updated = await c.pods.findOneAndUpdate(
|
||||||
|
{ id },
|
||||||
|
{ $set: { members, updatedAt: now() } },
|
||||||
|
{ returnDocument: 'after', projection: { _id: 0 } },
|
||||||
|
);
|
||||||
|
return updated ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addMember(id: string, rawName: string): Promise<Pod | null> {
|
||||||
|
const name = rawName.trim();
|
||||||
|
if (!name) throw new Error('member name is required');
|
||||||
|
const pod = await getPod(id);
|
||||||
|
if (!pod) return null;
|
||||||
|
if (pod.members.some((m) => m.toLowerCase() === name.toLowerCase())) return pod;
|
||||||
|
return setMembers(id, [...pod.members, name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeMember(id: string, rawName: string): Promise<Pod | null> {
|
||||||
|
const name = rawName.trim().toLowerCase();
|
||||||
|
const pod = await getPod(id);
|
||||||
|
if (!pod) return null;
|
||||||
|
return setMembers(
|
||||||
|
id,
|
||||||
|
pod.members.filter((m) => m.toLowerCase() !== name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Insert the default pods once, if the collection is empty. */
|
||||||
|
export async function seedDefaultPods(): Promise<void> {
|
||||||
|
const c = await collections();
|
||||||
|
if ((await c.pods.estimatedDocumentCount()) > 0) return;
|
||||||
|
const ts = now();
|
||||||
|
const defaults: Pod[] = [
|
||||||
|
{
|
||||||
|
id: 'demo-pod',
|
||||||
|
name: 'Demo Pod',
|
||||||
|
repo: 'karti-ai/podman',
|
||||||
|
description: 'The full crew — used for the live demo.',
|
||||||
|
members: ['Karti', 'Yahya', 'Ramis', 'Zander', 'Shakthi'],
|
||||||
|
createdAt: ts,
|
||||||
|
updatedAt: ts,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'frontend-pod',
|
||||||
|
name: 'Frontend Pod',
|
||||||
|
repo: 'karti-ai/podman',
|
||||||
|
description: 'PWA, capture, and PodMan card UI.',
|
||||||
|
members: ['Karti', 'Zander'],
|
||||||
|
createdAt: ts,
|
||||||
|
updatedAt: ts,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'backend-pod',
|
||||||
|
name: 'Backend Pod',
|
||||||
|
repo: 'karti-ai/podman',
|
||||||
|
description: 'LiveKit agent, vision, collision detector.',
|
||||||
|
members: ['Yahya', 'Ramis'],
|
||||||
|
createdAt: ts,
|
||||||
|
updatedAt: ts,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
await c.pods.insertMany(defaults.map((p) => ({ ...p })));
|
||||||
|
console.log('[pods] seeded default pods');
|
||||||
|
}
|
||||||
+68
-1
@@ -7,6 +7,16 @@ import { env } from './env.js';
|
|||||||
import { createSyncPr } from './github/client.js';
|
import { createSyncPr } from './github/client.js';
|
||||||
import { recordOutcome, memoryStats } from './memory/store.js';
|
import { recordOutcome, memoryStats } from './memory/store.js';
|
||||||
import { initMemory } from './memory/db.js';
|
import { initMemory } from './memory/db.js';
|
||||||
|
import {
|
||||||
|
listPods,
|
||||||
|
getPod,
|
||||||
|
createPod,
|
||||||
|
updatePod,
|
||||||
|
deletePod,
|
||||||
|
addMember,
|
||||||
|
removeMember,
|
||||||
|
seedDefaultPods,
|
||||||
|
} from './pods/store.js';
|
||||||
import type { InterventionOutcome } from '@podman/shared';
|
import type { InterventionOutcome } from '@podman/shared';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -54,6 +64,61 @@ app.get('/api/memory/stats', async (_req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Pods CRUD (Mongo-backed) ---
|
||||||
|
app.get('/api/pods', async (_req, res) => {
|
||||||
|
try {
|
||||||
|
res.json(await listPods());
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).json({ error: (e as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/pods', async (req, res) => {
|
||||||
|
try {
|
||||||
|
res.status(201).json(await createPod(req.body ?? {}));
|
||||||
|
} catch (e) {
|
||||||
|
res.status(400).json({ error: (e as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/pods/:id', async (req, res) => {
|
||||||
|
const pod = await getPod(req.params.id);
|
||||||
|
if (!pod) return res.status(404).json({ error: 'pod not found' });
|
||||||
|
res.json(pod);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.patch('/api/pods/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pod = await updatePod(req.params.id, req.body ?? {});
|
||||||
|
if (!pod) return res.status(404).json({ error: 'pod not found' });
|
||||||
|
res.json(pod);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(400).json({ error: (e as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/pods/:id', async (req, res) => {
|
||||||
|
const ok = await deletePod(req.params.id);
|
||||||
|
if (!ok) return res.status(404).json({ error: 'pod not found' });
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/pods/:id/members', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pod = await addMember(req.params.id, req.body?.name ?? '');
|
||||||
|
if (!pod) return res.status(404).json({ error: 'pod not found' });
|
||||||
|
res.json(pod);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(400).json({ error: (e as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/pods/:id/members/:name', async (req, res) => {
|
||||||
|
const pod = await removeMember(req.params.id, req.params.name);
|
||||||
|
if (!pod) return res.status(404).json({ error: 'pod not found' });
|
||||||
|
res.json(pod);
|
||||||
|
});
|
||||||
|
|
||||||
const http = createServer(app);
|
const http = createServer(app);
|
||||||
|
|
||||||
// ws relay: the agent pushes collision/intervention JSON here; PWAs subscribed by pod receive it.
|
// ws relay: the agent pushes collision/intervention JSON here; PWAs subscribed by pod receive it.
|
||||||
@@ -70,5 +135,7 @@ wss.on('connection', (ws) => {
|
|||||||
|
|
||||||
http.listen(env.PORT, '0.0.0.0', () => {
|
http.listen(env.PORT, '0.0.0.0', () => {
|
||||||
console.log(`[server] :${env.PORT}`);
|
console.log(`[server] :${env.PORT}`);
|
||||||
initMemory().catch((e) => console.warn(`[memory] init failed: ${(e as Error).message}`));
|
initMemory()
|
||||||
|
.then(() => seedDefaultPods())
|
||||||
|
.catch((e) => console.warn(`[memory] init failed: ${(e as Error).message}`));
|
||||||
});
|
});
|
||||||
|
|||||||
+109
-62
@@ -1,50 +1,97 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import type { Room } from 'livekit-client';
|
import type { Room } from 'livekit-client';
|
||||||
|
import type { Pod, PodInput } from '@podman/shared';
|
||||||
import { joinPod } from './lib/pod.js';
|
import { joinPod } from './lib/pod.js';
|
||||||
import { TEAMS, teamById } from './lib/teams.js';
|
import * as api from './lib/api.js';
|
||||||
import { TeamCard } from './components/TeamCard.js';
|
import { PodCard } from './components/PodCard.js';
|
||||||
|
import { CreatePodForm } from './components/CreatePodForm.js';
|
||||||
import { PodView } from './components/PodView.js';
|
import { PodView } from './components/PodView.js';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [teamId, setTeamId] = useState(TEAMS[0]!.id);
|
const [pods, setPods] = useState<Pod[]>([]);
|
||||||
const team = useMemo(() => teamById(teamId), [teamId]);
|
const [loading, setLoading] = useState(true);
|
||||||
const [member, setMember] = useState(team.members[0]!);
|
const [busy, setBusy] = useState(false);
|
||||||
const [room, setRoom] = useState<Room | null>(null);
|
|
||||||
const [joined, setJoined] = useState(false);
|
|
||||||
const [devMode, setDevMode] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [connecting, setConnecting] = useState(false);
|
|
||||||
|
|
||||||
function handleTeamChange(id: string) {
|
// join state
|
||||||
setTeamId(id);
|
const [joinedPod, setJoinedPod] = useState<Pod | null>(null);
|
||||||
const next = teamById(id);
|
const [member, setMember] = useState('');
|
||||||
if (!next.members.includes(member)) setMember(next.members[0]!);
|
const [devMode, setDevMode] = useState(false);
|
||||||
|
const [room, setRoom] = useState<Room | null>(null);
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
setPods(await api.listPods());
|
||||||
|
setError(null);
|
||||||
|
} catch (e) {
|
||||||
|
setError((e as Error).message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleJoin() {
|
useEffect(() => {
|
||||||
|
void refresh();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/** Run a mutation, reflect the result in local state, surface errors. */
|
||||||
|
async function mutate(fn: () => Promise<void>) {
|
||||||
|
setBusy(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setConnecting(true);
|
|
||||||
try {
|
try {
|
||||||
const identity = `${member}-${Math.random().toString(36).slice(2, 7)}`;
|
await fn();
|
||||||
const result = await joinPod(team.id, identity, member);
|
} catch (e) {
|
||||||
|
setError((e as Error).message);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsert = (p: Pod) => setPods((cur) => cur.map((x) => (x.id === p.id ? p : x)));
|
||||||
|
|
||||||
|
const handleCreate = (input: PodInput) =>
|
||||||
|
mutate(async () => {
|
||||||
|
const created = await api.createPod(input);
|
||||||
|
setPods((cur) => [...cur, created]);
|
||||||
|
});
|
||||||
|
const handleUpdate = (id: string, patch: PodInput) =>
|
||||||
|
mutate(async () => upsert(await api.updatePod(id, patch)));
|
||||||
|
const handleDelete = (id: string) =>
|
||||||
|
mutate(async () => {
|
||||||
|
await api.deletePod(id);
|
||||||
|
setPods((cur) => cur.filter((x) => x.id !== id));
|
||||||
|
});
|
||||||
|
const handleAddMember = (id: string, name: string) =>
|
||||||
|
mutate(async () => upsert(await api.addMember(id, name)));
|
||||||
|
const handleRemoveMember = (id: string, name: string) =>
|
||||||
|
mutate(async () => upsert(await api.removeMember(id, name)));
|
||||||
|
|
||||||
|
async function handleJoin(pod: Pod, who: string) {
|
||||||
|
setBusy(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const identity = `${who}-${Math.random().toString(36).slice(2, 7)}`;
|
||||||
|
const result = await joinPod(pod.id, identity, who);
|
||||||
setRoom(result.room);
|
setRoom(result.room);
|
||||||
setDevMode(result.mode === 'dev');
|
setDevMode(result.mode === 'dev');
|
||||||
setJoined(true);
|
setMember(who);
|
||||||
} catch (err) {
|
setJoinedPod(pod);
|
||||||
setError((err as Error).message);
|
} catch (e) {
|
||||||
|
setError((e as Error).message);
|
||||||
} finally {
|
} finally {
|
||||||
setConnecting(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLeave() {
|
function handleLeave() {
|
||||||
room?.disconnect();
|
room?.disconnect();
|
||||||
setRoom(null);
|
setRoom(null);
|
||||||
setJoined(false);
|
setJoinedPod(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto min-h-screen w-full max-w-4xl px-4 py-8 sm:px-6">
|
<div className="mx-auto min-h-screen w-full max-w-5xl px-4 py-8 sm:px-6">
|
||||||
<header className="mb-8 flex items-center gap-3">
|
<header className="mb-8 flex items-center gap-3">
|
||||||
<span className="text-3xl">🛰️</span>
|
<span className="text-3xl">🛰️</span>
|
||||||
<div>
|
<div>
|
||||||
@@ -55,48 +102,48 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{joined ? (
|
{joinedPod ? (
|
||||||
<PodView team={team} me={member} devMode={devMode} onLeave={handleLeave} />
|
<PodView team={joinedPod} me={member} devMode={devMode} onLeave={handleLeave} />
|
||||||
) : (
|
) : (
|
||||||
<main className="flex flex-col gap-6">
|
<main className="flex flex-col gap-4">
|
||||||
<section>
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="mb-3 text-sm font-medium text-slate-400">Pick a team</h2>
|
<h2 className="text-sm font-medium text-slate-400">
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
Pods {loading ? '…' : `(${pods.length})`}
|
||||||
{TEAMS.map((t) => (
|
</h2>
|
||||||
<TeamCard
|
<button
|
||||||
key={t.id}
|
className="text-xs text-slate-400 hover:text-slate-200 disabled:opacity-50"
|
||||||
team={t}
|
onClick={() => void refresh()}
|
||||||
selected={t.id === teamId}
|
disabled={loading}
|
||||||
onSelect={handleTeamChange}
|
>
|
||||||
|
↻ Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p className="rounded-md border border-red-900/60 bg-red-950/40 px-3 py-2 text-sm text-red-400">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<p className="text-sm text-slate-500">Loading pods…</p>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
{pods.map((pod) => (
|
||||||
|
<PodCard
|
||||||
|
key={pod.id}
|
||||||
|
pod={pod}
|
||||||
|
busy={busy}
|
||||||
|
onJoin={handleJoin}
|
||||||
|
onAddMember={handleAddMember}
|
||||||
|
onRemoveMember={handleRemoveMember}
|
||||||
|
onUpdate={handleUpdate}
|
||||||
|
onDelete={handleDelete}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
<CreatePodForm busy={busy} onCreate={handleCreate} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
)}
|
||||||
|
|
||||||
<section className="flex flex-col gap-3 rounded-xl border border-slate-800 bg-slate-900/40 p-4 sm:flex-row sm:items-end">
|
|
||||||
<label className="flex flex-1 flex-col gap-1 text-sm text-slate-400">
|
|
||||||
Join “{team.name}” as
|
|
||||||
<select
|
|
||||||
className="rounded-md border border-slate-700 bg-slate-900 px-3 py-2 text-base text-slate-100"
|
|
||||||
value={member}
|
|
||||||
onChange={(e) => setMember(e.target.value)}
|
|
||||||
>
|
|
||||||
{team.members.map((m) => (
|
|
||||||
<option key={m} value={m}>
|
|
||||||
{m}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
className="rounded-md bg-emerald-600 px-5 py-2 font-medium text-white hover:bg-emerald-500 disabled:opacity-50"
|
|
||||||
onClick={handleJoin}
|
|
||||||
disabled={connecting}
|
|
||||||
>
|
|
||||||
{connecting ? 'Joining…' : 'Join pod'}
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
{error && <p className="text-sm text-red-400">{error}</p>}
|
|
||||||
</main>
|
</main>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import type { PodInput } from '@podman/shared';
|
||||||
|
|
||||||
|
export function CreatePodForm({
|
||||||
|
busy,
|
||||||
|
onCreate,
|
||||||
|
}: {
|
||||||
|
busy: boolean;
|
||||||
|
onCreate: (input: PodInput) => void;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [repo, setRepo] = useState('karti-ai/podman');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
const [firstMember, setFirstMember] = useState('');
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
if (!name.trim()) return;
|
||||||
|
onCreate({
|
||||||
|
name: name.trim(),
|
||||||
|
repo: repo.trim(),
|
||||||
|
description: description.trim(),
|
||||||
|
members: firstMember.trim() ? [firstMember.trim()] : [],
|
||||||
|
});
|
||||||
|
setName('');
|
||||||
|
setDescription('');
|
||||||
|
setFirstMember('');
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="flex min-h-[140px] items-center justify-center rounded-xl border border-dashed border-slate-700 text-slate-400 hover:border-emerald-600 hover:text-emerald-400"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
+ New Pod
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 rounded-xl border border-emerald-700/50 bg-slate-900/50 p-4">
|
||||||
|
<h3 className="font-semibold text-slate-100">New Pod</h3>
|
||||||
|
<input
|
||||||
|
className="rounded-md border border-slate-700 bg-slate-900 px-3 py-1.5 text-sm"
|
||||||
|
placeholder="Pod name (e.g. Mobile Pod)"
|
||||||
|
value={name}
|
||||||
|
autoFocus
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="rounded-md border border-slate-700 bg-slate-900 px-3 py-1.5 text-sm"
|
||||||
|
placeholder="owner/repo"
|
||||||
|
value={repo}
|
||||||
|
onChange={(e) => setRepo(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="rounded-md border border-slate-700 bg-slate-900 px-3 py-1.5 text-sm"
|
||||||
|
placeholder="Description (optional)"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="rounded-md border border-slate-700 bg-slate-900 px-3 py-1.5 text-sm"
|
||||||
|
placeholder="Your name (optional first member)"
|
||||||
|
value={firstMember}
|
||||||
|
onChange={(e) => setFirstMember(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
className="rounded-md bg-emerald-600 px-3 py-1 text-sm font-medium hover:bg-emerald-500 disabled:opacity-50"
|
||||||
|
onClick={submit}
|
||||||
|
disabled={busy || !name.trim()}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="rounded-md border border-slate-700 px-3 py-1 text-sm hover:bg-slate-800"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import type { Pod, PodInput } from '@podman/shared';
|
||||||
|
import { Avatar } from './Avatar.js';
|
||||||
|
|
||||||
|
export function PodCard({
|
||||||
|
pod,
|
||||||
|
busy,
|
||||||
|
onJoin,
|
||||||
|
onAddMember,
|
||||||
|
onRemoveMember,
|
||||||
|
onUpdate,
|
||||||
|
onDelete,
|
||||||
|
}: {
|
||||||
|
pod: Pod;
|
||||||
|
busy: boolean;
|
||||||
|
onJoin: (pod: Pod, member: string) => void;
|
||||||
|
onAddMember: (id: string, name: string) => void;
|
||||||
|
onRemoveMember: (id: string, name: string) => void;
|
||||||
|
onUpdate: (id: string, patch: PodInput) => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
}) {
|
||||||
|
const [newMember, setNewMember] = useState('');
|
||||||
|
const [joinAs, setJoinAs] = useState(pod.members[0] ?? '');
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
const [draft, setDraft] = useState<PodInput>({
|
||||||
|
name: pod.name,
|
||||||
|
repo: pod.repo,
|
||||||
|
description: pod.description ?? '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// keep joinAs valid as members change
|
||||||
|
const joinMember = pod.members.includes(joinAs) ? joinAs : (pod.members[0] ?? '');
|
||||||
|
|
||||||
|
function add() {
|
||||||
|
const name = newMember.trim();
|
||||||
|
if (!name) return;
|
||||||
|
onAddMember(pod.id, name);
|
||||||
|
setNewMember('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveEdit() {
|
||||||
|
onUpdate(pod.id, {
|
||||||
|
name: draft.name?.trim(),
|
||||||
|
repo: draft.repo?.trim(),
|
||||||
|
description: draft.description?.trim(),
|
||||||
|
});
|
||||||
|
setEditing(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3 rounded-xl border border-slate-700 bg-slate-900/50 p-4">
|
||||||
|
{editing ? (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<input
|
||||||
|
className="rounded-md border border-slate-700 bg-slate-900 px-3 py-1.5 text-sm"
|
||||||
|
value={draft.name ?? ''}
|
||||||
|
placeholder="Pod name"
|
||||||
|
onChange={(e) => setDraft({ ...draft, name: e.target.value })}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="rounded-md border border-slate-700 bg-slate-900 px-3 py-1.5 text-sm"
|
||||||
|
value={draft.repo ?? ''}
|
||||||
|
placeholder="owner/repo"
|
||||||
|
onChange={(e) => setDraft({ ...draft, repo: e.target.value })}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="rounded-md border border-slate-700 bg-slate-900 px-3 py-1.5 text-sm"
|
||||||
|
value={draft.description ?? ''}
|
||||||
|
placeholder="Description"
|
||||||
|
onChange={(e) => setDraft({ ...draft, description: e.target.value })}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
className="rounded-md bg-emerald-600 px-3 py-1 text-sm font-medium hover:bg-emerald-500 disabled:opacity-50"
|
||||||
|
onClick={saveEdit}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="rounded-md border border-slate-700 px-3 py-1 text-sm hover:bg-slate-800"
|
||||||
|
onClick={() => setEditing(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h3 className="font-semibold text-slate-100">{pod.name}</h3>
|
||||||
|
<p className="text-xs text-slate-500">{pod.repo || 'no repo set'}</p>
|
||||||
|
{pod.description && <p className="mt-1 text-sm text-slate-300">{pod.description}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="flex shrink-0 gap-1">
|
||||||
|
<button
|
||||||
|
title="Edit pod"
|
||||||
|
className="rounded-md border border-slate-700 px-2 py-1 text-xs text-slate-300 hover:bg-slate-800"
|
||||||
|
onClick={() => {
|
||||||
|
setDraft({ name: pod.name, repo: pod.repo, description: pod.description ?? '' });
|
||||||
|
setEditing(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
title="Delete pod"
|
||||||
|
className="rounded-md border border-red-900/60 px-2 py-1 text-xs text-red-400 hover:bg-red-950/40 disabled:opacity-50"
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm(`Delete pod “${pod.name}”?`)) onDelete(pod.id);
|
||||||
|
}}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Members */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{pod.members.length === 0 && (
|
||||||
|
<span className="text-xs text-slate-600">No members yet.</span>
|
||||||
|
)}
|
||||||
|
{pod.members.map((m) => (
|
||||||
|
<span
|
||||||
|
key={m}
|
||||||
|
className="flex items-center gap-1.5 rounded-full bg-slate-800 py-0.5 pl-0.5 pr-2 text-sm text-slate-200"
|
||||||
|
>
|
||||||
|
<Avatar name={m} size={22} />
|
||||||
|
{m}
|
||||||
|
<button
|
||||||
|
title={`Remove ${m}`}
|
||||||
|
className="ml-0.5 text-slate-500 hover:text-red-400"
|
||||||
|
onClick={() => onRemoveMember(pod.id, m)}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
className="flex-1 rounded-md border border-slate-700 bg-slate-900 px-3 py-1.5 text-sm"
|
||||||
|
placeholder="Add your name…"
|
||||||
|
value={newMember}
|
||||||
|
onChange={(e) => setNewMember(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && add()}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="rounded-md border border-slate-600 px-3 py-1.5 text-sm hover:bg-slate-800 disabled:opacity-50"
|
||||||
|
onClick={add}
|
||||||
|
disabled={busy || !newMember.trim()}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Join */}
|
||||||
|
<div className="flex gap-2 border-t border-slate-800 pt-3">
|
||||||
|
<select
|
||||||
|
className="flex-1 rounded-md border border-slate-700 bg-slate-900 px-3 py-1.5 text-sm text-slate-100 disabled:opacity-50"
|
||||||
|
value={joinMember}
|
||||||
|
onChange={(e) => setJoinAs(e.target.value)}
|
||||||
|
disabled={pod.members.length === 0}
|
||||||
|
>
|
||||||
|
{pod.members.length === 0 ? (
|
||||||
|
<option>add a member first</option>
|
||||||
|
) : (
|
||||||
|
pod.members.map((m) => (
|
||||||
|
<option key={m} value={m}>
|
||||||
|
{m}
|
||||||
|
</option>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
className="rounded-md bg-emerald-600 px-4 py-1.5 text-sm font-medium hover:bg-emerald-500 disabled:opacity-50"
|
||||||
|
onClick={() => onJoin(pod, joinMember)}
|
||||||
|
disabled={busy || pod.members.length === 0}
|
||||||
|
>
|
||||||
|
Join
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { DemoTeam } from '../lib/teams.js';
|
import type { Pod } from '@podman/shared';
|
||||||
import { Avatar } from './Avatar.js';
|
import { Avatar } from './Avatar.js';
|
||||||
|
|
||||||
export function PodView({
|
export function PodView({
|
||||||
@@ -7,7 +7,7 @@ export function PodView({
|
|||||||
devMode,
|
devMode,
|
||||||
onLeave,
|
onLeave,
|
||||||
}: {
|
}: {
|
||||||
team: DemoTeam;
|
team: Pod;
|
||||||
me: string;
|
me: string;
|
||||||
devMode: boolean;
|
devMode: boolean;
|
||||||
onLeave: () => void;
|
onLeave: () => void;
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import type { DemoTeam } from '../lib/teams.js';
|
|
||||||
import { Avatar } from './Avatar.js';
|
|
||||||
|
|
||||||
export function TeamCard({
|
|
||||||
team,
|
|
||||||
selected,
|
|
||||||
onSelect,
|
|
||||||
}: {
|
|
||||||
team: DemoTeam;
|
|
||||||
selected: boolean;
|
|
||||||
onSelect: (id: string) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={() => onSelect(team.id)}
|
|
||||||
className={`flex w-full flex-col gap-3 rounded-xl border p-4 text-left transition ${
|
|
||||||
selected
|
|
||||||
? 'border-emerald-500 bg-emerald-950/30 ring-1 ring-emerald-500/40'
|
|
||||||
: 'border-slate-700 bg-slate-900/50 hover:border-slate-500 hover:bg-slate-900'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h3 className="font-semibold text-slate-100">{team.name}</h3>
|
|
||||||
<span className="rounded-full bg-slate-800 px-2 py-0.5 text-xs text-slate-400">
|
|
||||||
{team.members.length} members
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-slate-500">{team.repo}</p>
|
|
||||||
<p className="text-sm text-slate-300">{team.description}</p>
|
|
||||||
<div className="flex -space-x-2 pt-1">
|
|
||||||
{team.members.map((m) => (
|
|
||||||
<Avatar key={m} name={m} size={28} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+62
-3
@@ -1,7 +1,15 @@
|
|||||||
import type { InterventionOutcome } from '@podman/shared';
|
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 || 'http://localhost:8787';
|
||||||
|
|
||||||
|
async function json<T>(res: Response): Promise<T> {
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
||||||
|
throw new Error(body.error || `request failed: ${res.status}`);
|
||||||
|
}
|
||||||
|
return res.json() as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
/** Mint a LiveKit token from the backend. */
|
/** Mint a LiveKit token from the backend. */
|
||||||
export async function fetchToken(params: {
|
export async function fetchToken(params: {
|
||||||
room: string;
|
room: string;
|
||||||
@@ -14,8 +22,7 @@ export async function fetchToken(params: {
|
|||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/json' },
|
||||||
body: JSON.stringify(params),
|
body: JSON.stringify(params),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`token request failed: ${res.status}`);
|
return json(res);
|
||||||
return res.json() as Promise<{ token: string; url: string }>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Record an intervention outcome for the policy learning loop. */
|
/** Record an intervention outcome for the policy learning loop. */
|
||||||
@@ -27,3 +34,55 @@ export async function postOutcome(outcome: InterventionOutcome): Promise<void> {
|
|||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`outcome post failed: ${res.status}`);
|
if (!res.ok) throw new Error(`outcome post failed: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Pods CRUD ---
|
||||||
|
|
||||||
|
export async function listPods(): Promise<Pod[]> {
|
||||||
|
return json(await fetch(`${BACKEND_URL}/api/pods`));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPod(input: PodInput): Promise<Pod> {
|
||||||
|
return json(
|
||||||
|
await fetch(`${BACKEND_URL}/api/pods`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updatePod(id: string, patch: PodInput): Promise<Pod> {
|
||||||
|
return json(
|
||||||
|
await fetch(`${BACKEND_URL}/api/pods/${encodeURIComponent(id)}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify(patch),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePod(id: string): Promise<void> {
|
||||||
|
const res = await fetch(`${BACKEND_URL}/api/pods/${encodeURIComponent(id)}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`delete pod failed: ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addMember(id: string, name: string): Promise<Pod> {
|
||||||
|
return json(
|
||||||
|
await fetch(`${BACKEND_URL}/api/pods/${encodeURIComponent(id)}/members`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeMember(id: string, name: string): Promise<Pod> {
|
||||||
|
return json(
|
||||||
|
await fetch(
|
||||||
|
`${BACKEND_URL}/api/pods/${encodeURIComponent(id)}/members/${encodeURIComponent(name)}`,
|
||||||
|
{ method: 'DELETE' },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
/**
|
|
||||||
* Hardcoded teams/engineers for R&D so the join screen is select-only (no typing)
|
|
||||||
* while we develop. Swap for real data (backend/Atlas) once pods are persisted.
|
|
||||||
*/
|
|
||||||
export interface DemoTeam {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
repo: string;
|
|
||||||
description: string;
|
|
||||||
members: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TEAMS: DemoTeam[] = [
|
|
||||||
{
|
|
||||||
id: 'demo-pod',
|
|
||||||
name: 'Demo Pod',
|
|
||||||
repo: 'karti-ai/podman',
|
|
||||||
description: 'The full crew — used for the live demo.',
|
|
||||||
members: ['Karti', 'Yahya', 'Ramis', 'Shakthi'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'frontend-squad',
|
|
||||||
name: 'Frontend Squad',
|
|
||||||
repo: 'karti-ai/podman',
|
|
||||||
description: 'PWA, capture, and PodMan card UI.',
|
|
||||||
members: ['Karti', 'Ramis'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'backend-squad',
|
|
||||||
name: 'Backend Squad',
|
|
||||||
repo: 'karti-ai/podman',
|
|
||||||
description: 'LiveKit agent, vision, collision detector.',
|
|
||||||
members: ['Yahya', 'Ramis'],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function teamById(id: string): DemoTeam {
|
|
||||||
return TEAMS.find((t) => t.id === id) ?? TEAMS[0]!;
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
export type { Pod, Engineer } from './pod.js';
|
export type { Pod, PodInput, Engineer } from './pod.js';
|
||||||
export type { EngineerContext } from './engineer.js';
|
export type { EngineerContext } from './engineer.js';
|
||||||
export type { Collision, CollisionSeverity, GithubStateSnapshot } from './collision.js';
|
export type { Collision, CollisionSeverity, GithubStateSnapshot } from './collision.js';
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
+17
-2
@@ -1,11 +1,26 @@
|
|||||||
/** A pod is one room of engineers working together (one LiveKit room per pod). */
|
/**
|
||||||
|
* A pod is one room of engineers working together (one LiveKit room per pod).
|
||||||
|
* Persisted in MongoDB; CRUD'd via /api/pods.
|
||||||
|
*/
|
||||||
export interface Pod {
|
export interface Pod {
|
||||||
|
/** URL-safe slug, e.g. "frontend-pod". Stable id used everywhere. */
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
/** GitHub repo the pod is working in, as "owner/name". */
|
/** GitHub repo the pod is working in, as "owner/name". */
|
||||||
repo: string;
|
repo: string;
|
||||||
members: Engineer[];
|
description?: string;
|
||||||
|
/** Engineer display names PodMan uses when it speaks. */
|
||||||
|
members: string[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fields accepted when creating or updating a pod. */
|
||||||
|
export interface PodInput {
|
||||||
|
name?: string;
|
||||||
|
repo?: string;
|
||||||
|
description?: string;
|
||||||
|
members?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Engineer {
|
export interface Engineer {
|
||||||
|
|||||||
Reference in New Issue
Block a user