Instrumentation health
Status, freshness, 7-day completeness and a simple failure-risk read for every instrument. The same checks run by cron and email the administrator when something degrades; the laboratory admits its own gaps the moment they open.
';
$insts=db()->query("SELECT * FROM instruments ORDER BY zone, name")->fetchAll();
echo '| Instrument | Zone | Status | Last reading | Freshness | 7-day completeness | Risk |
';
foreach($insts as $i){
$q=db()->prepare("SELECT MAX(r.ts) m FROM readings r JOIN streams s ON s.id=r.stream_id WHERE s.instrument_id=? AND r.is_placeholder=(SELECT IF(MIN(mode)='live',0,1) FROM streams WHERE instrument_id=?)");
$q->execute([$i['id'],$i['id']]); $last=$q->fetch()['m'];
$age=$last?(time()-strtotime($last))/60:1e9;
$fresh=$last?($ageprepare("SELECT COUNT(*) c FROM readings r JOIN streams s ON s.id=r.stream_id WHERE s.instrument_id=? AND r.ts>DATE_SUB(NOW(),INTERVAL 7 DAY)");
$q2->execute([$i['id']]); $got=$q2->fetch()['c'];
$comp=min(100,round(100*$got/ max(1,$exp*max(1,db()->query("SELECT COUNT(*) c FROM streams WHERE instrument_id=".$i['id'])->fetch()['c']))));
$q3=db()->prepare("SELECT COUNT(*) c FROM events WHERE instrument_id=? AND class IN ('fault','gap') AND ts>DATE_SUB(NOW(),INTERVAL 14 DAY)");
$q3->execute([$i['id']]); $recent_faults=$q3->fetch()['c'];
$risk = $fresh==='silent'||$fresh==='none' ? 'HIGH' : ($comp<70||$recent_faults>2 ? 'ELEVATED' : ($comp<90||$recent_faults>0 ? 'WATCH' : 'LOW'));
$rc=['LOW'=>C_GOOD,'WATCH'=>C_WARN,'ELEVATED'=>C_SERIOUS,'HIGH'=>C_CRIT][$risk];
echo '| '.h($i['name']).' '.h($i['product']).' | '.h($i['zone']).' | ';
echo ''.inst_status_badge($i['status']).' | '.h($last?:'never').' | '.h($fresh).' | ';
echo ''.$comp.'% | ● '.$risk.' |
';
}
echo '
';
echo 'Risk heuristic v0: silence and completeness plus recent fault frequency. As the record grows this page gains trend-based prediction (rising gap frequency, battery and signal telemetry where instruments expose them) so failures are anticipated, not discovered.
';
page_footer();