/* Bee Smart Operations Portal - vNext canon prototype: routing + drill state + live-data boot gate. SITES/DEVICES are fetched by window.loadFleet() (OurData.jsx); the app shows loading -> ready / empty / error while that resolves. */ function CenterPane({children}){ return
{children}
; } function gateBtn(onClick,label){ return ; } function LoadingScreen(){ return
Loading fleet data…
Fetching live telemetry from the mesh.
; } function ErrorScreen({detail,onRetry}){ return
Couldn't load fleet data
{detail||'The live data service is unavailable.'}
{gateBtn(onRetry,'Retry')}
; } function EmptyScreen({onRefresh}){ return
No devices are reporting
The fleet returned no devices right now.
{gateBtn(onRefresh,'Refresh')}
; } function OpsApp(){ const [route,setRoute]=React.useState('overview'); const [device,setDevice]=React.useState(null); const [grower,setGrower]=React.useState(null); const [deviceFilter,setDeviceFilter]=React.useState(null); const [hist,setHist]=React.useState([]); // navigation history for the global Back button const [phase,setPhase]=React.useState('loading'); // 'loading' | 'ready' | 'error' const [loadErr,setLoadErr]=React.useState(null); const [,setTick]=React.useState(0); const [lastUpdated,setLastUpdated]=React.useState(null); // ms of the last successful load (PM-167 indicator) const [refreshing,setRefreshing]=React.useState(false); const inflight=React.useRef(false); // Single refresh path: the manual button, post-edit onSaved, and the 30s auto-poll // all route here. In-flight guard so a slow fetch can't pile up; stamps lastUpdated. const refresh=React.useCallback(()=>{ if(inflight.current) return; inflight.current=true; setRefreshing(true); window.loadFleet().then(()=>{ setLastUpdated(Date.now()); setTick(t=>t+1); }) .catch(e=>console.error('[canon] refresh failed:',e)) .finally(()=>{ inflight.current=false; setRefreshing(false); }); },[]); const boot=React.useCallback(()=>{ setPhase('loading'); setLoadErr(null); window.loadFleet().then(()=>{ setLastUpdated(Date.now()); setPhase('ready'); }).catch(e=>{ console.error('[canon] loadFleet failed:',e); setLoadErr(String(e&&e.message||e)); setPhase('error'); }); },[]); React.useEffect(()=>{ boot(); },[boot]); // PM-167 near-real-time status: auto-poll /api/fleet-snapshot every 30s, paused // while the tab is hidden (Page Visibility API), with an immediate refresh on re-focus. React.useEffect(()=>{ // 30s poll; also force a re-render each tick (even on a FAILED poll) so the // data-delayed state (>=10 min, below) can flip + raise its alert without a successful fetch. const id=setInterval(()=>{ if(!document.hidden){ refresh(); setTick(t=>t+1); } },30000); const onVis=()=>{ if(!document.hidden) refresh(); }; document.addEventListener('visibilitychange',onVis); return ()=>{ clearInterval(id); document.removeEventListener('visibilitychange',onVis); }; },[refresh]); const push=()=>setHist(h=>[...h,{route,device,grower,deviceFilter}]); const openDevice=d=>{ push(); setDevice(d); window.scrollTo(0,0); }; const openGrower=id=>{ const ds=DEVICES.filter(x=>x.site===id); if(ds.length===1){ openDevice(ds[0]); return; } /* single-device (standalone/consumer) grower -> straight to the device drill-down */ push(); setGrower(id); setDevice(null); window.scrollTo(0,0); }; const nav=r=>{ push(); setDevice(null); setGrower(null); setDeviceFilter(null); setRoute(r); window.scrollTo(0,0); }; const openDevices=(filter)=>{ push(); setDeviceFilter(filter||null); setDevice(null); setGrower(null); setRoute('devices'); window.scrollTo(0,0); }; const goBack=()=>{ if(hist.length){ const p=hist[hist.length-1]; setHist(hist.slice(0,-1)); setRoute(p.route); setDevice(p.device); setGrower(p.grower); setDeviceFilter(p.deviceFilter); } else { setDevice(null); setGrower(null); setDeviceFilter(null); setRoute('overview'); } window.scrollTo(0,0); }; if(phase==='loading') return ; if(phase==='error') return ; if(!DEVICES || DEVICES.length===0) return ; // Portal data-delayed: >=10 min since the last successful fetch -> the topbar LiveIndicator // goes green->red AND a client-side Critical alert is raised (bell badge + Alerts page). // Clears automatically when a poll succeeds (lastUpdated resets). Per-browser/session. const dataDelayed = lastUpdated!=null && (Date.now()-lastUpdated)>=600000; const extraAlerts = dataDelayed ? [{ id:'portal-data-delayed', severity:'critical', status:'active', title:'Portal data delayed', detail:'No fresh fleet data received for over 10 minutes -- the portal may have lost its connection to the backend; readings shown may be out of date.', site:null, node:null }] : []; let body; if(device) body=; else if(grower) body=nav('alerts')} onSaved={refresh}/>; else if(route==='growers') body=openDevices()} onSaved={refresh}/>; else if(route==='overview') body=; else if(route==='devices') body=; else if(route==='topology') body=; else if(route==='alerts') body=; else body=; // Back button: matches the Add Device (primBtn) vertical size - 8px pad + 1px border = 9px, 13px font; keeps its own white/outlined coloring. const backBtn=; const showBack = !(route==='overview' && !device && !grower); // no Back on the Overview landing page return { if(id&&id!=='all') openGrower(id); }} lastUpdated={lastUpdated} refreshing={refreshing} onRefresh={refresh}>{showBack?backBtn:null}{body}; } ReactDOM.createRoot(document.getElementById('root')).render();