// ───────────────────────────────────────────────────────────────── // TreeGuard — Asset Overview list // Reuses useSort/SortHeader from dashboard.jsx (loaded earlier). // ───────────────────────────────────────────────────────────────── // Real spans from the export — one row per conductor span across all 3 HV lines. // `alarms` / `alerts` are tree counts within the span's analysis envelope (from the ETL). const SPANS = (window.CLUSTERS || []).map(c => ({ id: c.id, n: c.spanSeq, line: c.line, designation: c.designation, type: 'OHL Span', alarms: c.alarms, alerts: c.alerts, createdBy: '—', updated: 'current export', })); const statusOf = (s) => s.alarms > 0 ? 'alarm' : s.alerts > 0 ? 'alert' : 'stable'; const statusTier = { alarm: 0, alert: 1, stable: 2 }; const ASSETS_KPIS = (() => { const total = SPANS.length; const withAlarms = SPANS.filter(s => statusOf(s) === 'alarm').length; const withAlerts = SPANS.filter(s => statusOf(s) === 'alert').length; const noWarnings = SPANS.filter(s => statusOf(s) === 'stable').length; return { total, withAlarms, withAlerts, noWarnings, // Deltas vs the previous survey update — no prior snapshot in this export. totalDelta: null, alarmDelta: null, alertDelta: null, noWarningDelta: null, }; })(); /* ────────────────────────────────────────────────────────────── */ const StatusFilterTabs = ({ value, onChange, counts }) => { const tabs = [ { id: 'all', label: 'All', n: counts.all }, { id: 'alarm', label: 'Alarm', n: counts.alarm }, { id: 'alert', label: 'Alert', n: counts.alert }, { id: 'stable', label: 'Stable', n: counts.stable }, ]; return (
{tabs.map(t => ( ))}
); }; const DeltaBadge = ({ delta, unit }) => { if (delta == null) return No prior data; const dir = delta > 0 ? 'up' : delta < 0 ? 'down' : null; return ( {dir && } {delta > 0 ? '+' : ''}{delta} {unit} vs Previous Update ); }; const AssetsKpiRow = ({ kpis, onJumpAlerts }) => (
Total Assets
{kpis.total}spans
Assets With Alarms
{kpis.withAlarms}spans
Assets With Alerts
{kpis.withAlerts}spans
Assets Without Warnings
{kpis.noWarnings}spans
); /* ────────────────────────────────────────────────────────────── */ const AssetRow = ({ s, idx }) => { const st = statusOf(s); return (
{idx + 1} {st === 'alarm' && TOP} {st === 'alert' && TOP}
HV Line {s.line} {s.designation} {s.type} {s.alerts > 0 ? {s.alerts} Alert{s.alerts === 1 ? '' : 's'} : Stable} {s.alarms > 0 ? {s.alarms} Alarm{s.alarms === 1 ? '' : 's'} : } {s.createdBy} {s.updated} ); }; const AssetTable = () => { const [q, setQ] = React.useState(''); const [filter, setFilter] = React.useState('all'); const updatedKey = (s) => { // Most-recent first when sorted desc: hours-ago < days-ago. const m = s.updated.match(/(\d+)\s+(hour|day)/); if (!m) return 99999; return parseInt(m[1], 10) * (m[2] === 'hour' ? 1 : 24); }; const filtered = React.useMemo(() => { const qq = q.trim().toLowerCase(); return SPANS.filter(s => { if (filter !== 'all' && statusOf(s) !== filter) return false; if (!qq) return true; return ( s.designation.toLowerCase().includes(qq) || s.createdBy.toLowerCase().includes(qq) || ('span ' + s.n).toLowerCase().includes(qq) ); }); }, [q, filter]); const { rows, sort, toggle } = useSort( filtered, { no: (s) => statusTier[statusOf(s)] * 1000 - s.alarms * 10 - s.alerts, infra: () => 0, asset: (s) => s.n, type: (s) => s.type, alerts: (s) => s.alerts, alarms: (s) => s.alarms, author: (s) => s.createdBy, updated: (s) => updatedKey(s), }, { col: 'no', dir: 'asc' } ); const counts = React.useMemo(() => ({ all: SPANS.length, alarm: SPANS.filter(s => statusOf(s) === 'alarm').length, alert: SPANS.filter(s => statusOf(s) === 'alert').length, stable: SPANS.filter(s => statusOf(s) === 'stable').length, }), []); return ( <>
setQ(e.target.value)} placeholder="Search by span, designation, or owner…"/>
{rows.length} of {SPANS.length}
NOInfrastructureAsset DesignationAsset TypeAlertsAlarmsCreated ByLast Update {rows.map((s, i) => )} {rows.length === 0 && ( )}
No spans match the current filter.
); }; Object.assign(window, { SPANS, ASSETS_KPIS, statusOf, AssetsKpiRow, AssetTable, });