28 lines
12 KiB
HTML
28 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Force Monitor — Process Capability</title>
|
||
<script src="/static/chart.umd.min.js"></script>
|
||
<style>
|
||
:root{--bg1:#050816;--bg2:#0b1224;--panel:rgba(255,255,255,.055);--border:rgba(255,255,255,.1);--text:#f4f4f5;--muted:#a1a1aa;--muted2:#71717a;--btnbg:rgba(255,255,255,.05);--good:#34d399;--warn:#facc15;--bad:#f87171;--info:#93c5fd;}body[data-theme="light"]{--bg1:#eef4ff;--bg2:#f8fafc;--panel:rgba(255,255,255,.84);--border:rgba(15,23,42,.1);--text:#0f172a;--muted:#475569;--muted2:#64748b;--btnbg:rgba(255,255,255,.88);--good:#059669;--warn:#b45309;--bad:#dc2626;--info:#1d4ed8;}*{box-sizing:border-box}body{margin:0;min-height:100vh;color:var(--text);font-family:'Segoe UI',system-ui,sans-serif;background:radial-gradient(circle at 10% 10%, rgba(34,211,238,.12), transparent 18%),radial-gradient(circle at 90% 10%, rgba(168,85,247,.14), transparent 18%),linear-gradient(180deg,var(--bg1),var(--bg2));}.wrap{width:min(95vw,1720px);margin:0 auto;padding:24px}.glass{background:var(--panel);border:1px solid var(--border);border-radius:24px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px)}.nav,.row,.toolbar{display:flex;gap:12px;flex-wrap:wrap;align-items:center}.spacer{flex:1 1 auto}.btn,.input{min-height:42px;border-radius:14px;border:1px solid var(--border);background:var(--btnbg);color:var(--text);padding:10px 14px;font:inherit}.btn{text-decoration:none;font-weight:600;display:inline-flex;align-items:center;justify-content:center;cursor:pointer}.btn.primary{background:rgba(14,165,233,.14);border-color:rgba(14,165,233,.35)}.page{padding:20px 22px;margin-bottom:18px}.kicker{font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--muted2)}.sub{color:var(--muted);margin-top:8px}.grid{display:grid;gap:16px}.cards{grid-template-columns:repeat(auto-fit,minmax(220px,1fr));margin-bottom:18px}.card{padding:18px 20px}.value{font-size:34px;font-weight:800;margin-top:8px}.mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}.chart-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.chart-box{height:360px}.table-wrap{overflow:auto}table{width:100%;border-collapse:collapse}th,td{padding:12px 10px;border-bottom:1px solid var(--border);text-align:left}th{font-size:12px;color:var(--muted2);text-transform:uppercase;letter-spacing:.16em}.pill{display:inline-flex;align-items:center;justify-content:center;min-width:88px;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:700;border:1px solid var(--border)}.good{color:var(--good)}.warning{color:var(--warn)}.critical{color:var(--bad)}@media(max-width:1080px){.chart-grid{grid-template-columns:1fr}.wrap{padding:16px}}</style></head>
|
||
<body data-theme="dark"><div class="wrap">
|
||
<div class="nav" style="margin-bottom:18px"><a class="btn" href="/">Dashboard</a><a class="btn" href="/history">History</a><a class="btn" href="/alarms">Alarms</a><a class="btn" href="/kiosk">Kiosk</a><a class="btn primary" href="/process-capability">Process capability</a><a class="btn" href="/reports">Reports</a><a class="btn" href="/license">License</a><div class="spacer"></div><button id="theme-toggle" class="btn" type="button">Light theme</button><button id="fullscreen-toggle" class="btn" type="button">Enter fullscreen</button></div>
|
||
<div class="glass page"><div class="row"><div><div class="kicker">Engineering capability</div><h1 style="margin:8px 0 0;font-size:42px;line-height:1.04">Process Capability & Distribution</h1><div class="sub">Histogram-based force and imbalance capability, one-sided CPU/CPK-style indicators against your configured thresholds, correlation between left and right columns, and suggested engineering action.</div></div><div class="spacer"></div><div class="mono sub" id="window-label">Window: --</div></div></div>
|
||
<div class="glass page"><div class="toolbar"><button class="btn primary window-btn" data-window="1h">1h</button><button class="btn window-btn" data-window="8h">8h</button><button class="btn window-btn" data-window="24h">24h</button><button class="btn window-btn" data-window="7d">7d</button><input id="custom-window" class="input" style="width:140px" placeholder="e.g. 3h or 2d"><button id="apply-window" class="btn" type="button">Apply</button><div class="spacer"></div><button id="refresh-btn" class="btn" type="button">Refresh</button></div></div>
|
||
<div class="grid cards"><div class="glass card"><div class="kicker">Total Cpk @ critical</div><div id="total-cpk-critical" class="value mono">--</div><div id="total-cpk-sub" class="sub">Capability versus critical load limit</div></div><div class="glass card"><div class="kicker">Imbalance Cpk @ critical</div><div id="imb-cpk-critical" class="value mono">--</div><div id="imb-cpk-sub" class="sub">Capability versus critical imbalance limit</div></div><div class="glass card"><div class="kicker">Left ↔ right correlation</div><div id="corr-value" class="value mono">--</div><div class="sub">Closer to 1.00 means both sides move together</div></div><div class="glass card"><div class="kicker">Suggested action</div><div id="action-pill" class="value" style="font-size:28px">--</div><div id="action-text" class="sub">Loading capability guidance…</div></div></div>
|
||
<div class="chart-grid"><div class="glass page"><div class="row"><h2 style="margin:0;font-size:28px">Total force distribution</h2><div class="spacer"></div><span class="pill good">histogram</span></div><div class="sub">Distribution of total peak force against configured warning and critical boundaries.</div><div class="chart-box"><canvas id="totalHist"></canvas></div></div><div class="glass page"><div class="row"><h2 style="margin:0;font-size:28px">Imbalance distribution</h2><div class="spacer"></div><span class="pill warning">histogram</span></div><div class="sub">Distribution of imbalance magnitude. A tight distribution below warning is usually what engineering wants.</div><div class="chart-box"><canvas id="imbHist"></canvas></div></div></div>
|
||
<div class="grid cards"><div class="glass card"><div class="kicker">Mean / σ total</div><div id="mean-total" class="value mono">--</div><div id="mean-total-sub" class="sub">P95 / P99 and warning occupancy</div></div><div class="glass card"><div class="kicker">Mean / σ imbalance</div><div id="mean-imb" class="value mono">--</div><div id="mean-imb-sub" class="sub">P95 and critical occupancy</div></div><div class="glass card"><div class="kicker">CPU warning / critical</div><div id="cpu-total" class="value mono">--</div><div class="sub">One-sided capability against upper limits</div></div><div class="glass card"><div class="kicker">Stability</div><div id="stability" class="value mono">--</div><div id="stability-sub" class="sub">Loading…</div></div></div>
|
||
<div class="glass page"><div class="row"><h2 style="margin:0;font-size:28px">Top outliers</h2><div class="spacer"></div><span class="pill critical">review points</span></div><div class="sub">Combined overload and imbalance stress points worth engineering review.</div><div class="table-wrap" style="margin-top:12px"><table><thead><tr><th>Time</th><th>Total %</th><th>Total kN</th><th>L %</th><th>R %</th><th>Imb %</th></tr></thead><tbody id="outlier-body"><tr><td colspan="6">No data</td></tr></tbody></table></div></div>
|
||
</div>
|
||
<script src="/static/app-common.js"></script>
|
||
<script>
|
||
let currentWindow='1h', cfg={ui:{title:'Force Monitor',unit_force:'kN',unit_percent:'%'},thresholds:{warning_percent:80,critical_percent:95,imbalance_warning_percent:10,imbalance_critical_percent:20}}, totalChart=null, imbChart=null;
|
||
function fmt(n,d=2){return Number(n||0).toFixed(d)}
|
||
function updateChartTheme(){const light=document.body.dataset.theme==='light'; [totalChart,imbChart].forEach(ch=>{ if(!ch) return; ch.options.scales.x.ticks.color=light?'#334155':'#a1a1aa'; ch.options.scales.y.ticks.color=light?'#334155':'#a1a1aa'; ch.options.scales.x.grid.color=light?'rgba(15,23,42,.10)':'rgba(255,255,255,.06)'; ch.options.scales.y.grid.color=light?'rgba(15,23,42,.10)':'rgba(255,255,255,.06)'; ch.update('none');});}
|
||
function makeHistChart(id,label,color){return new Chart(document.getElementById(id),{type:'bar',data:{labels:[],datasets:[{label:label,borderColor:color,backgroundColor:color+'55',data:[]}]},options:{responsive:true,maintainAspectRatio:false,animation:false,plugins:{legend:{labels:{color:'#f4f4f5'}}},scales:{x:{grid:{color:'rgba(255,255,255,.06)'},ticks:{color:'#a1a1aa'}},y:{grid:{color:'rgba(255,255,255,.06)'},ticks:{color:'#a1a1aa'}}}}});}
|
||
async function loadCfg(){try{cfg=await AppUI.fetchJson('/api/config/public',{timeoutMs:8000});}catch(e){console.warn('Config load error:',e)}}
|
||
async function refresh(){const r=await fetch('/api/process-capability?window='+encodeURIComponent(currentWindow),{cache:'no-store'}); if(!r.ok) throw new Error('HTTP '+r.status); const d=await r.json(); document.getElementById('window-label').textContent='Window: '+d.window+' • '+d.from+' → '+d.to; document.getElementById('total-cpk-critical').textContent=fmt(d.total_cpk_critical); document.getElementById('total-cpk-sub').textContent='Warning Cpk '+fmt(d.total_cpk_warning)+' • critical occupancy '+fmt(d.total_above_critical_pct,1)+'%'; document.getElementById('imb-cpk-critical').textContent=fmt(d.imbalance_cpk_critical); document.getElementById('imb-cpk-sub').textContent='Warning Cpk '+fmt(d.imbalance_cpk_warning)+' • critical occupancy '+fmt(d.imbalance_above_critical_pct,1)+'%'; document.getElementById('corr-value').textContent=fmt(d.left_right_correlation,3); document.getElementById('action-pill').textContent=(d.total_cpk_critical<1 || d.imbalance_cpk_critical<1)?'ACT':'OK'; document.getElementById('action-pill').className='value '+((d.total_cpk_critical<1 || d.imbalance_cpk_critical<1)?'critical':'good'); document.getElementById('action-text').textContent=d.suggested_action||'--'; document.getElementById('mean-total').textContent=fmt(d.total_mean_pct,1)+(cfg.ui.unit_percent||'%')+' / '+fmt(d.total_std_pct,2); document.getElementById('mean-total-sub').textContent='P95 '+fmt(d.total_p95_pct,1)+' • P99 '+fmt(d.total_p99_pct,1)+' • above warn '+fmt(d.total_above_warning_pct,1)+'%'; document.getElementById('mean-imb').textContent=fmt(d.imbalance_mean_pct,1)+(cfg.ui.unit_percent||'%')+' / '+fmt(d.imbalance_std_pct,2); document.getElementById('mean-imb-sub').textContent='P95 '+fmt(d.imbalance_p95_pct,1)+' • above warn '+fmt(d.imbalance_above_warning_pct,1)+'%'; document.getElementById('cpu-total').textContent=fmt(d.total_cpu_warning)+' / '+fmt(d.total_cpu_critical); document.getElementById('stability').textContent=String(d.stability||'--').toUpperCase(); document.getElementById('stability').className='value mono '+(d.stability==='unstable'?'critical':d.stability==='caution'?'warning':'good'); document.getElementById('stability-sub').textContent=d.stability_reason||'--';
|
||
if(!totalChart){ totalChart=makeHistChart('totalHist','Total %','#22d3ee'); imbChart=makeHistChart('imbHist','Imbalance %','#f59e0b'); updateChartTheme();}
|
||
totalChart.data.labels=(d.total_histogram||[]).map(b=>fmt(b.start,0)+'-'+fmt(b.end,0)); totalChart.data.datasets[0].data=(d.total_histogram||[]).map(b=>b.count); totalChart.update('none'); imbChart.data.labels=(d.imbalance_histogram||[]).map(b=>fmt(b.start,0)+'-'+fmt(b.end,0)); imbChart.data.datasets[0].data=(d.imbalance_histogram||[]).map(b=>b.count); imbChart.update('none');
|
||
const body=document.getElementById('outlier-body'); const rows=(d.top_outliers||[]).map(p=>'<tr><td>'+p.time+'</td><td>'+fmt(p.total_percent,1)+'</td><td>'+fmt(p.total_kn,1)+'</td><td>'+fmt(p.left_percent,1)+'</td><td>'+fmt(p.right_percent,1)+'</td><td>'+fmt(p.imbalance_percent,1)+'</td></tr>').join(''); body.innerHTML=rows||'<tr><td colspan="6">No data</td></tr>';}
|
||
function useWindow(v){currentWindow=v; document.querySelectorAll('.window-btn').forEach(btn=>btn.classList.toggle('primary',btn.dataset.window===v)); refresh().catch(console.warn)}
|
||
AppUI.initTheme({ onChange: ()=>{ if(totalChart||imbChart) updateChartTheme(); } }); AppUI.initFullscreen({ buttonId:'fullscreen-toggle' }); document.getElementById('refresh-btn').addEventListener('click',()=>refresh().catch(console.warn)); document.getElementById('apply-window').addEventListener('click',()=>{const v=document.getElementById('custom-window').value.trim(); if(v) useWindow(v)}); document.querySelectorAll('.window-btn').forEach(btn=>btn.addEventListener('click',()=>useWindow(btn.dataset.window))); loadCfg().then(()=>refresh().catch(console.warn));
|
||
</script></body></html> |