/* 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
;
}
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();