import { useMemo } from 'react' import type { OrgInfoPayload } from '../types/visual' import type { CommsStatePayload } from '../lib/wsClient' import { getRuntimeOrgView } from '../lib/runtimeOrg' function asRecord(value: unknown): Record { return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {} } function asRecordList(value: unknown): Record[] { return Array.isArray(value) ? value.filter((item) => item && typeof item === 'object' && !Array.isArray(item)) as Record[] : [] } function summarizeText(value: unknown, fallback = 'Pending') { const text = String(value ?? '').trim() return text || fallback } function humanizeId(value: unknown, fallback = 'Team') { const text = String(value ?? '').trim() if (!text) return fallback return text.replace(/[_-]+/g, ' ').replace(/\b\w/g, char => char.toUpperCase()) } function statusToken(value: unknown) { return summarizeText(value, 'idle').toLowerCase().replace(/[\s_]+/g, '-') } interface TeamCardInfo { key: string label: string managerLabel: string status: string seatCount: number pendingApprovals: number latestAlert?: string seats: Array<{ key: string roleLabel: string seatLabel: string status: string }> } interface ProjectCockpitProps { orgInfoData?: OrgInfoPayload | null recoveryStatus?: Record | null commsState?: CommsStatePayload | null onStopRun?: () => void embedded?: boolean } export function ProjectCockpit({ orgInfoData, recoveryStatus, commsState, onStopRun, embedded = false, }: ProjectCockpitProps) { const runtimeView = useMemo(() => getRuntimeOrgView(orgInfoData ?? null), [orgInfoData]) const projectRun = runtimeView.projectRun const seatDigests = runtimeView.seatDigests const pendingDecisionCount = seatDigests.reduce((total, digest) => { const managerDigest = asRecord(digest.manager_digest) return total + asRecordList(managerDigest.pending_decisions).length }, 0) const actionableCount = seatDigests.reduce((count, digest) => ( count + asRecordList(asRecord(digest.manager_digest).actionable_chat).length ), 0) const protocolCount = seatDigests.reduce((count, digest) => ( count + asRecordList(asRecord(digest.manager_digest).protocol_backlog).length ), 0) const notificationCount = seatDigests.reduce((count, digest) => ( count + asRecordList(asRecord(digest.manager_digest).notification_backlog).length ), 0) const unreadCount = actionableCount + protocolCount + notificationCount const interrupted = Array.isArray(recoveryStatus?.interrupted) ? recoveryStatus.interrupted.length : 0 const communicationItems = [ { label: 'Actionable', value: actionableCount }, { label: 'Protocol', value: protocolCount }, { label: 'Notifications', value: notificationCount }, { label: 'Meetings', value: commsState?.meetings?.length ?? 0 }, { label: 'Failures', value: commsState?.recent_failures?.length ?? 0 }, ] const teamCards = useMemo(() => { const digestBySeatId = new Map>() for (const digest of seatDigests) { const seatId = String(digest.seat_id ?? '').trim() if (seatId) digestBySeatId.set(seatId, asRecord(digest.manager_digest)) } const cards: TeamCardInfo[] = runtimeView.runtimeTeams.map((team) => { const teamKeys = [ String(team.cell_id ?? '').trim(), String(team.team_id ?? '').trim(), String(team.team_instance_id ?? '').trim(), ].filter(Boolean) const seats = runtimeView.runtimeSeats.filter((seat) => ( teamKeys.includes(String(seat.team_id ?? '').trim()) || teamKeys.includes(String(seat.team_instance_id ?? '').trim()) )) const pendingApprovals = seats.reduce((count, seat) => { const managerDigest = digestBySeatId.get(String(seat.seat_id ?? '').trim()) ?? {} return count + asRecordList(managerDigest.pending_decisions).length }, 0) const latestAlert = seats .map((seat) => summarizeText(seat.latest_notification?.subject ?? seat.latest_notification?.summary, '')) .find(Boolean) return { key: String(team.cell_id ?? team.team_id ?? team.team_instance_id ?? team.manager_role_id ?? 'team'), label: humanizeId(team.team_id ?? team.cell_id, 'Team'), managerLabel: humanizeId(team.manager_role_id, 'Unassigned manager'), status: summarizeText(team.status, 'idle'), seatCount: seats.length || team.member_role_ids.length, pendingApprovals, latestAlert, seats: seats.map((seat) => ({ key: String(seat.seat_id ?? seat.role_session_id ?? seat.role_id), roleLabel: humanizeId(seat.role_id, 'Seat'), seatLabel: summarizeText(seat.seat_id ?? seat.role_session_id, 'Seat'), status: summarizeText(seat.resident_status ?? seat.status, 'idle'), })), } }) const coveredTeamKeys = new Set( cards.flatMap((card) => card.seats.map((seat) => seat.key)) ) for (const seat of runtimeView.runtimeSeats) { const seatKey = String(seat.seat_id ?? seat.role_session_id ?? seat.role_id).trim() if (!seatKey || coveredTeamKeys.has(seatKey)) continue const teamKey = String(seat.team_id ?? seat.team_instance_id ?? 'unassigned').trim() || 'unassigned' let card = cards.find((item) => item.key === teamKey) if (!card) { card = { key: teamKey, label: humanizeId(teamKey, 'Unassigned Team'), managerLabel: 'Unassigned manager', status: summarizeText(seat.status, 'idle'), seatCount: 0, pendingApprovals: 0, seats: [], } cards.push(card) } const managerDigest = digestBySeatId.get(String(seat.seat_id ?? '').trim()) ?? {} card.pendingApprovals += asRecordList(managerDigest.pending_decisions).length if (!card.latestAlert) { card.latestAlert = summarizeText(seat.latest_notification?.subject ?? seat.latest_notification?.summary, '') } card.seats.push({ key: seatKey, roleLabel: humanizeId(seat.role_id, 'Seat'), seatLabel: summarizeText(seat.seat_id ?? seat.role_session_id, 'Seat'), status: summarizeText(seat.resident_status ?? seat.status, 'idle'), }) card.seatCount = card.seats.length } return cards.sort((left, right) => left.label.localeCompare(right.label)) }, [runtimeView.runtimeTeams, runtimeView.runtimeSeats, seatDigests]) const latestAlert = teamCards.map((team) => team.latestAlert).find(Boolean) || 'No current alerts' if (!projectRun?.run_id && teamCards.length === 0) { return null } return (
Team {summarizeText(projectRun?.run_id, 'Runtime Team Status')} {onStopRun && ( )}
{summarizeText(projectRun?.lifecycle_status, 'active')} Teams {teamCards.length} Seats {runtimeView.runtimeSeats.length} Approvals {pendingDecisionCount} Unread {unreadCount} Recovery {interrupted > 0 ? `${interrupted} interrupted` : summarizeText(asRecord(projectRun?.recovery_pointer).status, 'clean')}

Communication

{communicationItems.length}
{communicationItems.map((item) => ( {item.label} {item.value} ))}
{latestAlert}

Teams

{teamCards.length}
{teamCards.map((team) => (
{team.label} {team.managerLabel}
{team.status}
Seats {team.seatCount} Approvals {team.pendingApprovals} {team.latestAlert && {team.latestAlert}}
{team.seats.length > 0 ? team.seats.map((seat) => (
{seat.roleLabel} {seat.seatLabel} {seat.status}
)) : (
No seats assigned yet
)}
))}
) }