bug fix
This commit is contained in:
parent
ea9bdddef4
commit
8c0b353c90
43
main.go
43
main.go
|
|
@ -792,12 +792,13 @@ type mqttManager struct {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cfg Config
|
cfg Config
|
||||||
cfgMu sync.RWMutex
|
cfgMu sync.RWMutex
|
||||||
state AppState
|
state AppState
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
sampleCh chan Sample
|
sampleCh chan Sample
|
||||||
alarmCh chan AlarmEvent
|
alarmCh chan AlarmEvent
|
||||||
|
indexTmpl *template.Template
|
||||||
|
|
||||||
alarmTracker AlarmTracker
|
alarmTracker AlarmTracker
|
||||||
uiRevision uint64 = 1
|
uiRevision uint64 = 1
|
||||||
|
|
@ -2830,11 +2831,12 @@ func queryReportSummary(ctx context.Context, window time.Duration, label string)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ReportSummaryResponse{}, err
|
return ReportSummaryResponse{}, err
|
||||||
}
|
}
|
||||||
|
defer alarmRows.Close()
|
||||||
|
|
||||||
for alarmRows.Next() {
|
for alarmRows.Next() {
|
||||||
var tsUnix int64
|
var tsUnix int64
|
||||||
var severity, source, code string
|
var severity, source, code string
|
||||||
if err := alarmRows.Scan(&tsUnix, &severity, &source, &code); err != nil {
|
if err := alarmRows.Scan(&tsUnix, &severity, &source, &code); err != nil {
|
||||||
alarmRows.Close()
|
|
||||||
return ReportSummaryResponse{}, err
|
return ReportSummaryResponse{}, err
|
||||||
}
|
}
|
||||||
labelKey := reportBucketLabel(time.Unix(0, tsUnix), window)
|
labelKey := reportBucketLabel(time.Unix(0, tsUnix), window)
|
||||||
|
|
@ -2848,7 +2850,9 @@ func queryReportSummary(ctx context.Context, window time.Duration, label string)
|
||||||
plcDiscByBucket[labelKey]++
|
plcDiscByBucket[labelKey]++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
alarmRows.Close()
|
if err := alarmRows.Err(); err != nil {
|
||||||
|
return ReportSummaryResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
buckets := make([]ReportBucket, 0, len(order))
|
buckets := make([]ReportBucket, 0, len(order))
|
||||||
for _, key := range order {
|
for _, key := range order {
|
||||||
|
|
@ -3300,9 +3304,8 @@ async function activate(){
|
||||||
}
|
}
|
||||||
|
|
||||||
// License OK — serve the full dashboard template from the embedded static files
|
// License OK — serve the full dashboard template from the embedded static files
|
||||||
tmpl, err := template.ParseFS(embeddedStaticFiles, "static/index.html")
|
if indexTmpl == nil {
|
||||||
if err != nil {
|
log.Printf("dashboard template not initialized")
|
||||||
log.Printf("template parse error: %v", err)
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -3349,7 +3352,7 @@ async function activate(){
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
if err := indexTmpl.Execute(w, data); err != nil {
|
||||||
log.Printf("template execute error: %v", err)
|
log.Printf("template execute error: %v", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
@ -3622,6 +3625,11 @@ func main() {
|
||||||
log.Fatalf("invalid config: %v", err)
|
log.Fatalf("invalid config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
indexTmpl, err = template.ParseFS(embeddedStaticFiles, "static/index.html")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to parse embedded dashboard template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
dbPath := cfg.DB.Path
|
dbPath := cfg.DB.Path
|
||||||
if !filepath.IsAbs(dbPath) {
|
if !filepath.IsAbs(dbPath) {
|
||||||
dbPath = filepath.Join(wd, dbPath)
|
dbPath = filepath.Join(wd, dbPath)
|
||||||
|
|
@ -3742,11 +3750,12 @@ func main() {
|
||||||
mux.HandleFunc("/api/license/activate", apiLicenseActivate)
|
mux.HandleFunc("/api/license/activate", apiLicenseActivate)
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: cfg.Server.ListenAddr,
|
Addr: cfg.Server.ListenAddr,
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
ReadTimeout: 15 * time.Second,
|
ReadHeaderTimeout: 10 * time.Second,
|
||||||
WriteTimeout: 15 * time.Second,
|
ReadTimeout: 15 * time.Second,
|
||||||
IdleTimeout: 60 * time.Second,
|
WriteTimeout: 15 * time.Second,
|
||||||
|
IdleTimeout: 60 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Listening address: %s", cfg.Server.ListenAddr)
|
log.Printf("Listening address: %s", cfg.Server.ListenAddr)
|
||||||
|
|
|
||||||
|
|
@ -514,7 +514,7 @@
|
||||||
function wireEvents() {
|
function wireEvents() {
|
||||||
AppUI.initTheme({ onChange: (t) => { currentTheme = t; updateChartTheme(); } });
|
AppUI.initTheme({ onChange: (t) => { currentTheme = t; updateChartTheme(); } });
|
||||||
AppUI.initFullscreen({ buttonId:'fullscreen-toggle' });
|
AppUI.initFullscreen({ buttonId:'fullscreen-toggle' });
|
||||||
updateFullscreenButton();
|
AppUI.updateFullscreenButton('fullscreen-toggle');
|
||||||
qs('refresh-btn').addEventListener('click', refreshAll);
|
qs('refresh-btn').addEventListener('click', refreshAll);
|
||||||
qs('export-csv').addEventListener('click', exportCsv);
|
qs('export-csv').addEventListener('click', exportCsv);
|
||||||
qs('apply-window').addEventListener('click', () => {
|
qs('apply-window').addEventListener('click', () => {
|
||||||
|
|
@ -543,4 +543,4 @@
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -6,9 +6,12 @@
|
||||||
<title>Force Monitor — Kiosk</title>
|
<title>Force Monitor — Kiosk</title>
|
||||||
<style>
|
<style>
|
||||||
:root{--bg1:#030712;--bg2:#0f172a;--panel:rgba(255,255,255,.06);--border:rgba(255,255,255,.1);--text:#f8fafc;--muted:#94a3b8;--ok:#34d399;--warn:#facc15;--bad:#f87171;}
|
:root{--bg1:#030712;--bg2:#0f172a;--panel:rgba(255,255,255,.06);--border:rgba(255,255,255,.1);--text:#f8fafc;--muted:#94a3b8;--ok:#34d399;--warn:#facc15;--bad:#f87171;}
|
||||||
|
body[data-theme="light"]{--bg1:#eef4ff;--bg2:#f8fafc;--panel:rgba(255,255,255,.84);--border:rgba(15,23,42,.10);--text:#0f172a;--muted:#475569;--ok:#059669;--warn:#b45309;--bad:#dc2626;}
|
||||||
*{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 20% 10%, rgba(56,189,248,.14), transparent 20%),radial-gradient(circle at 80% 10%, rgba(168,85,247,.14), transparent 18%),linear-gradient(180deg,var(--bg1),var(--bg2));}
|
*{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 20% 10%, rgba(56,189,248,.14), transparent 20%),radial-gradient(circle at 80% 10%, rgba(168,85,247,.14), transparent 18%),linear-gradient(180deg,var(--bg1),var(--bg2));}
|
||||||
|
body[data-theme="light"]{background:radial-gradient(circle at 20% 10%, rgba(14,165,233,.10), transparent 20%),radial-gradient(circle at 80% 10%, rgba(168,85,247,.10), transparent 18%),linear-gradient(180deg,var(--bg1),var(--bg2));}
|
||||||
.wrap{width:min(96vw,1800px);margin:0 auto;padding:18px 22px 28px;} .row,.nav{display:flex;gap:12px;flex-wrap:wrap;align-items:center}.spacer{flex:1 1 auto}
|
.wrap{width:min(96vw,1800px);margin:0 auto;padding:18px 22px 28px;} .row,.nav{display:flex;gap:12px;flex-wrap:wrap;align-items:center}.spacer{flex:1 1 auto}
|
||||||
.btn{display:inline-flex;align-items:center;justify-content:center;min-height:42px;padding:10px 14px;border-radius:14px;border:1px solid var(--border);background:rgba(255,255,255,.05);color:var(--text);text-decoration:none;font-weight:600;cursor:pointer}
|
.btn{display:inline-flex;align-items:center;justify-content:center;min-height:42px;padding:10px 14px;border-radius:14px;border:1px solid var(--border);background:rgba(255,255,255,.05);color:var(--text);text-decoration:none;font-weight:600;cursor:pointer}
|
||||||
|
body[data-theme="light"] .btn{background:rgba(255,255,255,.88);}
|
||||||
.glass{background:var(--panel);border:1px solid var(--border);border-radius:24px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px)}
|
.glass{background:var(--panel);border:1px solid var(--border);border-radius:24px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px)}
|
||||||
.hero{padding:18px 24px;margin-bottom:18px}.status{font-size:64px;font-weight:900;line-height:1;margin-top:12px}.sub{color:var(--muted)} .mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}
|
.hero{padding:18px 24px;margin-bottom:18px}.status{font-size:64px;font-weight:900;line-height:1;margin-top:12px}.sub{color:var(--muted)} .mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}
|
||||||
.grid{display:grid;gap:16px}.cards{grid-template-columns:repeat(4,minmax(0,1fr));margin-bottom:18px}.card{padding:22px 24px}.label{font-size:12px;letter-spacing:.22em;text-transform:uppercase;color:var(--muted)} .value{font-size:54px;font-weight:900;line-height:1;margin-top:12px}
|
.grid{display:grid;gap:16px}.cards{grid-template-columns:repeat(4,minmax(0,1fr));margin-bottom:18px}.card{padding:22px 24px}.label{font-size:12px;letter-spacing:.22em;text-transform:uppercase;color:var(--muted)} .value{font-size:54px;font-weight:900;line-height:1;margin-top:12px}
|
||||||
|
|
@ -54,9 +57,6 @@
|
||||||
<script src="/static/app-common.js"></script>
|
<script src="/static/app-common.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let 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}};
|
let 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}};
|
||||||
function setTheme(theme){document.body.dataset.theme=theme; try{localStorage.setItem('force-monitor-theme',theme)}catch(e){} const btn=document.getElementById('theme-toggle'); if(btn) btn.textContent=theme==='light'?'Dark theme':'Light theme';}
|
|
||||||
function initTheme(){let t='dark'; try{t=localStorage.getItem('force-monitor-theme')||'dark'}catch(e){} setTheme(t==='light'?'light':'dark');}
|
|
||||||
function updateFullscreenButton(){const btn=document.getElementById('fullscreen-btn'); if(btn) btn.textContent=document.fullscreenElement?'Exit fullscreen':'Enter fullscreen';}
|
|
||||||
const fmt=(n,d=1)=>Number(n||0).toFixed(d); const cls=(z)=>z==='critical'?'critical':z==='warning'?'warning':'ok';
|
const fmt=(n,d=1)=>Number(n||0).toFixed(d); const cls=(z)=>z==='critical'?'critical':z==='warning'?'warning':'ok';
|
||||||
function zone(v,w,c){return v>=c?'critical':v>=w?'warning':'ok'}
|
function zone(v,w,c){return v>=c?'critical':v>=w?'warning':'ok'}
|
||||||
function setThemeTitle(){document.getElementById('title-kicker').textContent=cfg.ui.title+' • kiosk'}
|
function setThemeTitle(){document.getElementById('title-kicker').textContent=cfg.ui.title+' • kiosk'}
|
||||||
|
|
@ -100,4 +100,4 @@ AppUI.initTheme(); AppUI.initFullscreen({ buttonId:'fullscreen-btn' });
|
||||||
loadCfg().then(refreshAll); setInterval(refreshAll, 1500);
|
loadCfg().then(refreshAll); setInterval(refreshAll, 1500);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -15,8 +15,7 @@
|
||||||
<script src="/static/app-common.js"></script>
|
<script src="/static/app-common.js"></script>
|
||||||
<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;
|
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 setTheme(theme){document.body.dataset.theme=theme; try{localStorage.setItem('force-monitor-theme',theme)}catch(e){} document.getElementById('theme-toggle').textContent=theme==='light'?'Dark theme':'Light theme'; if(totalChart){updateChartTheme();}}
|
function fmt(n,d=2){return Number(n||0).toFixed(d)}
|
||||||
function initTheme(){let t='dark'; try{t=localStorage.getItem('force-monitor-theme')||'dark'}catch(e){} setTheme(t==='light'?'light':'dark')} function updateFullscreenButton(){const btn=document.getElementById('fullscreen-toggle'); if(btn) btn.textContent=document.fullscreenElement?'Exit fullscreen':'Enter fullscreen';} async function toggleFullscreen(){try{if(!document.fullscreenElement){await document.documentElement.requestFullscreen();}else{await document.exitFullscreen();}}catch(e){console.warn('Fullscreen error:',e)}finally{updateFullscreenButton();}}
|
|
||||||
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 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'}}}}});}
|
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 loadCfg(){try{cfg=await AppUI.fetchJson('/api/config/public',{timeoutMs:8000});}catch(e){console.warn('Config load error:',e)}}
|
||||||
|
|
@ -26,4 +25,4 @@ async function refresh(){const r=await fetch('/api/process-capability?window='+e
|
||||||
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>';}
|
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)}
|
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));
|
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>
|
</script></body></html>
|
||||||
|
|
@ -15,11 +15,11 @@
|
||||||
<script src="/static/app-common.js"></script>
|
<script src="/static/app-common.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let currentWindow='8h', reportCache=null, cfg={ui:{unit_force:'kN',unit_percent:'%'}}, chart=null;
|
let currentWindow='8h', reportCache=null, cfg={ui:{unit_force:'kN',unit_percent:'%'}}, chart=null;
|
||||||
function fmt(n,d=1){return Number(n||0).toFixed(d)} function setTheme(theme){document.body.dataset.theme=theme; try{localStorage.setItem('force-monitor-theme',theme)}catch(e){} document.getElementById('theme-toggle').textContent=theme==='light'?'Dark theme':'Light 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');}}
|
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');}}
|
||||||
function initTheme(){let t='dark'; try{t=localStorage.getItem('force-monitor-theme')||'dark'}catch(e){} setTheme(t==='light'?'light':'dark')} function updateFullscreenButton(){const btn=document.getElementById('fullscreen-toggle'); if(btn) btn.textContent=document.fullscreenElement?'Exit fullscreen':'Enter fullscreen';} async function toggleFullscreen(){try{if(!document.fullscreenElement){await document.documentElement.requestFullscreen();}else{await document.exitFullscreen();}}catch(e){console.warn('Fullscreen error:',e)}finally{updateFullscreenButton();}}
|
|
||||||
async function loadCfg(){try{cfg=await AppUI.fetchJson('/api/config/public',{timeoutMs:8000});}catch(e){console.warn('Config load error:',e)}}
|
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');}
|
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>';}
|
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)}
|
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));
|
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));
|
||||||
</script></body></html>
|
</script></body></html>
|
||||||
Loading…
Reference in a new issue