/* Bee Smart Operations Portal - vNext canon prototype: all in-scope screens. Composed from the proven ops-portal kit primitives (OpsKit.jsx, preserved verbatim) over our REAL fleet (OurData.jsx). Each screen carries a ReviewNote stating what is DS-faithful, what is a deliberate delta, and what renders `-`. Forks locked in s353: labeled-cluster Network, real MapLibre map kept, superset Devices columns, per-sensor detail route, MiniStat/KStat Growers, "Stale" label. */ const dash = v => (v==null||v==='') ? - : v; // styles (lifted from the kit so the prototype is pixel-faithful) const th={textAlign:'center',padding:'10px 24px',fontFamily:O.mono,fontSize:10,fontWeight:600,letterSpacing:'.06em',textTransform:'uppercase',color:O.fg3,whiteSpace:'nowrap'}; const td={padding:'11px 24px',verticalAlign:'middle',whiteSpace:'nowrap',textAlign:'center'}; const tdM={padding:'11px 24px',verticalAlign:'middle',whiteSpace:'nowrap',fontFamily:O.mono,fontSize:12,color:O.fg2,textAlign:'center'}; const chip={background:'#fff',borderWidth:1,borderStyle:'solid',borderColor:O.line,borderRadius:999,padding:'7px 13px',cursor:'pointer',fontFamily:O.sans,fontWeight:600,fontSize:12.5,color:O.fg2}; const chipOn={background:O.azure,borderColor:O.azure,color:'#fff'}; const primBtn={display:'inline-flex',alignItems:'center',gap:7,background:O.azure,color:'#fff',border:'none',borderRadius:9,padding:'9px 15px',cursor:'pointer',fontFamily:O.sans,fontWeight:700,fontSize:13}; const ghostBtn={display:'inline-flex',alignItems:'center',gap:7,background:'#fff',color:O.fg1,border:`1px solid ${O.lineS}`,borderRadius:9,padding:'9px 15px',cursor:'pointer',fontFamily:O.sans,fontWeight:600,fontSize:13}; const miniBtn={background:'#fff',border:`1px solid ${O.lineS}`,borderRadius:7,padding:'5px 11px',cursor:'pointer',fontFamily:O.sans,fontWeight:600,fontSize:12,color:O.fg2}; const linkBtn={background:'none',border:'none',cursor:'pointer',color:O.azure,fontFamily:O.sans,fontWeight:700,fontSize:12.5}; // ---- Review-note callout (NOT part of the DS; prototype chrome only) ---- function ReviewNote({ title, faithful=[], delta=[], absent=[], flag }){ if(PROD_PORTAL) return null; // prototype-only chrome; hidden when served as production const Row=({label,items,c})=> items.length?
{label} {items.join(' · ')}
: null; return
Review note - {title}
{flag &&
Decide {flag}
}
; } function Meta({ label, v, c }){ return
{label}
{v}
; } function KStat({ n, l, c, onClick }){ return
{n}
{l}
; } function MiniStat({ n, l, c }){ return
{n}
{l}
; } // ================= OVERVIEW ================= // Faithful reproduction of the deployed production Overview (app/page.tsx): same KPI // set incl "Readings (24h)", FleetHealth donuts + offline/stale/low-battery/sites // strip, active-alert feed, caption footer. s366: 24h sparklines added to the KPI tiles. // s366: Overview KPI sparkline. Ports lib/ui/Spark.tsx + chart-math.sparkPath into the // canon (browser-Babel, no bundler). 24h trend from the listSeries 30m buckets. A flat // baseline renders for <2 points or a constant series (placeholder where no history). function sparkPath(data,w,h){ if(!data||data.length<2){const y=h/2;return {line:`M0 ${y} L${w} ${y}`,area:`M0 ${y} L${w} ${y} L${w} ${h} L0 ${h} Z`};} const max=Math.max(...data),min=Math.min(...data); if(max===min){const y=h/2;return {line:`M0 ${y} L${w} ${y}`,area:`M0 ${y} L${w} ${y} L${w} ${h} L0 ${h} Z`};} const rng=max-min; const pts=data.map((v,i)=>[(i/(data.length-1))*w, h-((v-min)/rng)*(h-4)-2]); const line=pts.map((p,i)=>(i?'L':'M')+p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' '); return {line, area:`${line} L${w} ${h} L0 ${h} Z`}; } function Spark({ data, color }){ const w=120,h=30; const c=color||O.azure; const flat=!data||data.length<2; const {line,area}=sparkPath(data,w,h); const gid='sk'+React.useId().replace(/:/g,''); return ; } function KpiTile({ label, value, unit, accent, big, onClick, spark }){ return
{label}
{value}{unit&&{unit}}
{spark!==undefined&&}
; } function Overview({ onNav, onDevices }){ const rows=DEVICES; const on=rows.filter(d=>d.status==='online').length; const tier=k=>{const a=rows.filter(d=>d.kind===k);return {on:a.filter(d=>d.status==='online').length,total:a.length};}; const g=tier('gateway'),r=tier('router'),s=tier('sensor'); const uptime=(on/rows.length*100).toFixed(1); const offline=rows.filter(d=>d.status==='offline').length; const stale=rows.filter(d=>d.status==='stale').length; const low=rows.filter(d=>d.battery!=null&&d.battery>0&&d.battery<=25).length; const gateways=rows.filter(d=>d.kind==='gateway').length; // s366: 24h KPI trends from the listSeries 30m buckets (window.SERIES = [{messages,devices}]). // Real for Readings (volume), Devices Online (distinct devices/bucket), Uptime (devices/total); // Active Alerts has no stored history -> no sparkline (s368 review), just the number. const series=(typeof SERIES!=='undefined'&&Array.isArray(SERIES))?SERIES:[]; const tReadings=series.map(p=>p.messages); const tOnline=series.map(p=>p.devices); const tUptime=series.map(p=>rows.length?Math.min(100,p.devices/rows.length*100):0); return
{!PROD_PORTAL &&
Production Overview layout - KPI + Fleet-Health cards are now clickable + floating (deep-link to Devices / Alerts / Growers) per user direction.
} {/* KPI row - exact production labels + order */}
=99.9?O.ok:O.crit} spark={tUptime} onClick={()=>onDevices('all')}/> onDevices('online')}/> onNav('topology')}/> 0?O.crit:O.ok} big onClick={()=>onNav('alerts')}/>
{[['Gateways',g,'router','gateway'],['Routers',r,'share-2','router'],['Sensors',s,'radio','sensor']].map(([lbl,d,ic,kind])=>(
onDevices({kind})} className="clk" title={`View ${lbl} in Devices`} style={{border:`1px solid ${O.line}`,borderRadius:11,padding:'14px 14px 12px',display:'flex',flexDirection:'column',alignItems:'center',gap:8,cursor:'pointer'}}> 0.5?O.warn:O.crit} size={88}/>
{lbl}
{d.on} / {d.total}
))}
{[[SITES.length,'sites',O.ok,()=>onNav('growers')],[offline,'offline',offline>0?O.crit:O.leaf,()=>onDevices({stat:'offline'})],[stale,'stale',stale>0?O.warn:O.leaf,()=>onDevices({stat:'stale'})],[low,'low battery',low>0?O.warn:O.leaf,()=>onDevices({stat:'low'})]].map(([n,l,c,go],i)=>(
{n}
{l}
))}
onNav('alerts')} className="clk" style={{display:'flex',flexDirection:'column',cursor:'pointer'}}> View all}/> {activeAlerts.length===0 ?
All clear
No active alerts. Mesh fresh; batteries healthy.
:
{activeAlerts.slice(0,4).map(a=>{ const s=SEV[a.severity]||SEV.info; return
{a.title}
{siteName(a.site)}{a.node?' · '+a.node:''}
{s.label}
;})}
}
; } // ================= DEVICES (kit columns superset + reading columns) ================= function Devices({ onDevice, initial }){ const init=typeof initial==='string'?{stat:initial}:(initial||{}); const [kind,setKind]=React.useState(init.kind||'all'); const [stat,setStat]=React.useState(init.stat||'all'); const [site,setSite]=React.useState(init.site||'all'); const [addDev,setAddDev]=React.useState(false); const [removing,setRemoving]=React.useState(null); const matchStat=d=> stat==='all'?true : stat==='low'?(d.battery!=null&&d.battery>0&&d.battery<=25) : d.status===stat; const filtered=DEVICES.filter(d=>(kind==='all'||d.kind===kind)&&(site==='all'||d.site===site)&&matchStat(d)); const tabs=[['all','All',DEVICES.length],['gateway','Gateways',DEVICES.filter(d=>d.kind==='gateway').length],['router','Routers',DEVICES.filter(d=>d.kind==='router').length],['sensor','Sensors',DEVICES.filter(d=>d.kind==='sensor').length]]; return
}/> Router > Sensors) for clear level separation (s356)","Signal column HIDDEN from the table (s356) -- the RSSI data field is RETAINED in the data layer; re-add the column when PM-160 lands real data","Last seen column moved next to Status (left of Battery) (s359)"]} absent={["Signal/RSSI not collected yet (PM-160)"]}/>
{tabs.map(([k,l,n])=>)} {[['all','All'],['online','Online'],['stale','Stale'],['offline','Offline'],['low','Low battery']].map(([k,l])=>)} {site!=='all'&&}
{['Device','Type','Site','Status','Last seen','Battery','Temp','Humidity','Baro','',''].map((h,i)=>)} {filtered.map(d=>{ const rd=d.reading||{}; const lvl=d.kind==='gateway'?0:d.kind==='router'?1:2; // device hierarchy indent: Gateway > Router > Sensors return onDevice(d)} style={{cursor:'pointer',borderTop:`1px solid ${O.line}`}} className="ops-row"> ; })}
{h}
{d.name}
{d.node}{d.uuid?' · '+d.uuid.slice(0,8):''}
{KIND[d.kind].label} {siteName(d.site)} 120?O.crit:O.fg1}}>{fmtAgo(d.lastSeen)} {d.battery!=null?:dash(null)} {dash(rd.tempF!=null?rd.tempF+'°':null)} {dash(rd.humidity!=null?rd.humidity+'%':null)} {dash(rd.pressure!=null?rd.pressure:null)}
{addDev&&setAddDev(false)}/>} {removing&&setRemoving(null)}/>} ; } // ================= DEVICE DETAIL (per-sensor route + mesh pill) ================= function DeviceDetail({ device, onDevice }){ const d=device; const parent=DEVICES.find(x=>x.id===d.parent); const gw=d.kind==='gateway'?d:DEVICES.find(x=>x.kind==='gateway'&&x.site===d.site); const children=DEVICES.filter(x=>x.parent===d.id); const meshSensors = d.kind==='gateway' ? DEVICES.filter(x=>x.kind==='sensor'&&x.site===d.site) : []; // s359: gateway Connected Devices shows downstream sensors as small status squares (DS pattern OpsScreens2) const rd=d.reading||(d.kind==='gateway'?(d.weather?{tempF:d.weather.tempF,pressure:d.weather.pressure,humidity:d.weather.humidity}:{tempF:null,pressure:null,humidity:null}):null); // s359: a gateway (incl. offline standalone) always shows the weather tiles -- '-' when no data const metrics=[]; if(rd){ metrics.push(['Temperature',rd.tempF!=null?rd.tempF:null,'°F','thermometer']); metrics.push(['Pressure',rd.pressure!=null?rd.pressure:null,'inHg','gauge']); metrics.push(['Humidity',rd.humidity!=null?rd.humidity:null,'%','droplets']); if(d.kind==='sensor'){ metrics.push(['Noise',rd.noise!=null?rd.noise:null,'dB','volume-2']); metrics.push(['Air quality',null,'AQI','wind']); } // s358: Noise is live (sensors only); Air quality stays '-' (gas frozen, PM-180) } const stale = d.lastSeen!=null&&d.lastSeen>120; const leaf = d.kind!=='gateway'; // sensor + router share the title-row tile treatment (s357) // s357: on the sensor + router drill-downs, Battery + Last seen become their own tiles on the // title row alongside any readings (readings to the right); they leave the meta grid below. const tileSty={border:`1px solid ${O.line}`,borderRadius:10,padding:'7px 12px',minWidth:76,textAlign:'center'}; const tileLbl={display:'flex',alignItems:'center',justifyContent:'center',gap:5,color:O.fg3,fontFamily:O.mono,fontSize:9,fontWeight:600,letterSpacing:'.04em',textTransform:'uppercase'}; const tileVal={fontFamily:O.disp,fontWeight:800,fontSize:17,marginTop:3,display:'flex',alignItems:'center',justifyContent:'center',minHeight:21}; const isGw = d.kind==='gateway'; // s359: gateway gets the tile-row treatment too const tileCount=2+metrics.length; // leaf title row: Battery + Last seen + readings (gateway uses a fixed 3-col grid below) // s368 PM-184 review (owner direction): reusable tile renderers so the gateway and leaf // rows compose in their own order. Gateway row reordered -- weather (Temp/Humidity/Pressure) // on top, Battery/Last seen/Elevation below; the GPS tile is dropped (Lat/Long lives on the // map); the Last seen value renders lighter than the metric values. leaf/isGw exclusive. const wxTile=(lbl,val,unit,ic)=>(
{lbl}
{val==null?'-':val}{val!=null&& {unit}}
); const batteryTile=(
Battery
{d.battery!=null?:dash(null)}
); // s368 review (owner direction): the Last seen value renders lighter (600) than the // metric values (800) on every device detail -- gateway and leaf alike. const lastSeenTile=()=>(
Last seenLast
{fmtAgo(d.lastSeen)}
); const elevationTile=(
Elevation
{d.elevationFt!=null?d.elevationFt.toLocaleString()+' ft':'-'}
); const tileEls = isGw ? <> {wxTile('Temp', rd?rd.tempF:null, '°F', 'thermometer')} {wxTile('Humidity', rd?rd.humidity:null, '%', 'droplets')} {wxTile('Pressure', rd?rd.pressure:null, 'inHg', 'gauge')} {batteryTile} {lastSeenTile()} {elevationTile} : <> {batteryTile} {lastSeenTile()} {metrics.map(([lbl,val,unit,ic])=>wxTile(lbl,val,unit,ic))} ; const headerBody = <>

