import { useEffect, useState } from 'react'; import { type DashboardInboxAction, type DashboardOverview, type StatusSnapshot, fetchDashboardData, runInboxAction, sendRoomMessage, } from './api'; import { isLocale, matchLocale, type Locale } from './i18n'; import { GlassesPanel } from './GlassesPanel'; import './styles.css'; import './glasses.css'; interface GlassesState { overview: DashboardOverview; snapshots: StatusSnapshot[]; } type InboxItem = DashboardOverview['inbox'][number]; type InboxActionKey = `${string}:${DashboardInboxAction}`; const REFRESH_INTERVAL_MS = 15_000; const DASHBOARD_STALE_MS = 75_000; const LOCALE_STORAGE_KEY = 'ejclaw.dashboard.locale.v2'; function makeClientRequestId(): string { return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; } 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 'en'; } function dashboardAgeMs(value: string | null | undefined): number | null { if (!value) return null; const date = new Date(value); if (Number.isNaN(date.getTime())) return null; return Math.max(0, Date.now() - date.getTime()); } function freshnessLabel( online: boolean, generatedAt: string | null | undefined, ): string { if (!online) return 'offline'; const age = dashboardAgeMs(generatedAt); if (age !== null && age > DASHBOARD_STALE_MS) return 'stale'; return 'fresh'; } function readNickname(): string { if (typeof window === 'undefined') return ''; return window.localStorage.getItem('ejclaw-nickname') ?? ''; } export function GlassesApp() { const [data, setData] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [online, setOnline] = useState(() => typeof navigator === 'undefined' ? true : navigator.onLine, ); const [locale] = useState(readInitialLocale); const [inboxActionKey, setInboxActionKey] = useState( null, ); const [roomMessageKey, setRoomMessageKey] = useState(null); async function refresh(showSpinner = false) { if (showSpinner) setRefreshing(true); try { const nextData = await fetchDashboardData(); setData({ overview: nextData.overview, snapshots: nextData.snapshots, }); setError(null); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); setRefreshing(false); } } async function handleInboxAction( item: InboxItem, action: DashboardInboxAction, ) { const actionKey: InboxActionKey = `${item.id}:${action}`; setInboxActionKey(actionKey); try { await runInboxAction(item.id, action, { lastOccurredAt: item.lastOccurredAt, requestId: makeClientRequestId(), }); await refresh(false); return true; } catch (err) { setError(err instanceof Error ? err.message : String(err)); return false; } finally { setInboxActionKey(null); } } async function handleRoomMessage( roomJid: string, text: string, requestId: string, ) { setRoomMessageKey(roomJid); try { await sendRoomMessage(roomJid, text, requestId, readNickname() || null); void refresh(false); return true; } catch (err) { setError(err instanceof Error ? err.message : String(err)); return false; } finally { setRoomMessageKey(null); } } useEffect(() => { document.documentElement.lang = locale; }, [locale]); useEffect(() => { function handleOnline() { setOnline(true); } function handleOffline() { setOnline(false); } window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); useEffect(() => { void refresh(); const id = window.setInterval(() => { void refresh(); }, REFRESH_INTERVAL_MS); return () => window.clearInterval(id); }, []); if (loading && !data) { return (
EJClaw Loading
); } if (!data) { return (

{error ?? 'Dashboard unavailable'}

); } return ( void refresh(true)} onSendRoomMessage={handleRoomMessage} overview={data.overview} refreshing={refreshing} roomMessageKey={roomMessageKey} snapshots={data.snapshots} /> ); }