Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions web/src/monitors/ApprovalsQueuePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { CSSProperties } from 'react';
import type { SessionSummary } from '@/state/useSessionList';
import { Monitor } from './Monitor';

interface Props {
queue: SessionSummary[];
onSelect: (sid: string) => void;
}

const row: CSSProperties = {
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '6px 8px',
borderBottom: '1px solid var(--hair)',
cursor: 'pointer',
fontFamily: 'var(--ff-sans)',
};

function ageStr(iso: string): string {
const t = new Date(iso).getTime();
if (isNaN(t)) return '—';
const sec = Math.max(0, Math.floor((Date.now() - t) / 1000));
if (sec < 60) return `${sec}s`;
if (sec < 3600) return `${Math.floor(sec / 60)}m`;
return `${Math.floor(sec / 3600)}h`;
}

export function ApprovalsQueuePanel({ queue, onSelect }: Props) {
return (
<Monitor title="Approvals Queue" count={queue.length} pinned>
{queue.length === 0 ? (
<div style={{ fontSize: 11, color: 'var(--good)' }}>No approvals waiting.</div>
) : (
queue.map((s) => (
<div key={s.id} data-row onClick={() => onSelect(s.id)} style={row}>
<span style={{ fontFamily: 'var(--ff-mono)', fontSize: 11, color: 'var(--ink-1)' }}>{s.id}</span>
<span
style={{
flex: 1,
fontSize: 11,
color: 'var(--ink-2)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{s.label ?? '—'}
</span>
<span style={{ fontFamily: 'var(--ff-mono)', fontSize: 10, color: 'var(--warn)' }}>
{ageStr(s.updated_at)}
</span>
</div>
))
)}
</Monitor>
);
}
74 changes: 74 additions & 0 deletions web/src/monitors/HealthPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useQuery } from '@tanstack/react-query';
import { Monitor } from './Monitor';

interface Health {
status: 'ok' | 'degraded' | 'down' | string;
uptime_seconds?: number;
}

const dotColor: Record<string, string> = {
ok: 'var(--good)',
degraded: 'var(--warn)',
down: 'var(--danger)',
};

function uptimeStr(sec: number | undefined): string | null {
if (sec === undefined || sec === null) return null;
if (sec < 60) return `${sec}s`;
if (sec < 3600) return `${Math.floor(sec / 60)}m`;
if (sec < 86400) return `${Math.floor(sec / 3600)}h`;
return `${Math.floor(sec / 86400)}d`;
}

export function HealthPanel() {
const { data, dataUpdatedAt } = useQuery<Health>({
queryKey: ['health'],
// /health is at the server root (NOT /api/v1) — bypass apiFetch's prefix.
queryFn: async () => {
const r = await fetch('/health', { method: 'GET' });
if (!r.ok) throw new Error('health check failed');
return (await r.json()) as Health;
},
refetchInterval: 30_000,
});

const status = data?.status ?? 'unknown';
const uptime = uptimeStr(data?.uptime_seconds);
const lastPoll = dataUpdatedAt
? new Date(dataUpdatedAt).toLocaleTimeString(undefined, {
hour12: false,
})
: null;

return (
<Monitor title="System Health" pinned={false}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
<span
data-health-dot
data-status={status}
aria-hidden
style={{
display: 'inline-block',
width: 6,
height: 6,
borderRadius: '50%',
background: dotColor[status] ?? 'var(--ink-3)',
}}
/>
<span style={{ fontFamily: 'var(--ff-sans)', fontSize: 12, color: 'var(--ink-1)' }}>
{status}
</span>
</div>
{uptime !== null && (
<div style={{ fontFamily: 'var(--ff-mono)', fontSize: 11, color: 'var(--ink-3)' }}>
uptime {uptime}
</div>
)}
{lastPoll !== null && (
<div style={{ fontFamily: 'var(--ff-mono)', fontSize: 11, color: 'var(--ink-3)' }}>
last poll {lastPoll}
</div>
)}
</Monitor>
);
}
73 changes: 73 additions & 0 deletions web/src/monitors/LessonsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '@/api/client';
import { Monitor } from './Monitor';

interface Lesson {
id?: string;
title?: string;
summary?: string;
agent?: string;
[k: string]: unknown;
}

interface Props {
sessionId: string | null;
}

export function LessonsPanel({ sessionId }: Props) {
const { data } = useQuery<Lesson[]>({
queryKey: ['lessons', sessionId],
queryFn: () => apiFetch<Lesson[]>(`/sessions/${sessionId}/lessons`),
enabled: sessionId !== null,
staleTime: 60_000,
});

return (
<Monitor title="Lessons" count={data?.length ?? 0} pinned={false}>
{!data || data.length === 0 ? (
<div style={{ fontSize: 11, color: 'var(--ink-3)' }}>
No lessons relevant to this session yet.
</div>
) : (
data.map((l, i) => (
<div key={l.id ?? i} style={{ marginBottom: 8 }}>
<div
style={{
fontFamily: 'var(--ff-sans)',
fontSize: 12,
color: 'var(--ink-1)',
fontWeight: 500,
}}
>
{l.title ?? '—'}
</div>
{l.summary && (
<div
style={{
fontFamily: 'var(--ff-sans)',
fontSize: 11,
color: 'var(--ink-2)',
marginTop: 2,
}}
>
{l.summary}
</div>
)}
{l.agent && (
<div
style={{
fontFamily: 'var(--ff-mono)',
fontSize: 10,
color: 'var(--ink-3)',
marginTop: 2,
}}
>
{l.agent}
</div>
)}
</div>
))
)}
</Monitor>
);
}
97 changes: 97 additions & 0 deletions web/src/monitors/OtherSessionsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { CSSProperties } from 'react';
import type { SessionSummary } from '@/state/useSessionList';
import { Monitor } from './Monitor';

interface Props {
sessions: SessionSummary[];
activeSid: string | null;
onSelect: (sid: string) => void;
}

const tile: CSSProperties = {
display: 'flex',
flexDirection: 'column',
gap: 2,
padding: '6px 8px',
border: '1px solid var(--hair)',
background: 'var(--bg-elev)',
cursor: 'pointer',
fontFamily: 'var(--ff-sans)',
marginBottom: 4,
};

const stateColor: Record<string, string> = {
in_progress: 'var(--acc)',
awaiting_input: 'var(--warn)',
resolved: 'var(--good)',
error: 'var(--danger)',
stopped: 'var(--ink-3)',
matched: 'var(--good)',
escalated: 'var(--danger)',
new: 'var(--acc)',
};

function shortStatus(s: string): string {
return s.replace('_', ' ').slice(0, 6).toUpperCase();
}

export function OtherSessionsPanel({ sessions, activeSid, onSelect }: Props) {
const others = sessions.filter((s) => s.id !== activeSid);
return (
<Monitor title="Other Sessions" count={others.length} pinned>
{others.length === 0 ? (
<div style={{ fontSize: 11, color: 'var(--ink-3)' }}>
Only this session is active right now.
</div>
) : (
others.map((s) => (
<div key={s.id} data-tile onClick={() => onSelect(s.id)} style={tile}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 6 }}>
<span
style={{
fontFamily: 'var(--ff-mono)',
fontSize: 11,
color: 'var(--ink-1)',
}}
>
{s.id}
</span>
<span
style={{
fontFamily: 'var(--ff-mono)',
fontSize: 9,
color: stateColor[s.status] ?? 'var(--ink-3)',
letterSpacing: '0.14em',
}}
>
{shortStatus(s.status)}
</span>
</div>
<div
style={{
fontSize: 11,
color: 'var(--ink-2)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{s.label ?? '—'}
</div>
{s.active_agent && (
<div
style={{
fontFamily: 'var(--ff-mono)',
fontSize: 10,
color: 'var(--ink-3)',
}}
>
{s.active_agent}
</div>
)}
</div>
))
)}
</Monitor>
);
}
Loading
Loading