{d.name}

{d.kind==='gateway' ? siteName(d.site) : <>{d.uuid||d.node} · {KIND[d.kind].label} · {siteName(d.site)}}
{leaf&&
{tileEls}
}
{/* s359: gateway tiles render in a 3-column grid BELOW the title (Last seen/Battery/GPS on top, weather below) -- wider cards, and the gateway card is narrow (shares the row with the map) so a single row beside the title overlapped. No second meta-grid section: faint divider + Firmware + Signal were removed per owner direction. */} {isGw&&
{tileEls}
} ; return
Last seen > Mesh hops > Firmware > Signal; Link dropped (s356)","sensor + router drill-downs share the same header look: Battery + Last seen are their own tiles on the title row beside any readings (readings to the right), dropped from the meta grid below; router gains the Mesh Path card; leaf header is now a SINGLE row of equal-width, evenly-spaced tiles (second meta-grid section removed -- Mesh hops/Firmware/Signal dropped from the leaf view) (s357)","gateway drill-down now shows a 3-column tile grid below the title: Last seen / Battery / GPS (stacked lat over long, 2 dp) on top, weather (Temp/Pressure/Humidity) below; its second meta-grid section + faint divider + Firmware + Signal removed; the meta line under the name is the location only (id + device-type dropped); the standalone retail gateway shows the same Last seen/Battery/weather tiles, rendered '-' while offline (s359)","gateway Connected Devices card adds the downstream sensors as small status squares (green online / red offline, DS cluster pattern) that drill into the sensor detail; the router card is unchanged (s359)"]} absent={["Signal (PM-160)","Firmware (PM-175)","sparkline 24h trend (real listChannelSeries wired in prod; single snapshot here)","Logs/Ping/Configure (no device-command backend)"]} flag={null}/> {d.kind==='gateway' ?
{/* s358: mesh path merged INTO the top gateway card. Standalone gateways (no mesh) drop the "Mesh Path" label and show just the pills (Gateway -> Cloud). */} {headerBody}
{!isStandalone(d.site)&&
Mesh Path
}
: {headerBody}} {/* s358: leaf devices (sensor + router) keep the standalone Mesh Path card; gateway now carries it inline above */} {(d.kind==='sensor'||d.kind==='router')&& } {children.length>0 ?
{/* s365: "Connected Devices" header + icon removed (user direction). The Router card is unchanged; the sensors now sit in their OWN card styled like the Router card, with the small status squares (green online / red offline) inside it. */}
{children.map(c=>)} {meshSensors.length>0&&
Sensors
{meshSensors.map(s=>)}
}
Wired to real deviceEvents in production. No events in this point-in-time snapshot.
:
Wired to real deviceEvents in production. No events in this point-in-time snapshot.
}
; } // ================= NETWORK (labeled-cluster hybrid) ================= function Topology({ onDevice, onGrower }){ return
grower"]} delta={["each site card now shows the deployment Mesh Path spine (Sensors > Router > Gateway > Cloud) - the same reusable component as the Growers cards (s356 user direction)","legend moved to the top, trimmed to the 3 node states + card sized to content"]} absent={[]}/>
{['online','offline','stale'].map(k=>{const s=STATUS[k];return {s.label};})}
{SITES.map(st=>{ const gw=DEVICES.find(d=>d.kind==='gateway'&&d.site===st.id); const sensors=DEVICES.filter(d=>d.kind==='sensor'&&d.site===st.id); const son=sensors.filter(s=>s.status==='online').length; const dark=gw&&gw.status!=='online'; return onGrower&&onGrower(st.id)} className="clk" style={{borderColor:O.line,display:'flex',flexDirection:'column',minHeight:176,cursor:'pointer'}}>
{st.name}
{son}/{sensors.length} Sensors online
All data
{son}/{sensors.length}
Sensors
; })}
; } // ================= ALERTS (derived; honest empty) ================= function Alerts({ onDevice, extra=[] }){ const [f,setF]=React.useState('active'); const [sev,setSev]=React.useState(null); // s358: severity filter driven by the clickable top summary cards const allAlerts=[...extra,...ALERTS]; // extra = client-side synthetic alerts (e.g. Portal data delayed) const allActive=[...extra,...activeAlerts]; const list=allAlerts.filter(a=>(f==='all'||a.status===f)&&(!sev||a.severity===sev)); return
Alert rules}/> Critical, <=25% battery -> Warning (s356)","severity summary cards are clickable filters (toggle); alert cards open the alerting device's drill-down (s358)"]} absent={["Info severity / Ack / Resolve lifecycle (PM-161)"]}/>
{[['critical','Critical',O.crit,'alert-octagon'],['warning','Warning',O.warn,'alert-triangle'],['info','Info',O.info,'info']].map(([k,l,c,ic])=>( setSev(sev===k?null:k)} className="clk" title={sev===k?'Clear '+l+' filter':'Filter to '+l} style={{borderLeft:`3px solid ${c}`,cursor:'pointer',...(sev===k?{outline:`2px solid ${c}`,outlineOffset:'-1px'}:{})}}>
{allActive.filter(a=>a.severity===k).length}
{l} · active
))}
{[['active','Active'],['all','All']].map(([k,l])=>)}
{list.length===0 ?
All clear
No active alerts. The G1 mesh is fresh and batteries are healthy.
:
{list.map(a=>{ const s=SEV[a.severity]||SEV.info; const dev=DEVICES.find(d=>d.node===a.node&&d.site===a.site); // s358: alert -> alerting device drill-down return onDevice(dev):undefined} className={dev&&onDevice?'clk':undefined} style={{borderLeft:`3px solid ${s.c}`,cursor:dev&&onDevice?'pointer':'default'}}>
{a.title}{s.label}
{a.detail}
{siteName(a.site)}{a.node?' · '+a.node:''}
;})}
}
; } // ================= INTEGRATIONS ================= function Integrations(){ return
non-clickable (s366)","per-component health: UpCloud/InfluxDB/Grafana shown Operational (verified via the live data path); Soracom/nRF Cloud await live probes (s358)","the sidebar platform pill derives from these (green unless degraded/down)"]} absent={["live per-component health for Soracom + nRF Cloud (PM-159)"]}/>
{INTEGRATIONS.map(it=>( // s366: whole card opens the console in a new tab (the per-card "Open console" button // was removed). Cards without a console URL (InfluxDB - self-hosted) stay non-clickable. window.open(it.url,'_blank','noopener,noreferrer'):undefined} className={it.url?'clk':undefined} style={{display:'flex',flexDirection:'column',gap:12,cursor:it.url?'pointer':'default'}}>
{it.name}
{it.role}
{it.url&&}
{(()=>{ const up=it.status==='up'; const c=up?O.leaf:it.status==='degraded'?O.goldDeep:it.status==='down'?O.crit:O.faint; const lbl=up?'Operational':it.status==='degraded'?'Degraded':it.status==='down'?'Down':'Health -'; return
{lbl}{it.status?'verified':'PM-159'}
; })()}
))}
These consoles open in their native dashboards. Deep metric/log federation into this portal (per-card live health) is planned - see PM-159.
; } // ---- Mesh Path (reusable site spine: Sensors -> Router -> Gateway -> Cloud) ---- // A labeled, status-coloured chain rendered at the deployment level. No icons; it // ALWAYS lays out as a single row (.mesh-path CSS: flex + container-query font/pill // sizing) so it fits any container - narrow grower card or wide detail card alike. // Standalone gateways (no router/sensors) collapse to Gateway -> Cloud. function MeshPath({ siteId, h, icons }){ const gw=DEVICES.find(d=>d.kind==='gateway'&&d.site===siteId); const router=DEVICES.find(d=>d.kind==='router'&&d.site===siteId); const sensors=DEVICES.filter(d=>d.kind==='sensor'&&d.site===siteId); const son=sensors.filter(s=>s.status==='online').length; const hops=[]; if(sensors.length){ const s=son===sensors.length?STATUS.online:STATUS.offline; // any sensor not online -> the aggregate card goes red (no amber middle state) hops.push(['Sensors',son+'/'+sensors.length+' online',s.c,s.bg,'radio']); } if(router){ const s=STATUS[router.status]; hops.push(['Router',router.node,s.c,s.bg,'share-2']); } if(gw){ const s=STATUS[gw.status]; hops.push(['Gateway',gw.node,s.c,s.bg,'router']); } const cloudOk=gw&&gw.status==='online'; hops.push(['Cloud','LTE',cloudOk?O.leaf:O.fg3,cloudOk?O.okBg:O.sunken,'cloud']); return
{hops.map((n,i,arr)=>
{icons&&} {n[0]} {n[1]}
{i{'>'}}
)}
; } // ================= GROWERS (list) ================= function Growers({ onGrower, onSensors, onSaved }){ const [add,setAdd]=React.useState(false); const [addDev,setAddDev]=React.useState(false); // after Add Grower -> immediately open Add Device const [removing,setRemoving]=React.useState(null); const [editing,setEditing]=React.useState(null); return
setAdd(true)} className="clk" style={primBtn}>Add Grower}/>
{SITES.map(st=>{ const ds=DEVICES.filter(d=>d.site===st.id); const sensors=ds.filter(d=>d.kind==='sensor'); const son=sensors.filter(s=>s.status==='online').length; const gw=ds.find(d=>d.kind==='gateway'); const dark=gw&&gw.status!=='online'; return
onGrower(st.id)} style={{textAlign:'left',background:O.raised,border:`1px solid ${O.line}`,borderRadius:14,padding:16}}>
{st.name}{st.kind}
{dash(st.crop)} · {dash(st.region)} · {st.site}
{e.stopPropagation();onSensors&&onSensors(st.id);}} title="View sensors" className="clk" style={{flex:1,border:`1px solid ${O.line}`,borderRadius:9,padding:'8px 4px',textAlign:'center'}}>
{son}/{sensors.length}
Sensors
; })}
{add&&setAdd(false)} onAdded={()=>{ setAdd(false); setAddDev(true); }}/>} {addDev&&setAddDev(false)}/>} {removing&&setRemoving(null)}/>} {editing&&setEditing(null)} onSaved={onSaved}/>}
; } // ================= GROWER DETAIL ================= function GrowerDetail({ siteId, onDevice, onDevices, onAlerts, onSaved }){ const st=SITES.find(s=>s.id===siteId)||SITES[0]; const ds=DEVICES.filter(d=>d.site===siteId); const gw=ds.find(d=>d.kind==='gateway'); const router=ds.find(d=>d.kind==='router'); const sensors=ds.filter(d=>d.kind==='sensor'); const son=sensors.filter(s=>s.status==='online').length; const devOn=ds.filter(d=>d.status==='online').length; const devOff=ds.filter(d=>d.status!=='online').length; const devLow=ds.filter(d=>d.battery!=null&&d.battery>0&&d.battery<=25).length; const siteAlerts=activeAlerts.filter(a=>a.site===siteId).length; const dark=gw&&gw.status!=='online'; const [edit,setEdit]=React.useState(false); return
device detail; Open Device pill removed + card height tightened (s357)","Signal column removed from the Sensor Readings table (s357)"]} absent={["acres/crop/region","Air-qual per-sensor (gas frozen, PM-180)"]}/>

