diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bc6813b..c19441e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,12 +9,15 @@ import { PodView } from './components/PodView.js'; const SESSION_KEY = 'podman.session'; +const fmt = new Intl.NumberFormat('en', { notation: 'compact' }); + export default function App() { const [pods, setPods] = useState([]); const [loading, setLoading] = useState(true); const [pending, setPending] = useState>(new Set()); const [error, setError] = useState(null); const [presence, setPresence] = useState>({}); + const [memory, setMemory] = useState(null); // join state — keep the id and derive the pod so it never goes stale const [joinedPodId, setJoinedPodId] = useState(null); @@ -28,7 +31,14 @@ export default function App() { async function refresh() { setLoading(true); try { - setPods(await api.listPods()); + const [nextPods, nextPresence, nextMemory] = await Promise.all([ + api.listPods(), + api.getPresence().catch(() => presence), + api.getMemoryStats().catch(() => memory), + ]); + setPods(nextPods); + setPresence(nextPresence); + setMemory(nextMemory); setError(null); } catch (e) { setError((e as Error).message); @@ -69,8 +79,14 @@ export default function App() { let alive = true; const tick = async () => { try { - const p = await api.getPresence(); - if (alive) setPresence(p); + const [p, m] = await Promise.all([ + api.getPresence(), + api.getMemoryStats().catch(() => memory), + ]); + if (alive) { + setPresence(p); + setMemory(m); + } } catch { /* presence is best-effort */ } @@ -181,72 +197,229 @@ export default function App() { } const showReconnecting = restoring || (joinedPodId !== null && joinedPod === null); + const liveNames = Array.from(new Set(Object.values(presence).flat())); + const liveTotal = liveNames.length; + const totalMembers = pods.reduce((sum, p) => sum + p.members.length, 0); + const activeRooms = Object.values(presence).filter((names) => names.length > 0).length; + const podManOnline = liveNames.some((name) => name.toLowerCase() === 'podman'); + const latestActivity = memory + ? memory.observations + memory.collisions + memory.interventions + memory.outcomes + : 0; return ( -
-
- 🛰️ -
-

PodMan

-

- The teammate that sees what git can't — collisions caught before you push. -

-
-
- - {joinedPod ? ( - - ) : showReconnecting ? ( -
-

Reconnecting to your pod…

- -
- ) : ( -
-
-

- Pods {loading ? '…' : `(${pods.length})`} -

- +
+
+
+
+
+
+ PM +
+
+

Live team coordination

+

PodMan

+
+
+

+ Active work, team memory, and coordination signals in one quiet workspace. +

- {error && ( -

- {error} -

- )} +
+ + + + +
+
- {loading ? ( -

Loading pods…

- ) : ( -
- {pods.map((pod) => ( - +
+ {joinedPod ? ( + + ) : showReconnecting ? ( +
+

Reconnecting to your pod...

+ +
+ ) : ( +
+
+
+

Team workspaces

+

+ Pods, presence, and intervention state. +

+
+ +
+ + {error && ( +

+ {error} +

+ )} + + {loading ? ( +
+ + +
+ ) : ( +
+ {pods.map((pod) => ( + + ))} + +
+ )} +
+ )} +
+ +
- )} + + 0 ? 'learning' : 'ready'} + /> + +
+ + +
+

Intervention model

+
+ + + +
+
+ +
+

Live roster

+
+ {liveNames.length ? ( + liveNames.map((name) => ( + + {name} + + )) + ) : ( + No live participants yet. + )} +
+
+ + + + + ); +} + +function Metric({ label, value, tone }: { label: string; value: string; tone: string }) { + const toneClass = + tone === 'green' + ? 'text-emerald-700 bg-emerald-50 border-emerald-200' + : tone === 'blue' + ? 'text-blue-700 bg-blue-50 border-blue-200' + : tone === 'amber' + ? 'text-amber-700 bg-amber-50 border-amber-200' + : 'text-slate-700 bg-white border-slate-200'; + return ( +
+

{label}

+

{value}

+
+ ); +} + +function SignalRow({ label, state }: { label: string; state: string }) { + return ( +
+ {label} + {state} +
+ ); +} + +function TimelineItem({ title, text }: { title: string; text: string }) { + return ( +
+

{title}

+

{text}

+
+ ); +} + +function SkeletonCard() { + return ( +
+
+
+
); } diff --git a/frontend/src/components/CreatePodForm.tsx b/frontend/src/components/CreatePodForm.tsx index 8ef45d8..828a227 100644 --- a/frontend/src/components/CreatePodForm.tsx +++ b/frontend/src/components/CreatePodForm.tsx @@ -36,52 +36,61 @@ export function CreatePodForm({ if (!open) { return ( ); } return ( -
-

New Pod

+
+
+

New pod

+

Name, repository, and first member.

+
setName(e.target.value)} /> setRepo(e.target.value)} /> setDescription(e.target.value)} /> setFirstMember(e.target.value)} /> -
+
) : ( -
+
-
-

{pod.name}

+
+

{pod.name}

{presence.length > 0 && ( - - - {presence.length} in room + + + {presence.length} live )}
-

{pod.repo || 'no repo set'}

- {pod.description &&

{pod.description}

} +

{pod.repo || 'no repo set'}

+ {pod.description && ( +

+ {pod.description} +

+ )}
)} - {/* Members — click your name to join */} -
-

