/* ============================================================ hud.jsx — Phase 1 full-screen HUD overlay ============================================================ - Cmd+H / Ctrl+H opens overlay with default 3 widgets - Esc closes - Widgets self-fetch their data - Exposed via window.HUD, window.useHUD, window.HUD_WIDGETS */ const { useState: _hudUseState, useEffect: _hudUseEffect, useCallback: _hudUseCallback } = React; /* ---------- shared HUD state (window-level + custom event) ---------- */ if (typeof window.__hudState__ === 'undefined') { window.__hudState__ = { open: false, widgets: null, title: null }; } function _setHudState(next) { window.__hudState__ = { ...window.__hudState__, ...next }; window.dispatchEvent(new CustomEvent('hud-state-change', { detail: window.__hudState__ })); } function useHUD() { const [state, setState] = _hudUseState(() => window.__hudState__); _hudUseEffect(() => { const h = (e) => setState({ ...e.detail }); window.addEventListener('hud-state-change', h); return () => window.removeEventListener('hud-state-change', h); }, []); const openHUD = _hudUseCallback((widgets, title) => { _setHudState({ open: true, widgets: widgets || null, title: title || null }); }, []); const closeHUD = _hudUseCallback(() => { _setHudState({ open: false }); }, []); return { open: state.open, widgets: state.widgets, title: state.title, openHUD, closeHUD }; } /* ---------- individual widget render functions ---------- */ function _HudTodayRoster() { const [rows, setRows] = _hudUseState(null); const [err, setErr] = _hudUseState(null); _hudUseEffect(() => { let cancelled = false; fetch('/api/attendance', { credentials: 'same-origin' }) .then(r => r.ok ? r.json() : Promise.reject(r.status)) .then(d => { if (!cancelled) setRows(Array.isArray(d) ? d : []); }) .catch(e => { if (!cancelled) setErr(String(e)); }); return () => { cancelled = true; }; }, []); if (err) return
{(window.appT||(s=>s))("Error")}: {err}
; if (rows === null) return
{(window.appT||(s=>s))("Loading roster…")}
; if (!rows.length) return
{(window.appT||(s=>s))("No attendance data.")}
; const fmt = (iso) => { if (!iso) return '—'; const d = new Date(iso); if (isNaN(d)) return '—'; // 12-hour AM/PM in the BROWSER's local timezone (matches the rest of // the dashboard now that ack DMs and EditableCells are 12h too). return d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit', hour12: true }); }; const statusOf = (a) => { if (a.signed_off) return 'Off'; if (a.signed_in) return a.was_late ? 'Late' : 'In'; return 'Out'; }; const colorFor = (s) => s === 'In' ? 'var(--ok, #16a34a)' : s === 'Late' ? 'var(--warn, #d97706)' : s === 'Off' ? 'var(--text-3, #94a3b8)' : 'var(--text-3)'; return (
{rows.map((r, i) => { const s = statusOf(r); return ( ); })}
{(window.appT||(s=>s))("Person")} {(window.appT||(s=>s))("Status")} {(window.appT||(s=>s))("In")} {(window.appT||(s=>s))("Off")} {(window.appT||(s=>s))("Hours")}
{r.display_name || r.user_id || '—'} {(window.appT||(s=>s))(s)} {fmt(r.sign_in_at)} {fmt(r.sign_off_at)} {typeof r.hours_worked === 'number' ? r.hours_worked.toFixed(2) : '—'}
); } function _HudHoursBar() { const [rows, setRows] = _hudUseState(null); _hudUseEffect(() => { let cancelled = false; fetch('/api/attendance', { credentials: 'same-origin' }) .then(r => r.ok ? r.json() : []) .then(d => { if (!cancelled) setRows(Array.isArray(d) ? d : []); }) .catch(() => { if (!cancelled) setRows([]); }); return () => { cancelled = true; }; }, []); if (rows === null) return
{(window.appT||(s=>s))("Loading hours…")}
; const data = rows .filter(r => typeof r.hours_worked === 'number' && r.hours_worked > 0) .map(r => ({ label: (r.display_name || r.user_id || '?').split(/\s+/)[0].slice(0, 8), value: +Number(r.hours_worked).toFixed(2), })) .sort((a, b) => b.value - a.value) .slice(0, 14); if (!data.length) return
{(window.appT||(s=>s))("No hours logged yet today.")}
; const BC = window.BarChart; if (!BC) return
{(window.appT||(s=>s))("BarChart not loaded.")}
; return ; } function _HudAttendancePie() { const [rows, setRows] = _hudUseState(null); _hudUseEffect(() => { let cancelled = false; fetch('/api/attendance', { credentials: 'same-origin' }) .then(r => r.ok ? r.json() : []) .then(d => { if (!cancelled) setRows(Array.isArray(d) ? d : []); }) .catch(() => { if (!cancelled) setRows([]); }); return () => { cancelled = true; }; }, []); if (rows === null) return
{(window.appT||(s=>s))("Loading…")}
; let present = 0, late = 0, absent = 0; rows.forEach(r => { if (r.signed_in && r.was_late) late += 1; else if (r.signed_in) present += 1; else absent += 1; }); const total = present + late + absent || 1; // Inline SVG pie with brand colors const size = 220, r = size / 2 - 10, cx = size / 2, cy = size / 2; const c = 2 * Math.PI * r; let off = 0; const slice = (v, color, label) => { if (!(v > 0)) return null; const len = (v / total) * c; const node = ( {label}: {v} ); off += len; return node; }; const Legend = ({ color, label, value }) => (
{label} {value}
); return (
{slice(present, 'var(--ok, #16a34a)', (window.appT||(s=>s))('Present'))} {slice(late, 'var(--warn, #d97706)', (window.appT||(s=>s))('Late'))} {slice(absent, 'var(--text-3, #94a3b8)', (window.appT||(s=>s))('Absent'))}
s))("Present")} value={present} /> s))("Late")} value={late} /> s))("Absent")} value={absent} />
{(window.appT||(s=>s))("Total")}: {present + late + absent}
); } /* ---------- Phase 2 widgets ---------- */ // Tiny error boundary so one widget crash doesn't nuke the whole HUD. class _HudBoundary extends React.Component { constructor(p) { super(p); this.state = { err: null }; } static getDerivedStateFromError(e) { return { err: String(e && e.message || e) }; } componentDidCatch(_e, _info) { /* swallow */ } render() { if (this.state.err) { return
{(window.appT||(s=>s))("Render error")}: {this.state.err}
; } return this.props.children; } } function _hudFmt12(iso) { if (!iso) return '—'; const d = new Date(iso); if (isNaN(d)) return '—'; return d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit', hour12: true }); } function _hudFmt12Date(iso) { if (!iso) return '—'; const d = new Date(iso); if (isNaN(d)) return '—'; return d.toLocaleString([], { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true }); } /* compliance-table — strikes per user from /api/employees/strikes */ function _HudCompliance() { const [rows, setRows] = _hudUseState(null); const [err, setErr] = _hudUseState(null); _hudUseEffect(() => { let cancelled = false; fetch('/api/employees/strikes', { credentials: 'same-origin' }) .then(r => r.ok ? r.json() : Promise.reject(r.status)) .then(d => { if (!cancelled) setRows(Array.isArray(d) ? d : []); }) .catch(e => { if (!cancelled) setErr(String(e)); }); return () => { cancelled = true; }; }, []); if (err) return
{(window.appT||(s=>s))("Error")}: {err}
; if (rows === null) return
{(window.appT||(s=>s))("Loading strikes…")}
; if (!rows.length) return
{(window.appT||(s=>s))("No employees.")}
; const sorted = [...rows].sort((a, b) => (b.strike_count || 0) - (a.strike_count || 0)); const colorFor = (n) => n >= 3 ? 'var(--err, #dc2626)' : n >= 1 ? 'var(--warn, #d97706)' : 'var(--ok, #16a34a)'; return (
{sorted.map((r, i) => { const n = +(r.strike_count || 0); return ( ); })}
{(window.appT||(s=>s))("Person")} {(window.appT||(s=>s))("Strikes")} {(window.appT||(s=>s))("Last warning")}
{r.display_name || r.matrix_id || '—'} {n} {_hudFmt12Date(r.last_warning)}
); } /* monthly-hours-grid — per-user week_hours sparkline. Reuses /api/today/scoreboard (weekly aggregate) since there's no 30-day per-day endpoint — we render one sparkline per user from that user's scoreboard's day-level breakdown when present. */ function _HudMonthlyHours() { const [rows, setRows] = _hudUseState(null); const [err, setErr] = _hudUseState(null); _hudUseEffect(() => { let cancelled = false; fetch('/api/today/scoreboard', { credentials: 'same-origin' }) .then(r => r.ok ? r.json() : Promise.reject(r.status)) .then(d => { if (!cancelled) setRows(Array.isArray(d) ? d : []); }) .catch(e => { if (!cancelled) setErr(String(e)); }); return () => { cancelled = true; }; }, []); if (err) return
{(window.appT||(s=>s))("Error")}: {err}
; if (rows === null) return
{(window.appT||(s=>s))("Loading hours history…")}
; if (!rows.length) return
{(window.appT||(s=>s))("No data.")}
; const SP = window.Sparkline; const sorted = [...rows].sort((a, b) => (b.week_hours || 0) - (a.week_hours || 0)).slice(0, 20); return (
{sorted.map((r, i) => { // Build a small synthetic series from on_time_days/late_days/absent_days // when day_breakdown isn't a full per-day series. const series = Array.isArray(r.day_breakdown) ? r.day_breakdown.map(d => +Number(d.hours_worked || 0).toFixed(1)) : [ +(r.on_time_days || 0), +(r.late_days || 0), +(r.days_present || 0), +Number(r.week_hours || 0).toFixed(1), ]; return ( ); })}
{(window.appT||(s=>s))("Person")} {(window.appT||(s=>s))("Trend (week)")} {(window.appT||(s=>s))("Hours")} {(window.appT||(s=>s))("Present")}
{r.display_name || '—'} {SP ? : } {Number(r.week_hours || 0).toFixed(1)} {r.days_present || 0}/{r.days_expected || 0}
); } /* presence-now — uses the presence_today field on /api/attendance rows. */ function _HudPresence() { const [rows, setRows] = _hudUseState(null); const [err, setErr] = _hudUseState(null); _hudUseEffect(() => { let cancelled = false; fetch('/api/attendance', { credentials: 'same-origin' }) .then(r => r.ok ? r.json() : Promise.reject(r.status)) .then(d => { if (!cancelled) setRows(Array.isArray(d) ? d : []); }) .catch(e => { if (!cancelled) setErr(String(e)); }); return () => { cancelled = true; }; }, []); if (err) return
{(window.appT||(s=>s))("Error")}: {err}
; if (rows === null) return
{(window.appT||(s=>s))("Loading presence…")}
; if (!rows.length) return
{(window.appT||(s=>s))("No data.")}
; const enriched = rows.map(r => { const p = r.presence_today || {}; const on = +(p.online_minutes || p.online || 0); const aw = +(p.away_minutes || p.away || 0); const off= +(p.offline_minutes|| p.offline|| 0); return { name: r.display_name || r.user_id || '—', on, aw, off, total: on + aw + off }; }).filter(x => x.total > 0).sort((a, b) => b.on - a.on); if (!enriched.length) return
{(window.appT||(s=>s))("No presence telemetry today.")}
; const max = Math.max(...enriched.map(x => x.total)) || 1; return (
{enriched.map((x, i) => { const w = (n) => `${(n / max) * 100}%`; return (
{x.name}
s))("Online")} ${x.on}m`} style={{ width: w(x.on), background: 'var(--ok, #16a34a)' }} />
s))("Away")} ${x.aw}m`} style={{ width: w(x.aw), background: 'var(--warn, #d97706)' }} />
s))("Offline")} ${x.off}m`} style={{ width: w(x.off), background: 'var(--text-3, #94a3b8)' }} />
{x.on}m
); })}
); } /* signin-histogram — hours-of-day distribution from today's /api/attendance. (No 14-day historical endpoint is exposed publicly — we render today's distribution and label that clearly so we're honest about the source.) */ function _HudSigninHistogram() { const [rows, setRows] = _hudUseState(null); const [err, setErr] = _hudUseState(null); _hudUseEffect(() => { let cancelled = false; fetch('/api/attendance', { credentials: 'same-origin' }) .then(r => r.ok ? r.json() : Promise.reject(r.status)) .then(d => { if (!cancelled) setRows(Array.isArray(d) ? d : []); }) .catch(e => { if (!cancelled) setErr(String(e)); }); return () => { cancelled = true; }; }, []); if (err) return
{(window.appT||(s=>s))("Error")}: {err}
; if (rows === null) return
{(window.appT||(s=>s))("Loading…")}
; const bins = new Array(24).fill(0); rows.forEach(r => { if (!r.sign_in_at) return; const d = new Date(r.sign_in_at); if (isNaN(d)) return; bins[d.getHours()] += 1; }); const data = bins.map((v, h) => { const am = h === 0 ? '12a' : h < 12 ? `${h}a` : h === 12 ? '12p' : `${h - 12}p`; return { label: am, value: v }; }); const nonzero = data.some(x => x.value > 0); if (!nonzero) return
{(window.appT||(s=>s))("No sign-ins yet today.")}
; const BC = window.BarChart; if (!BC) return
{(window.appT||(s=>s))("BarChart not loaded.")}
; return (
{(window.appT||(s=>s))("Today's sign-ins by hour (12-hour)")}
); } /* audit-recent — last 20 audit events */ function _HudAuditRecent() { const [rows, setRows] = _hudUseState(null); const [err, setErr] = _hudUseState(null); _hudUseEffect(() => { let cancelled = false; fetch('/api/audit?limit=20', { credentials: 'same-origin' }) .then(r => r.ok ? r.json() : Promise.reject(r.status)) .then(d => { if (!cancelled) setRows(Array.isArray(d) ? d : []); }) .catch(e => { if (!cancelled) setErr(String(e)); }); return () => { cancelled = true; }; }, []); if (err) return
{(window.appT||(s=>s))("Error")}: {err}
; if (rows === null) return
{(window.appT||(s=>s))("Loading audit…")}
; if (!rows.length) return
{(window.appT||(s=>s))("No audit events.")}
; return (
{/* #6/#10 extension: Actor = who DID this (admin name, bot label if bot-initiated, derived from API's `actor` field). Subject = the employee it concerns (kept as a sub-line). */} {rows.map((r, i) => { const actor = r.actor || null; const subject = r.user_display_name || null; return ( {/* Issue #32: humanized label (API provides action_label; local table fallback via window.humanizeActionType). */} ); })}
{(window.appT||(s=>s))("When")}{(window.appT||(s=>s))("Actor · Subject")} {(window.appT||(s=>s))("Action")}
{_hudFmt12Date(r.occurred_at)}
{actor || subject || '—'} {actor && subject && actor !== subject && ( → {subject} )}
{r.action_label || (window.humanizeActionType ? window.humanizeActionType(r.action_type) : r.action_type) || '—'}
); } /* pto-calendar — current month, cells colored by PTO count */ function _HudPtoCalendar() { const [rows, setRows] = _hudUseState(null); const [err, setErr] = _hudUseState(null); const now = new Date(); const year = now.getFullYear(), month = now.getMonth(); _hudUseEffect(() => { let cancelled = false; const first = new Date(year, month, 1); const last = new Date(year, month + 1, 0); const iso = (d) => d.toISOString().slice(0, 10); const url = `/api/pto?from_date=${iso(first)}&to_date=${iso(last)}`; fetch(url, { credentials: 'same-origin' }) .then(r => r.ok ? r.json() : Promise.reject(r.status)) .then(d => { if (!cancelled) setRows(Array.isArray(d) ? d : []); }) .catch(e => { if (!cancelled) setErr(String(e)); }); return () => { cancelled = true; }; }, [year, month]); if (err) return
{(window.appT||(s=>s))("Error")}: {err}
; if (rows === null) return
{(window.appT||(s=>s))("Loading PTO…")}
; // Count PTO bodies per day. const counts = {}; rows.forEach(p => { const s = new Date(p.start_date), e = new Date(p.end_date); for (let d = new Date(s); d <= e; d.setDate(d.getDate() + 1)) { if (d.getFullYear() === year && d.getMonth() === month) { const k = d.getDate(); counts[k] = (counts[k] || 0) + 1; } } }); const daysInMonth = new Date(year, month + 1, 0).getDate(); const firstDow = new Date(year, month, 1).getDay(); // 0=Sun const cells = []; for (let i = 0; i < firstDow; i++) cells.push(null); for (let d = 1; d <= daysInMonth; d++) cells.push(d); while (cells.length % 7 !== 0) cells.push(null); const max = Math.max(1, ...Object.values(counts)); const colorFor = (n) => { if (!n) return 'rgba(255,255,255,0.04)'; const t = Math.min(1, n / max); // Blend brand-blue → warn for heat const r = Math.round(74 + (217 - 74) * t); const g = Math.round(170 + (119 - 170) * t); const b = Math.round(255 + (6 - 255) * t); return `rgb(${r},${g},${b})`; }; const todayDay = (now.getMonth() === month && now.getFullYear() === year) ? now.getDate() : -1; const monthName = now.toLocaleString([], { month: 'long', year: 'numeric' }); const dows = [ (window.appT||(s=>s))('Su'), (window.appT||(s=>s))('Mo'), (window.appT||(s=>s))('Tu'), (window.appT||(s=>s))('We'), (window.appT||(s=>s))('Th'), (window.appT||(s=>s))('Fr'), (window.appT||(s=>s))('Sa'), ]; return (
{monthName} — {rows.length} {(window.appT||(s=>s))("PTO blocks")}
{dows.map((d, i) => (
{d}
))} {cells.map((d, i) => d == null ? (
) : (
s))("on PTO")}`} style={{ background: colorFor(counts[d] || 0), borderRadius: 3, padding: '6px 4px', textAlign: 'center', fontFamily: 'var(--font-mono)', fontSize: 11, color: counts[d] ? '#0b0f14' : 'var(--text-2)', outline: d === todayDay ? '1px solid var(--brand, #4af)' : 'none', fontWeight: counts[d] ? 700 : 400, }}> {d}{counts[d] ?
×{counts[d]}
: null}
))}
); } /* schema-browser — DB table/column introspection via /api/ai/hud-schema */ function _HudSchemaBrowser() { const [tables, setTables] = _hudUseState(null); const [err, setErr] = _hudUseState(null); const [selected, setSelected] = _hudUseState(null); _hudUseEffect(() => { let cancelled = false; fetch('/api/ai/hud-schema', { credentials: 'same-origin' }) .then(r => r.ok ? r.json() : Promise.reject(r.status)) .then(d => { if (!cancelled) { const arr = Array.isArray(d) ? d : []; setTables(arr); if (arr.length) setSelected(arr[0].name); } }) .catch(e => { if (!cancelled) setErr(String(e)); }); return () => { cancelled = true; }; }, []); if (err) return
{(window.appT||(s=>s))("Error")}: {err}
; if (tables === null) return
{(window.appT||(s=>s))("Loading schema…")}
; if (!tables.length) return
{(window.appT||(s=>s))("No tables found.")}
; const tbl = tables.find(t => t.name === selected) || tables[0]; return (
{tables.map(t => (
setSelected(t.name)} style={{ padding: '3px 6px', borderRadius: 3, cursor: 'pointer', fontSize: 11, fontFamily: 'var(--font-mono)', color: t.name === selected ? 'var(--brand, #4af)' : 'var(--text-2)', background: t.name === selected ? 'rgba(74,170,255,0.12)' : 'transparent', marginBottom: 2, }}> {t.name}
))}
{tbl.doc &&
{tbl.doc}
} {(tbl.columns || []).map((c, i) => ( ))}
{(window.appT||(s=>s))("Name")} {(window.appT||(s=>s))("Type")} {(window.appT||(s=>s))("Nullable")} {(window.appT||(s=>s))("Default")} {(window.appT||(s=>s))("PK")}
{c.name} {c.type} {c.nullable ? (window.appT||(s=>s))('yes') : (window.appT||(s=>s))('no')} {c.default || '—'} {c.pk ? 'PK' : ''}
); } /* code-search — debounced grep of app/*.py via /api/ai/hud-codebase */ function _HudCodeSearch() { const [query, setQuery] = _hudUseState(''); const [results, setResults] = _hudUseState(null); const [err, setErr] = _hudUseState(null); const [loading, setLoading] = _hudUseState(false); const timerRef = React.useRef(null); _hudUseEffect(() => { const q = query.trim(); if (!q) { setResults(null); setErr(null); return; } clearTimeout(timerRef.current); timerRef.current = setTimeout(() => { setLoading(true); setErr(null); fetch(`/api/ai/hud-codebase?q=${encodeURIComponent(q)}`, { credentials: 'same-origin' }) .then(r => r.ok ? r.json() : Promise.reject(r.status)) .then(d => { setResults(Array.isArray(d) ? d : []); setLoading(false); }) .catch(e => { setErr(String(e)); setLoading(false); }); }, 300); return () => clearTimeout(timerRef.current); }, [query]); return (
setQuery(e.target.value)} placeholder={(window.appT||(s=>s))("Type a search term…")} style={{ background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(74,170,255,0.35)', borderRadius: 4, color: 'var(--text-1)', fontFamily: 'var(--font-mono)', fontSize: 12, padding: '6px 10px', outline: 'none', width: '100%', boxSizing: 'border-box', }} /> {loading &&
{(window.appT||(s=>s))("Searching…")}
} {err &&
{(window.appT||(s=>s))("Error")}: {err}
} {results !== null && !loading && ( results.length === 0 ?
{(window.appT||(s=>s))("No matches.")}
:
{results.map((m, i) => (
{m.path} :{m.line} {m.snippet}
))}
)} {results === null && !loading && !err && (
{(window.appT||(s=>s))("Type a search term to grep app/*.py")}
)}
); } const HUD_WIDGETS = { 'today-roster': { id: 'today-roster', title: (window.appT||(s=>s))('Today — Roster'), render: () => <_HudBoundary><_HudTodayRoster /> }, 'hours-bar': { id: 'hours-bar', title: (window.appT||(s=>s))('Hours Worked Today'), render: () => <_HudBoundary><_HudHoursBar /> }, 'attendance-pie': { id: 'attendance-pie', title: (window.appT||(s=>s))('Attendance Breakdown'), render: () => <_HudBoundary><_HudAttendancePie /> }, 'compliance-table': { id: 'compliance-table', title: (window.appT||(s=>s))('Compliance — strikes per user'), render: () => <_HudBoundary><_HudCompliance /> }, 'monthly-hours-grid': { id: 'monthly-hours-grid', title: (window.appT||(s=>s))('Hours — week trend'), render: () => <_HudBoundary><_HudMonthlyHours /> }, 'presence-now': { id: 'presence-now', title: (window.appT||(s=>s))('Live K9 presence'), render: () => <_HudBoundary><_HudPresence /> }, 'signin-histogram': { id: 'signin-histogram', title: (window.appT||(s=>s))('Sign-in time distribution'), render: () => <_HudBoundary><_HudSigninHistogram /> }, 'audit-recent': { id: 'audit-recent', title: (window.appT||(s=>s))('Recent audit events'), render: () => <_HudBoundary><_HudAuditRecent /> }, 'pto-calendar': { id: 'pto-calendar', title: (window.appT||(s=>s))('PTO calendar — this month'), render: () => <_HudBoundary><_HudPtoCalendar /> }, 'schema-browser': { id: 'schema-browser', title: (window.appT||(s=>s))('DB Schema Browser'), render: () => <_HudBoundary><_HudSchemaBrowser /> }, 'code-search': { id: 'code-search', title: (window.appT||(s=>s))('Codebase Search'), render: () => <_HudBoundary><_HudCodeSearch /> }, }; const DEFAULT_WIDGETS = [HUD_WIDGETS['today-roster'], HUD_WIDGETS['hours-bar'], HUD_WIDGETS['attendance-pie']]; /* ---------- main HUD component ---------- */ // Phase 6 — dedupe set persists for the full page session (module scope). const _hudSeenEventIds = new Set(); function HUD() { const { open, widgets, title, openHUD, closeHUD } = useHUD(); const [proactiveToast, setProactiveToast] = _hudUseState(null); // {phrase, id} const [mutedAutoOpen, setMutedAutoOpen] = _hudUseState( () => localStorage.getItem('hud_proactive_off') === '1' ); // Phase 6 — SSE connection for proactive HUD events. _hudUseEffect(() => { let es; try { es = new EventSource('/api/hud/stream', { withCredentials: true }); } catch (_) { return; } es.addEventListener('hud_proactive', (e) => { try { const ev = JSON.parse(e.data); const evId = ev.id || (ev.kind + ':' + ev.created_at); // Dedupe: skip already-seen event ids. if (_hudSeenEventIds.has(evId)) return; _hudSeenEventIds.add(evId); // Cap set size to avoid unbounded growth on long-lived tabs. if (_hudSeenEventIds.size > 200) { const first = _hudSeenEventIds.values().next().value; _hudSeenEventIds.delete(first); } const widgetList = (ev.widgets || []).map(id => HUD_WIDGETS[id]).filter(Boolean); const phrase = ev.phrase || (window.appT||(s=>s))('Proactive update'); // Read mute preference at receipt time (not just mount time). const muted = localStorage.getItem('hud_proactive_off') === '1'; if (window.__hudState__.open) { // HUD already open: surface non-blocking toast. setProactiveToast({ phrase, id: evId }); setTimeout(() => setProactiveToast(t => t && t.id === evId ? null : t), 8000); } else if (!muted && widgetList.length) { openHUD(widgetList, (window.appT||(s=>s))('Auto: ') + phrase.slice(0, 60)); } } catch (_) { /* parse error — ignore */ } }); return () => { try { es.close(); } catch (_) {} }; }, [openHUD]); // Sync mute toggle to localStorage. const toggleMute = _hudUseCallback(() => { setMutedAutoOpen(prev => { const next = !prev; if (next) localStorage.setItem('hud_proactive_off', '1'); else localStorage.removeItem('hud_proactive_off'); return next; }); }, []); // Global keybind: Cmd+H / Ctrl+H opens with defaults _hudUseEffect(() => { const onKey = (e) => { const isMod = e.metaKey || e.ctrlKey; if (isMod && (e.key === 'h' || e.key === 'H')) { // Avoid hijacking browser's Hide-Window (macOS Cmd+H is OS-level on // some apps but Chrome forwards it). Only open when not already open. e.preventDefault(); if (!window.__hudState__.open) { openHUD(DEFAULT_WIDGETS, (window.appT||(s=>s))('Tactical Overview')); } else { closeHUD(); } } if (e.key === 'Escape' && window.__hudState__.open) { e.preventDefault(); closeHUD(); } }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [openHUD, closeHUD]); // Phase 3 — deep-link `?hud=id1,id2&q=…` opens the HUD on page load with // the named widgets. Used by the bot's "Open in new tab" affordance and by // shareable HUD URLs. Runs once on mount. _hudUseEffect(() => { try { const params = new URLSearchParams(window.location.search); const ids = (params.get('hud') || '').split(',').map(s => s.trim()).filter(Boolean); if (!ids.length) return; const list = ids.map(i => HUD_WIDGETS[i]).filter(Boolean); if (!list.length) return; const q = params.get('q') || ''; openHUD(list, q ? `From: "${q.slice(0, 60)}"` : (window.appT||(s=>s))('Deep-linked')); // Strip the params from the URL so a refresh doesn't keep re-opening. const url = new URL(window.location.href); url.searchParams.delete('hud'); url.searchParams.delete('q'); window.history.replaceState({}, '', url.toString()); } catch (_) { /* no-op */ } }, [openHUD]); // Phase 6: proactive toast rendered even when HUD is closed // (so we can show it as a floating notification without opening the overlay). const toastEl = proactiveToast ? (
{(window.appT||(s=>s))("Proactive")}: {proactiveToast.phrase}
) : null; if (!open) return toastEl; // Single declaration only — a prior edit accidentally duplicated this // pair, which crashed babel-standalone with "Identifier 'list' has // already been declared" and left the whole React dashboard blank. const list = (widgets && widgets.length) ? widgets : DEFAULT_WIDGETS; const subtitle = title || (window.appT||(s=>s))('Tactical Overview'); const overlay = { position: 'fixed', inset: 0, zIndex: 9999, background: 'rgba(0,0,0,0.92)', backdropFilter: 'blur(8px)', WebkitBackdropFilter: 'blur(8px)', color: '#e8eef6', display: 'flex', flexDirection: 'column', fontFamily: 'inherit', }; const header = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 22px', borderBottom: '1px solid var(--brand, #4af)', boxShadow: '0 0 24px -6px var(--brand, #4af)', background: 'linear-gradient(180deg, rgba(0,0,0,0.6), rgba(0,0,0,0.2))', }; const titleStyle = { display: 'flex', alignItems: 'baseline', gap: 14, }; const titleMain = { fontFamily: 'var(--font-mono, ui-monospace, Menlo, monospace)', fontWeight: 700, letterSpacing: '0.18em', fontSize: 16, color: 'var(--brand, #4af)', textShadow: '0 0 12px var(--brand, #4af)', }; const titleSub = { fontFamily: 'var(--font-mono, ui-monospace, Menlo, monospace)', fontSize: 11, letterSpacing: '0.14em', textTransform: 'uppercase', color: '#9aa6b6', }; const closeBtn = { background: 'transparent', border: '1px solid var(--brand, #4af)', color: 'var(--brand, #4af)', borderRadius: 4, width: 32, height: 32, cursor: 'pointer', fontSize: 18, lineHeight: '28px', display:'flex', alignItems:'center', justifyContent:'center', }; const tabBtn = { ...closeBtn, width: 'auto', padding: '0 10px', fontSize: 11, fontFamily: 'var(--font-mono, ui-monospace, Menlo, monospace)', letterSpacing: '0.12em', textTransform: 'uppercase' }; // Build a deep-link to re-open this HUD in a new tab (preserves widget set // + question subtitle so the bot's voice flow can pop the HUD elsewhere). const openNewTab = () => { const ids = list.map(w => w.id).join(','); const q = (title && title.startsWith('From: ')) ? title.replace(/^From: \"?/, '').replace(/\"?$/, '') : ''; const url = `${window.location.origin}/?hud=${encodeURIComponent(ids)}` + (q ? `&q=${encodeURIComponent(q)}` : ''); window.open(url, '_blank', 'noopener'); }; const grid = { flex: 1, overflowY: 'auto', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(380px, 1fr))', gap: 16, padding: 22, }; const card = { background: 'rgba(8,12,18,0.85)', border: '1px solid rgba(74,170,255,0.35)', borderRadius: 6, padding: 14, boxShadow: 'inset 0 0 0 1px rgba(255,255,255,0.02), 0 0 18px -8px var(--brand, #4af)', display: 'flex', flexDirection: 'column', gap: 10, minHeight: 160, }; const cardTitle = { fontFamily: 'var(--font-mono, ui-monospace, Menlo, monospace)', fontSize: 11, letterSpacing: '0.14em', textTransform: 'uppercase', color: 'var(--brand, #4af)', borderBottom: '1px dashed rgba(74,170,255,0.3)', paddingBottom: 6, }; return (
{ if (e.target === e.currentTarget) closeHUD(); }}>
{(window.appT||(s=>s))("HUD")} ⛶
{subtitle}
{list.map(w => (
{w.title}
{w.render()}
))}
{toastEl}
); } window.HUD = HUD; window.useHUD = useHUD; window.HUD_WIDGETS = HUD_WIDGETS; window.HUD_DEFAULT_WIDGETS = DEFAULT_WIDGETS;