{st.name}

0?'online':sensors.length===0?'offline':'stale'}/>
{dash(st.crop)} · {dash(st.region)} · {st.site} · acres {dash(st.acres)}
0?O.leaf:O.fg3} onClick={()=>onDevices&&onDevices({site:siteId,stat:'online'})}/> 0?O.crit:O.leaf} onClick={()=>onDevices&&onDevices({site:siteId,stat:'offline'})}/> 0?O.warn:O.leaf} onClick={()=>onDevices&&onDevices({site:siteId,stat:'low'})}/> 0?O.crit:O.leaf} onClick={()=>onAlerts&&onAlerts()}/>
{(gw||router)&&
{[gw,router].filter(Boolean).map(d=>( onDevice(d)} className="clk" style={{display:'flex',flexDirection:'column',cursor:'pointer'}}>
{d.name}
{d.node}
{d.battery!=null&&}
{fmtAgo(d.lastSeen)}
{d.kind==='gateway'&&d.weather&&
} {d.kind!=='gateway'&&
}
))}
} {sensors.length>0&&
Sensor Readings · {sensors.length} Sensors
{['Sensor','Status','Battery','Last seen','Temp','Baro','Humidity','Noise','Air qual.'].map((h,i)=>)} {sensors.map(s=>{ const r=s.reading||{}; return onDevice(s)} style={{cursor:'pointer',borderTop:`1px solid ${O.line}`}} className="ops-row"> ; })}
{h}
{s.name}
{s.node}
{s.battery!=null?:dash(null)} {fmtAgo(s.lastSeen)} {dash(r.tempF!=null?r.tempF+'°':null)} {dash(r.pressure)} {dash(r.humidity!=null?r.humidity+'%':null)} {dash(r.noise!=null?r.noise:null)} {dash(null)}
} {edit && setEdit(false)} onSaved={onSaved}/>}
; } // s366 PM-192: real live MapLibre map (OSM raster tiles, no API key -- mirrors the real Next app's // components/GatewayMap.tsx, Port-18). Replaces the s353-s365 SVG stand-in. Centers on the gateway's // GPS with a single pin; keeps the Lat/Long + ELE overlay pills. No fix -> neutral placeholder. function RealMapPanel({ gps, label, elevationFt }){ const ll = gps ? gps.split(',').map(s=>parseFloat(s)) : null; const hasFix = !!(ll && ll.length>=2 && !isNaN(ll[0]) && !isNaN(ll[1])); const gpsLabel = hasFix ? `${ll[0].toFixed(2)}, ${ll[1].toFixed(2)}` : (gps||'no GPS fix'); const ref = React.useRef(null); const mapRef = React.useRef(null); React.useEffect(()=>{ if(!hasFix || !ref.current || mapRef.current || !window.maplibregl) return; const lat=ll[0], lon=ll[1]; const map = new window.maplibregl.Map({ container: ref.current, style: { version:8, sources:{ osm:{ type:'raster', tiles:['https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'], tileSize:256, attribution:'(c) OpenStreetMap contributors' } }, layers:[{ id:'osm', type:'raster', source:'osm' }] }, center:[lon,lat], zoom:13, scrollZoom:false, attributionControl:{ compact:true }, }); map.addControl(new window.maplibregl.NavigationControl({ showCompass:false }), 'bottom-right'); const el=document.createElement('div'); el.style.cssText=`width:18px;height:18px;border-radius:50%;background:${O.azure};border:3px solid #fff;box-shadow:0 2px 7px rgba(31,111,224,0.55)`; new window.maplibregl.Marker({ element:el }).setLngLat([lon,lat]).addTo(map); mapRef.current=map; return ()=>{ map.remove(); mapRef.current=null; }; }, [gps]); if(!hasFix) return
No GPS fix reported
; return
{gpsLabel}
Lat / Long
{/* s365: elevation pill (PM-192 ELE). s368 (PM-184 review, owner direction): moved to the map top-left. */}
{elevationFt!=null?elevationFt+' ft':'-'}
ELE
; } // ================= Modals (Add / Remove) ================= const inp={width:'100%',boxSizing:'border-box',height:34,padding:'6px 10px',borderRadius:8,border:`1px solid ${O.lineS}`,background:'#fff',fontFamily:O.sans,fontSize:13.5,color:O.fg1,outline:'none'}; const lab={fontFamily:O.mono,fontSize:10,fontWeight:600,letterSpacing:'.06em',textTransform:'uppercase',color:O.fg3,marginBottom:3,display:'block'}; function Modal({ title, onClose, children, footer, headerActions }){ return
e.stopPropagation()} style={{background:O.raised,borderRadius:16,boxShadow:'0 20px 60px rgba(14,28,51,0.4)',width:520,maxWidth:'100%',maxHeight:'90vh',overflowY:'auto'}}> {/* sticky: the modal box scrolls as one piece, so the header (and any headerActions, e.g. Edit-Grower Save) stays visible while fields scroll. */}
{title}
{headerActions}
{children}
{footer &&
{footer}
}
; } function AddDeviceModal({ onClose }){ const [kind,setKind]=React.useState('sensor'); const [siteId,setSiteId]=React.useState((SITES[0]||{}).id||''); // s379 (owner direction): the attach list is the selected grower's REAL fleet -- its // gateway + any routers under it -- not generic placeholders. Router attaches ONLY to // the gateway; a sensor attaches to a router OR directly to the gateway (the edge // router is a range-extending relay, not a required hop -- a routerless sensor/gateway // mesh is a valid topology). const siteDevs=DEVICES.filter(d=>d.site===siteId); const gw=siteDevs.find(d=>d.kind==='gateway'); const routers=siteDevs.filter(d=>d.kind==='router'); const attach=kind==='sensor' ? [...routers.map(r=>r.name), ...(gw?[gw.name+' (direct)']:[])] : (gw?[gw.name]:[]); return }>
{[['gateway','Gateway'],['router','Router'],['sensor','Sensor']].map(([k,l])=>)}
{kind!=='gateway' ?
{attach.length ? : }
:
} {/* s368 review (owner direction): no Node address field -- the mesh assigns the node address at provisioning; the operator never sets it. Only a human Label is entered. */}
{!PROD_PORTAL &&
UI shown for the Page-Preview Gate. The reversible ownership write-back rides the auth work (PM-184; admin / AUTH_MODE=real).
}
; } function AddGrowerModal({ onClose, onAdded }){ return }>
{!PROD_PORTAL &&
UI shown for the Page-Preview Gate. Writes to the reversible ownership store (PM-183; admin / AUTH_MODE=real).
}
; } // Admin token kept client-side only (operator-entered; never baked). PM-184. function getAdminToken(){ try{ return localStorage.getItem('bs_admin_token')||''; }catch{ return ''; } } function setAdminToken(t){ try{ if(t) localStorage.setItem('bs_admin_token',t); }catch{} } function clearAdminToken(){ try{ localStorage.removeItem('bs_admin_token'); }catch{} } // Edit-Grower: pre-filled from the grower's current record (the widened CanonSite). // PATCHes the editable whitelist (name, business, owner, email, phone, address, region, // crop, acres, notes, kind, + the location name), then onSaved() soft-refreshes. // Target id = site.growerId (the customer_id). PM-184. function EditGrowerModal({ site, onClose, onSaved }){ const TEXT=['name','businessName','ownerName','email','phone','address','region','crop','acres','notes']; const init={ kind: site.kind||'commercial', locationName: site.site||'' }; TEXT.forEach(k=>{ const v=site[k]; init[k]= v==null?'':String(v); }); const [f,setF]=React.useState(init); const [busy,setBusy]=React.useState(false); const [err,setErr]=React.useState(null); const set=(k)=>(e)=>setF(s=>({...s,[k]:e.target.value})); const save=async()=>{ setErr(null); let token=getAdminToken(); if(!token){ token=window.prompt('Admin token (required to edit)')||''; if(!token) return; setAdminToken(token); } const fields={ kind: f.kind, locationName: (f.locationName||'').trim() }; TEXT.forEach(k=>{ const v=(f[k]||'').trim(); if(k==='acres') fields[k]= v===''?null:Number(v); else fields[k]= v===''?null:v; }); if(!fields.name){ setErr('Grower Name is required.'); return; } setBusy(true); try{ await window.patchGrower(site.growerId, fields, token); setBusy(false); onClose(); if(onSaved) onSaved(); }catch(e){ setBusy(false); if(e&&e.code===401){ clearAdminToken(); setErr('Admin token invalid -- re-enter on next save.'); } else setErr('Save failed ('+((e&&e.message)||'error')+'). The record was not changed.'); } }; const field=(k,label,opts={})=>(
{opts.area ?