2026-04-22 08:42:52 +00:00
<!DOCTYPE html>
< html lang = "en" > < head > < meta charset = "UTF-8" > < meta name = "viewport" content = "width=device-width, initial-scale=1.0" > < title > Force Monitor — Reports< / 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,1760px);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;color:var(--muted2);text-transform:uppercase}.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-box{height:420px}.list{margin:10px 0 0;padding-left:18px}.list li{margin:8px 0;color:var(--muted)}.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)}.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}@media(max-width:1080px){.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" href = "/process-capability" > Process capability< / a > < a class = "btn primary" 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" > Management & engineering report< / div > < h1 style = "margin:8px 0 0;font-size:42px;line-height:1.04" > Shift, Day & Week Reports< / h1 > < div class = "sub" > A report-friendly view for engineering and boss departments with health score, availability estimate, event counts, peak summaries, trend deltas, and a bucket chart for the selected period.< / div > < / div > < div class = "spacer" > < / div > < div class = "mono sub" id = "report-range" > Window: --< / div > < / div > < / div >
< div class = "glass page" > < div class = "toolbar" > < button class = "btn primary window-btn" data-window = "8h" > Shift (8h)< / button > < button class = "btn window-btn" data-window = "24h" > Day< / button > < button class = "btn window-btn" data-window = "7d" > Week< / button > < button class = "btn window-btn" data-window = "30d" > Month< / button > < input id = "custom-window" class = "input" style = "width:160px" placeholder = "e.g. 48h or 14d" > < button id = "apply-window" class = "btn" type = "button" > Apply< / button > < div class = "spacer" > < / div > < button id = "refresh-btn" class = "btn" type = "button" > Refresh< / button > < button id = "download-json" class = "btn" type = "button" > Download JSON< / button > < / div > < / div >
< div class = "grid cards" > < div class = "glass card" > < div class = "kicker" > Health score< / div > < div id = "health" class = "value mono" > --< / div > < div id = "health-sub" class = "sub" > Availability and event pressure< / div > < / div > < div class = "glass card" > < div class = "kicker" > Avg / peak total< / div > < div id = "avg-peak" class = "value mono" > --< / div > < div id = "avg-peak-sub" class = "sub" > Total force summary< / div > < / div > < div class = "glass card" > < div class = "kicker" > Avg / peak imbalance< / div > < div id = "avg-imb" class = "value mono" > --< / div > < div id = "avg-imb-sub" class = "sub" > Centering summary< / div > < / div > < div class = "glass card" > < div class = "kicker" > Events< / div > < div id = "events" class = "value mono" > --< / div > < div id = "events-sub" class = "sub" > Warnings, criticals, PLC disconnects< / div > < / div > < / div >
< div class = "glass page" > < div class = "row" > < h2 style = "margin:0;font-size:28px" > Executive summary< / h2 > < div class = "spacer" > < / div > < span id = "summary-pill" class = "pill good" > loading< / span > < / div > < div id = "executive-summary" class = "sub" style = "font-size:18px;margin-top:14px" > Loading report…< / div > < ul id = "findings" class = "list" > < li > Loading findings…< / li > < / ul > < / div >
< div class = "glass page" > < div class = "row" > < h2 style = "margin:0;font-size:28px" > Bucket trend< / h2 > < div class = "spacer" > < / div > < span class = "pill" > selected period< / span > < / div > < div class = "sub" > Each bucket summarizes average total force, maximum force, and event density inside the selected report window.< / div > < div class = "chart-box" > < canvas id = "reportChart" > < / canvas > < / div > < / div >
< div class = "glass page" > < div class = "row" > < h2 style = "margin:0;font-size:28px" > Top peaks in report window< / h2 > < div class = "spacer" > < / div > < span class = "pill critical" > top load moments< / span > < / div > < div class = "table-wrap" style = "margin-top:12px" > < table > < thead > < tr > < th > Time< / th > < th > Total %< / th > < th > Total kN< / th > < th > Imb %< / th > < th > L %< / th > < th > R %< / th > < / tr > < / thead > < tbody id = "top-peaks-body" > < tr > < td colspan = "6" > No data< / td > < / tr > < / tbody > < / table > < / div > < / div >
< / div >
< script src = "/static/app-common.js" > < / script >
< script >
let currentWindow='8h', reportCache=null, cfg={ui:{unit_force:'kN',unit_percent:'%'}}, chart=null;
2026-04-22 14:16:27 +00:00
function fmt(n,d=1){return Number(n||0).toFixed(d)} function setTheme(theme){if(chart){const light=theme==='light'; chart.options.scales.x.ticks.color=light?'#334155':'#a1a1aa'; chart.options.scales.y.ticks.color=light?'#334155':'#a1a1aa'; chart.options.scales.y1.ticks.color=light?'#334155':'#a1a1aa'; chart.options.scales.x.grid.color=light?'rgba(15,23,42,.10)':'rgba(255,255,255,.06)'; chart.options.scales.y.grid.color=light?'rgba(15,23,42,.10)':'rgba(255,255,255,.06)'; chart.options.scales.y1.grid.color='transparent'; chart.update('none');}}
2026-04-22 08:42:52 +00:00
async function loadCfg(){try{cfg=await AppUI.fetchJson('/api/config/public',{timeoutMs:8000});}catch(e){console.warn('Config load error:',e)}}
function makeChart(){chart=new Chart(document.getElementById('reportChart'),{type:'bar',data:{labels:[],datasets:[{type:'bar',label:'Avg total %',backgroundColor:'rgba(34,211,238,.55)',borderColor:'#22d3ee',data:[]},{type:'line',label:'Max total %',borderColor:'#f87171',backgroundColor:'rgba(248,113,113,.12)',tension:.18,borderWidth:3,data:[],yAxisID:'y'},{type:'line',label:'Warning+Critical events',borderColor:'#facc15',backgroundColor:'rgba(250,204,21,.10)',tension:.18,borderWidth:3,data:[],yAxisID:'y1'}]},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'}},y1:{position:'right',grid:{color:'transparent'},ticks:{color:'#a1a1aa'}}}}}); setTheme(document.body.dataset.theme||'dark');}
async function refresh(){const r=await fetch('/api/reports/summary?window='+encodeURIComponent(currentWindow),{cache:'no-store'}); if(!r.ok) throw new Error('HTTP '+r.status); const d=await r.json(); reportCache=d; document.getElementById('report-range').textContent='Window: '+d.window+' • '+d.from+' → '+d.to; document.getElementById('health').textContent=d.health_score+'/100'; document.getElementById('health').className='value mono '+(d.health_score< 70 ? ' critical ' :d . health_score < 85 ? ' warning ' : ' good ' ) ; document . getElementById ( ' health-sub ' ) . textContent = 'Availability ' + fmt ( d . availability_pct , 1 ) + ' % • stability ' + String ( d . stability | | ' -- ' ) . toUpperCase ( ) ; document . getElementById ( ' avg-peak ' ) . textContent = fmt(d.average_total_pct,1)+' / ' + fmt ( d . peak_total_pct , 1 ) + ( cfg . ui . unit_percent | | ' % ' ) ; document . getElementById ( ' avg-peak-sub ' ) . textContent = 'Avg ' + fmt ( d . average_total_kn , 1 ) + ' ' + ( cfg . ui . unit_force | | ' kN ' ) + ' • peak ' + fmt ( d . peak_total_kn , 1 ) + ' ' + ( cfg . ui . unit_force | | ' kN ' ) ; document . getElementById ( ' avg-imb ' ) . textContent = fmt(d.average_imbalance_pct,1)+' / ' + fmt ( d . peak_imbalance_pct , 1 ) + ( cfg . ui . unit_percent | | ' % ' ) ; document . getElementById ( ' avg-imb-sub ' ) . textContent = 'Δ force ' + ( ( d . force_delta_pct > =0)?'+':'')+fmt(d.force_delta_pct,1)+' • Δ imb '+((d.imbalance_delta_pct>=0)?'+':'')+fmt(d.imbalance_delta_pct,1); document.getElementById('events').textContent=d.warning_events+' / '+d.critical_events; document.getElementById('events-sub').textContent='Warnings / criticals • PLC disconnects '+d.plc_disconnects; document.getElementById('executive-summary').textContent=d.executive_summary||'--'; document.getElementById('summary-pill').textContent=String(d.stability||'stable').toUpperCase(); document.getElementById('summary-pill').className='pill '+(d.stability==='unstable'?'critical':d.stability==='caution'?'warning':'good'); const findings=document.getElementById('findings'); findings.innerHTML=''; (d.findings||[]).forEach(item=>{const li=document.createElement('li'); li.textContent=item; findings.appendChild(li);}); if(!chart) makeChart(); chart.data.labels=(d.buckets||[]).map(b=>b.label); chart.data.datasets[0].data=(d.buckets||[]).map(b=>b.avg_total_pct); chart.data.datasets[1].data=(d.buckets||[]).map(b=>b.max_total_pct); chart.data.datasets[2].data=(d.buckets||[]).map(b=>(b.warning_events||0)+(b.critical_events||0)); chart.update('none'); const rows=(d.top_peaks||[]).map(p=>'< tr > < td > '+p.time+'< / td > < td > '+fmt(p.total_percent,1)+'< / td > < td > '+fmt(p.total_kn,1)+'< / td > < td > '+fmt(p.imbalance_percent,1)+'< / td > < td > '+fmt(p.left_percent,1)+'< / td > < td > '+fmt(p.right_percent,1)+'< / td > < / tr > ').join(''); document.getElementById('top-peaks-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(chart) setTheme(document.body.dataset.theme || 'dark'); } }); 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))); document.getElementById('download-json').addEventListener('click',()=>{ if(!reportCache) return; const blob=new Blob([JSON.stringify(reportCache,null,2)],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='force-monitor-report-'+currentWindow+'.json'; a.click(); URL.revokeObjectURL(a.href);}); loadCfg().then(()=>refresh().catch(console.warn));
2026-04-22 14:16:27 +00:00
< / script > < / body > < / html >