import { useEffect, useMemo, useState, type ReactNode } from 'react'; import { type DashboardOverview, type DashboardTask, type StatusSnapshot, fetchDashboardData, } from './api'; import { LOCALES, isLocale, languageNames, localeTags, matchLocale, messages, type Locale, type Messages, } from './i18n'; import './styles.css'; interface DashboardState { overview: DashboardOverview; snapshots: StatusSnapshot[]; tasks: DashboardTask[]; } type UsageRow = DashboardOverview['usage']['rows'][number]; type RiskLevel = 'ok' | 'warn' | 'critical'; type UsageGroup = 'primary' | 'codex'; type DashboardView = 'usage' | 'health' | 'rooms' | 'scheduled'; type TaskGroupKey = 'watchers' | 'scheduled' | 'paused' | 'completed'; type TaskResultTone = 'ok' | 'fail' | 'none'; const REFRESH_INTERVAL_MS = 15_000; const LOCALE_STORAGE_KEY = 'ejclaw.dashboard.locale'; const DEFAULT_VIEW: DashboardView = 'usage'; function isDashboardView( value: string | null | undefined, ): value is DashboardView { return ( value === 'usage' || value === 'health' || value === 'rooms' || value === 'scheduled' ); } function readViewFromHash(): DashboardView { if (typeof window === 'undefined') return DEFAULT_VIEW; const raw = window.location.hash.replace(/^#\/?/, ''); return isDashboardView(raw) ? raw : DEFAULT_VIEW; } function readInitialLocale(): Locale { const stored = typeof window === 'undefined' ? null : window.localStorage.getItem(LOCALE_STORAGE_KEY); if (isLocale(stored)) return stored; const languages = typeof navigator === 'undefined' ? [] : [...(navigator.languages || []), navigator.language]; for (const language of languages) { const matched = matchLocale(language); if (matched) return matched; } return 'ko'; } function formatDate(value: string | null | undefined, locale: Locale): string { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return new Intl.DateTimeFormat(localeTags[locale], { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', }).format(date); } function formatTaskDate( value: string | null | undefined, locale: Locale, ): string { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return new Intl.DateTimeFormat(localeTags[locale], { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }).format(date); } function formatRelativeDate( value: string | null | undefined, locale: Locale, t: Messages, ): string { if (!value) return t.tasks.noTime; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; const diffMs = date.getTime() - Date.now(); const absMs = Math.abs(diffMs); if (absMs < 45_000) return t.tasks.now; const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [ ['day', 86_400_000], ['hour', 3_600_000], ['minute', 60_000], ]; const [unit, unitMs] = units.find(([, threshold]) => absMs >= threshold) ?? units.at(-1)!; return new Intl.RelativeTimeFormat(localeTags[locale], { numeric: 'auto', style: 'short', }).format(Math.round(diffMs / unitMs), unit); } function formatPct(value: number): string { if (value < 0) return '-'; return `${Math.round(value)}%`; } function usagePeak(row: UsageRow): number { return Math.max(row.h5pct, row.d7pct); } function usageRiskLevel(row: UsageRow): RiskLevel { const peak = usagePeak(row); if (peak >= 85) return 'critical'; if (peak >= 65) return 'warn'; return 'ok'; } function usageGroup(row: UsageRow): UsageGroup { return row.name.toLowerCase().startsWith('codex') ? 'codex' : 'primary'; } function statusLabel(status: string, t: Messages): string { if (status in t.status) return t.status[status as keyof Messages['status']]; return status; } function formatDuration(value: number | null, t: Messages): string { if (value === null) return '-'; const seconds = Math.floor(value / 1000); if (seconds < 60) return `${seconds}${t.units.second}`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}${t.units.minute}`; const hours = Math.floor(minutes / 60); return `${hours}${t.units.hour} ${minutes % 60}${t.units.minute}`; } function queueLabel( pendingTasks: number, pendingMessages: boolean, t: Messages, ) { const parts = [`${pendingTasks} ${t.units.task}`]; if (pendingMessages) parts.push(t.units.messageShort); return parts.join(' · '); } const SECRET_ASSIGNMENT_RE = /\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY|AUTH|PRIVATE_KEY)[A-Z0-9_]*)\s*=\s*([^\s"'`]+)/gi; const SECRET_VALUE_RE = /\b(?:sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{12,}|xox[baprs]-[A-Za-z0-9-]{12,}|eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,})\b/g; function safePreview( value: string | null | undefined, fallback: string, ): string { const cleaned = (value ?? '') .replace(SECRET_ASSIGNMENT_RE, '$1=') .replace(SECRET_VALUE_RE, '') .replace(/\s+/g, ' ') .trim(); if (!cleaned) return fallback; return cleaned.length > 120 ? `${cleaned.slice(0, 120)}...` : cleaned; } function taskGroupKey(task: DashboardTask): TaskGroupKey { if (task.status === 'completed') return 'completed'; if (task.status === 'paused') return 'paused'; if (task.isWatcher) return 'watchers'; return 'scheduled'; } function taskResultTone(task: DashboardTask): TaskResultTone { if (!task.lastResult) return 'none'; const normalized = task.lastResult.toLowerCase(); if ( normalized.includes('fail') || normalized.includes('error') || normalized.includes('timeout') || normalized.includes('cancel') || normalized.includes('reject') ) { return 'fail'; } return 'ok'; } function taskDisplayName(task: DashboardTask, t: Messages): string { if (task.isWatcher) return t.tasks.ciWatch; if (task.scheduleType) return task.scheduleType; return task.id; } function navItems(t: Messages) { return [ { href: '#/usage', label: t.nav.usage, view: 'usage' as const }, { href: '#/health', label: t.nav.health, view: 'health' as const }, { href: '#/rooms', label: t.nav.rooms, view: 'rooms' as const }, { href: '#/scheduled', label: t.nav.scheduled, view: 'scheduled' as const }, ]; } function EmptyState({ children }: { children: ReactNode }) { return
{children}
; } function LanguageSelector({ locale, onLocaleChange, t, }: { locale: Locale; onLocaleChange: (locale: Locale) => void; t: Messages; }) { return ( ); } function LoadingSkeleton({ t }: { t: Messages }) { return (
{Array.from({ length: 4 }, (_, index) => (
))}
); } function SideRail({ activeView, lastRefreshed, locale, onNavigate, onLocaleChange, onRefresh, refreshing, t, }: { activeView: DashboardView; lastRefreshed: string | null; locale: Locale; onNavigate: (view: DashboardView) => void; onLocaleChange: (locale: Locale) => void; onRefresh: () => void; refreshing: boolean; t: Messages; }) { return ( ); } function SectionNav({ activeView, drawerOpen, lastRefreshed, locale, onCloseDrawer, onLocaleChange, onNavigate, onOpenDrawer, refreshing, onRefresh, t, }: { activeView: DashboardView; drawerOpen: boolean; lastRefreshed: string | null; locale: Locale; onCloseDrawer: () => void; onLocaleChange: (locale: Locale) => void; onNavigate: (view: DashboardView) => void; onOpenDrawer: () => void; refreshing: boolean; onRefresh: () => void; t: Messages; }) { const activeLabel = navItems(t).find((item) => item.view === activeView)?.label ?? t.nav.usage; return ( <> {drawerOpen ? ( <>
{t.nav.updated} {formatDate(lastRefreshed, locale)}
) : null} ); } function ControlRail({ data, t }: { data: DashboardState; t: Messages }) { const queue = data.snapshots.reduce( (acc, snapshot) => { for (const entry of snapshot.entries) { acc.pendingTasks += entry.pendingTasks; if (entry.pendingMessages) acc.pendingMessageRooms += 1; } return acc; }, { pendingTasks: 0, pendingMessageRooms: 0 }, ); return (
{t.metrics.rooms} {data.overview.rooms.active + data.overview.rooms.waiting}/ {data.overview.rooms.total} {t.control.activeRooms}
{t.control.queue} {queue.pendingTasks} {queue.pendingMessageRooms} {t.control.pendingRooms}
{t.metrics.agents} {data.overview.services.length} {t.panels.heartbeat}
{t.metrics.ciWatchers} {data.overview.tasks.watchers.active} {data.overview.tasks.watchers.paused} {t.status.paused}
); } function ServicePanel({ overview, locale, t, }: { overview: DashboardOverview; locale: Locale; t: Messages; }) { if (overview.services.length === 0) { return {t.service.empty}; } return (
{overview.services.map((service) => (
{service.agentType}

{service.assistantName}

{t.service.heartbeat} {formatDate(service.updatedAt, locale)}
{t.service.service}
{service.serviceId}
{t.service.rooms}
{service.activeRooms}/{service.totalRooms} {t.status.active}
{t.service.updated}
{formatDate(service.updatedAt, locale)}
))}
); } function RoomPanel({ snapshots, t, }: { snapshots: StatusSnapshot[]; t: Messages; }) { const entries = snapshots.flatMap((snapshot) => snapshot.entries.map((entry) => ({ ...entry, serviceId: snapshot.serviceId, })), ); if (entries.length === 0) { return {t.rooms.empty}; } return ( <>
{entries.map((entry) => ( ))}
{t.rooms.room} {t.rooms.service} {t.rooms.agent} {t.rooms.status} {t.rooms.queue}
{entry.name} {entry.folder} · {entry.jid} {entry.serviceId} {entry.agentType} {statusLabel(entry.status, t)} {formatDuration(entry.elapsedMs, t)} {queueLabel(entry.pendingTasks, entry.pendingMessages, t)}
{entries.map((entry) => (
{entry.name} {entry.folder}
{statusLabel(entry.status, t)}
{t.rooms.queue} {queueLabel(entry.pendingTasks, entry.pendingMessages, t)} {t.rooms.agent} {entry.agentType} {t.rooms.service} {entry.serviceId} {t.rooms.elapsed} {formatDuration(entry.elapsedMs, t)}

{entry.jid}

))}
); } function UsageMeter({ label, pct, reset, rowName, t, }: { label: string; pct: number; reset: string; rowName: string; t: Messages; }) { return (
{label} {formatPct(pct)}
{t.usage.reset} {reset || '-'}
); } function UsagePanel({ overview, locale, t, }: { overview: DashboardOverview; locale: Locale; t: Messages; }) { const rows = useMemo(() => [...overview.usage.rows], [overview.usage.rows]); const watched = rows.filter((row) => usagePeak(row) >= 65).length; if (rows.length === 0) { return {t.usage.empty}; } const highest = rows[0]; const groups = [ { key: 'primary' as const, label: t.usage.groupPrimary, rows: rows.filter((row) => usageGroup(row) === 'primary'), }, { key: 'codex' as const, label: t.usage.groupCodex, rows: rows.filter((row) => usageGroup(row) === 'codex'), }, ].filter((group) => group.rows.length > 0); return (
{t.usage.highest} {highest.name} · {formatPct(usagePeak(highest))}
{t.usage.watch} {watched}
{t.usage.updated} {formatDate(overview.usage.fetchedAt, locale)}
{t.usage.usage} {t.usage.window5h} {t.usage.window7d}
{groups.map((group) => (
{group.label}
{group.rows.map((row) => { const risk = usageRiskLevel(row); return (
{row.name} {t.usage.risk[risk]}
); })}
))}
); } function TaskPanel({ tasks, locale, t, }: { tasks: DashboardTask[]; locale: Locale; t: Messages; }) { const taskGroups = useMemo(() => { const groups: Record = { watchers: [], scheduled: [], paused: [], completed: [], }; for (const task of tasks) { groups[taskGroupKey(task)].push(task); } for (const groupTasks of Object.values(groups)) { groupTasks.sort((a, b) => (a.nextRun ?? a.lastRun ?? a.createdAt).localeCompare( b.nextRun ?? b.lastRun ?? b.createdAt, ), ); } return [ { key: 'watchers' as const, tasks: groups.watchers }, { key: 'scheduled' as const, tasks: groups.scheduled }, { key: 'paused' as const, tasks: groups.paused }, { key: 'completed' as const, tasks: groups.completed }, ]; }, [tasks]); if (tasks.length === 0) { return {t.tasks.empty}; } return (
{taskGroups.map((group) => { const label = t.tasks.groups[group.key]; const groupHead = (
{label} {group.tasks.length} {t.tasks.count}
{group.tasks.length}
); const groupBody = group.tasks.length === 0 ? (
{t.tasks.groupEmpty}
) : (
{group.tasks.map((task) => { const resultTone = taskResultTone(task); const lastResult = safePreview( task.lastResult, t.tasks.noResult, ); return (
{taskDisplayName(task, t)} {task.groupFolder}
{statusLabel(task.status, t)} {task.ciProvider ? ( {task.ciProvider} ) : null}
{t.tasks.next} {formatTaskDate(task.nextRun, locale)} {formatRelativeDate(task.nextRun, locale, t)} {t.tasks.last} {formatTaskDate(task.lastRun, locale)} {formatRelativeDate(task.lastRun, locale, t)} {t.tasks.schedule} {task.scheduleType} {task.scheduleValue}
{task.suspendedUntil ? (
{t.tasks.suspendedUntil} {formatTaskDate(task.suspendedUntil, locale)} {formatRelativeDate(task.suspendedUntil, locale, t)}
) : null}
{resultTone === 'fail' ? t.tasks.resultFail : resultTone === 'ok' ? t.tasks.resultOk : t.tasks.result} {lastResult}
{t.tasks.prompt}

{safePreview(task.promptPreview, t.tasks.emptyPrompt)}

{task.id} · {task.contextMode} · {task.promptLength}{' '} {t.units.chars}
); })}
); if (group.key === 'completed') { return (
{label} {group.tasks.length} {t.tasks.count}
{group.tasks.length}
{groupBody}
); } return (
{groupHead} {groupBody}
); })}
); } function App() { const [data, setData] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [lastRefreshed, setLastRefreshed] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); const [activeView, setActiveView] = useState(readViewFromHash); const [locale, setLocale] = useState(readInitialLocale); const t = messages[locale]; function setDashboardLocale(nextLocale: Locale) { setLocale(nextLocale); window.localStorage.setItem(LOCALE_STORAGE_KEY, nextLocale); } function navigateToView(view: DashboardView) { setActiveView(view); if (window.location.hash !== `#/${view}`) { window.location.hash = `/${view}`; } } async function refresh(showSpinner = false) { if (showSpinner) setRefreshing(true); try { const nextData = await fetchDashboardData(); setData(nextData); setLastRefreshed(new Date().toISOString()); setError(null); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); setRefreshing(false); } } useEffect(() => { document.documentElement.lang = localeTags[locale]; }, [locale]); useEffect(() => { function handleHashChange() { setActiveView(readViewFromHash()); } handleHashChange(); window.addEventListener('hashchange', handleHashChange); return () => window.removeEventListener('hashchange', handleHashChange); }, []); useEffect(() => { void refresh(); const id = window.setInterval(() => { void refresh(); }, REFRESH_INTERVAL_MS); return () => window.clearInterval(id); }, []); useEffect(() => { if (!drawerOpen) return; function handleKeyDown(event: KeyboardEvent) { if (event.key === 'Escape') setDrawerOpen(false); } window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [drawerOpen]); if (loading && !data) { return ; } return (
void refresh(true)} refreshing={refreshing} t={t} />
setDrawerOpen(false)} onLocaleChange={setDashboardLocale} onNavigate={navigateToView} onOpenDrawer={() => setDrawerOpen(true)} onRefresh={() => void refresh(true)} refreshing={refreshing} t={t} /> {error ? (
{t.error.api}: {error}
) : null} {data ? (
{activeView === 'usage' ? ( <>

{t.panels.usage}

{t.panels.usageWindow}
) : null} {activeView === 'health' ? (

{t.panels.health}

{t.panels.heartbeat}
) : null} {activeView === 'rooms' ? (

{t.panels.rooms}

{t.panels.queue}
) : null} {activeView === 'scheduled' ? (

{t.panels.scheduled}

{t.panels.promptPreviews}
) : null}
) : null}
); } export default App;