/* PM-198 Tool pages - one shared template at #/tools/: title + plain description, parameter controls (only what the tool needs), Run, full-width result area, last-run provenance line. Saved queries + export run through /api/tools/query (server binds params); health-probe hits /api/platform-health ?fresh=1; reflash is the PM-199 placeholder; kiosk relaunches the wall display. */ const FRIENDLY_COL = { _time:'Time', _value:'Value', gateway_id:'Gateway', node:'Device', sensor_type:'Channel', site_id:'Site' }; function ToolParamControls({ def, params, setParams }){ return
{def.params.map(p=>{ const v=params[p.name]||''; const set=val=>setParams(Object.assign({},params,{[p.name]:val})); const lab=
{p.label}{p.required?' *':''}
; const sel={padding:'8px 10px',borderRadius:9,border:`1px solid ${O.line}`,background:'#fff',fontFamily:O.sans,fontSize:12.5,color:O.fg1,outline:'none'}; if(p.type==='device') return
{lab}
; if(p.type==='range') return
{lab}
; return null; })}
; } function ToolResultTable({ result }){ if(!result) return null; if(result.rowCount===0) return
No rows returned.
; return
{result.columns.map(c=>)}{result.rows.map((r,i)=> {result.columns.map(c=>)} )}
{FRIENDLY_COL[c]||c}
{r[c]==null?'-':(typeof r[c]==='number'?(Math.round(r[c]*100)/100):String(r[c]))}
; } function ToolPage({ id }){ const [params,setParams]=React.useState({}); const [running,setRunning]=React.useState(false); const [result,setResult]=React.useState(null); const [err,setErr]=React.useState(null); const [ranAt,setRanAt]=React.useState(null); const [cat,setCat]=React.useState(window.TOOL_CATALOG); React.useEffect(()=>{ if(!cat) window.loadToolCatalog().then(setCat).catch(e=>setErr(String(e.message||e))); },[]); const def=(cat||[]).find(t=>t.id===id); if(!cat&&!err) return
Loading tool catalog...
; if(!def) return
No tool with id {String(id)} in the catalog.{err?' ('+err+')':''}
; // Effective params: defaults fill anything unset. const eff={}; def.params.forEach(p=>{ const v=params[p.name]||p.default||''; if(v) eff[p.name]=v; }); const missing=def.params.some(p=>p.required&&!eff[p.name]); const run=()=>{ setRunning(true); setErr(null); setRanAt(null); // stale provenance never outlives a failed run const go = id==='health-probe' ? window.loadPlatformHealth(true).then(p=>({ probe:p })) : window.runTool(id, eff); go.then(r=>{ setResult(r); setRanAt(new Date()); }) .catch(e=>{ setResult(null); setErr(String(e.message||e)); }) .finally(()=>setRunning(false)); }; return
{def.kind==='placeholder' ?
Coming soon
Web-triggered bench reflash arrives with the bench agent (PM-199). This page will carry every step, status, and completion message for the job.
: def.kind==='launcher' ? :
{def.csv&&{ if(missing) e.preventDefault(); }}>Export}
{err&&
Run failed: {err}
} {result&&result.probe&&
{result.probe.services.map(s=>
{s.label}
{s.ok?'UP':'DOWN'} · {s.latencyMs!=null?s.latencyMs+' ms':'-'} · {s.detail}
)}
} {result&&!result.probe&&} {ranAt&&
Last run {ranAt.toLocaleTimeString()}{result&&result.rowCount!=null?` · ${result.rowCount} rows`:''} · params {JSON.stringify(eff)}
}
}
; } /* PM-198 Overview teaser: full-width quadrant-preview card, bottom of Overview. THREE static mini-tiles showing data Overview does NOT already display (PLATFORM service dots / DATA QUALITY 24h continuity / READY TO ACT). The whole card is ONE click target into Mission Control; owner exception: the Ready-to-Act buttons are live links to their Tool pages. */ function MissionTeaser({ onMission, onTool }){ const [,setTick]=React.useState(0); React.useEffect(()=>{ let on=true; const load=()=>{ Promise.all([window.loadPlatformHealth().catch(()=>{}),window.loadToolCatalog().catch(()=>{})]).then(()=>{ if(on) setTick(t=>t+1); }); }; load(); const id=setInterval(load,60000); return ()=>{ on=false; clearInterval(id); }; },[]); const plat=window.PLATFORM_HEALTH, q=window.QUALITY24H; const acts=(window.TOOL_CATALOG||[]).filter(t=>t.kind==='saved-query'||t.kind==='utility').slice(0,4); const tile={border:`1px solid ${O.line}`,borderRadius:10,padding:'8px 12px',flex:1,minWidth:0}; const tl=l=>
{l}
; return
{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); onMission(); } }} title="Open Mission Control" style={{marginTop:10,cursor:'pointer'}}>
{tl('Data quality')}
{q?q.availabilityPct+'%':'-'}
24h availability
0?O.warn:O.fg3,marginTop:3}}>{q?q.gapBuckets+' gap bucket'+(q.gapBuckets===1?'':'s'):'-'}
{tl('Platform')}
{(plat?plat.services:[{id:'influxdb',label:'InfluxDB'},{id:'grafana',label:'Grafana'},{id:'soracom',label:'Soracom'}]).map(s=> {s.label}{plat&&s.latencyMs!=null? {s.latencyMs}ms:null} )}
{tl('Ready to act')}
{acts.length?acts.map(t=> ):-}
; }