Click your name to join:

-
+
+ + + +
+ +
+
+

Roster

+

Members

+
+
{pod.members.length === 0 && ( No members yet — add your name below. )} {pod.members.map((m) => (
- {/* Add your name (and join) */} -
+
setNewMember(e.target.value)} onKeyDown={(e) => { @@ -167,7 +187,7 @@ export function PodCard({ }} />
); } + +function Stat({ label, value }: { label: string; value: number | string }) { + return ( +
+

{label}

+

{value}

+
+ ); +} diff --git a/frontend/src/components/PodView.tsx b/frontend/src/components/PodView.tsx index 191f8f9..6b17861 100644 --- a/frontend/src/components/PodView.tsx +++ b/frontend/src/components/PodView.tsx @@ -150,112 +150,200 @@ export function PodView({ } const liveCount = participants.length; + const podmanPresent = participants.some((p) => p.name.toLowerCase() === 'podman'); return (
-
+
-

{team.name}

-

{team.repo}

+
+

{team.name}

+ + {room ? 'live room' : 'local'} + +
+

{team.repo}

+

+ Presence, media controls, and intervention state for this pod. +

{devMode && ( -

- DEV MODE — LiveKit not configured, so this is a local-only mock (no real room). +

+ Dev mode: LiveKit is not configured, so this is a local-only mock.

)} - {/* Connectivity test controls */} -
- - - - {playingBeat - ? '🔊 broadcasting beat to the pod' - : 'press “Play beat” — everyone should hear it'} - +
+
+

Broadcast controls

+

Audio, screen, and room signal.

+
+
+ + +
- {note &&

{note}

} + {note && ( +

+ {note} +

+ )} -
- {/* Live participants */} -
-

In the room now ({liveCount})

+
+ + + + +
+ +
+
+
+
+

Room presence

+

Live participants and speaking state.

+
+ + {liveCount} connected + +
{liveCount === 0 ? ( -

Connecting…

+

Connecting...

) : ( -
+
{participants.map((p) => (
- +
-

{p.name}

-

+

{p.name}

+

{p.isLocal ? 'you' : 'connected'} - {p.speaking ? ' · 🔊' : ''} + {p.speaking ? ' - speaking' : ''}

))}
)} -

- Pod roster: {team.members.join(', ') || '—'} +

+ Pod roster: {team.members.join(', ') || '-'}

- {/* PodMan panel */} -