This commit is contained in:
Mateusz Gruszczyński
2026-03-06 10:06:14 +01:00
parent e8f6c4c609
commit 7b8a81dc3b
28 changed files with 1270 additions and 312 deletions

View File

@@ -1,8 +1,4 @@
(function(){
function ringPush(arr, item, max){
arr.push(item);
if(arr.length > max) arr.splice(0, arr.length-max);
}
function ensureLibs(){
return new Promise((resolve)=>{
const tick=()=>{ if(window.io && window.Chart) return resolve(); setTimeout(tick,50); };
@@ -11,40 +7,42 @@
}
const charts=new Map();
const seriesData=new Map(); // widgetId -> {name: [{x,y}]}
const seriesUnit=new Map(); // widgetId -> {name: unit}
const maxPoints=900;
const seriesData=new Map();
const seriesUnit=new Map();
const selectedRange=new Map();
const maxWindowMs=10*60*60*1000;
const rangeMap={
'1m': 1*60*1000,
'10m': 10*60*1000,
'1h': 60*60*1000,
'3h': 3*60*60*1000,
'10h': 10*60*60*1000,
};
function isNum(v){ return typeof v==='number' && Number.isFinite(v); }
function scale(v, unit){
if(!isNum(v)) return {v, u:''};
const u=(unit||'').toLowerCase();
if(u==='%' || u==='pct' || u==='percent') return {v, u:'%'};
if(u==='bps' || u==='bit/s' || u==='bits/s'){
const steps=['bps','Kbps','Mbps','Gbps','Tbps'];
let i=0, x=v;
while(Math.abs(x)>=1000 && i<steps.length-1){ x/=1000; i++; }
return {v:x, u:steps[i]};
}
if(u==='b/s' || u==='bytes/s' || u==='byte/s'){
const steps=['B/s','KiB/s','MiB/s','GiB/s','TiB/s'];
let i=0, x=v;
while(Math.abs(x)>=1024 && i<steps.length-1){ x/=1024; i++; }
return {v:x, u:steps[i]};
}
if(u==='b' || u==='bytes'){
const steps=['B','KiB','MiB','GiB','TiB'];
let i=0, x=v;
while(Math.abs(x)>=1024 && i<steps.length-1){ x/=1024; i++; }
return {v:x, u:steps[i]};
}
// fallback
return {v, u:unit||''};
}
@@ -61,6 +59,7 @@
for(let i=0;i<str.length;i++){ h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
return h>>>0;
}
function colorFor(name){
const hue = (hash32(String(name)) % 360);
return {
@@ -69,6 +68,17 @@
};
}
function getRangeMs(widgetId){
return rangeMap[selectedRange.get(widgetId) || '1h'] || rangeMap['1h'];
}
function pruneStore(store, latestTs){
const minTs=latestTs-maxWindowMs;
for(const name of Object.keys(store)){
store[name]=store[name].filter(p=>p.x.getTime()>=minTs);
}
}
function upsertChart(widgetId){
const el=document.getElementById('chart-'+widgetId);
if(!el) return null;
@@ -90,7 +100,6 @@
beginAtZero:false,
ticks:{
callback:(tick)=>{
// jeśli w widżecie jest 1 unit -> formatuj, jeśli różne -> surowo
const uMap = seriesUnit.get(widgetId) || {};
const units = Object.values(uMap).filter(Boolean);
const uniq = [...new Set(units)];
@@ -120,6 +129,7 @@
charts.set(widgetId, chart);
seriesData.set(widgetId, {});
seriesUnit.set(widgetId, {});
if(!selectedRange.has(widgetId)) selectedRange.set(widgetId, '1h');
return chart;
}
@@ -147,31 +157,19 @@
tbody.innerHTML=(msg.rows||[]).map(r=>'<tr>'+cols.map(c=>'<td>'+escapeHtml(r[c] ?? '')+'</td>').join('')+'</tr>').join('');
}
function handleTimeseries(msg){
const chart=upsertChart(msg.widget_id);
function renderChart(widgetId, latestTs){
const chart=upsertChart(widgetId);
if(!chart) return;
const store=seriesData.get(widgetId) || {};
const rangeMs=getRangeMs(widgetId);
const minTs=latestTs-rangeMs;
const t=new Date(msg.ts);
const store=seriesData.get(msg.widget_id) || {};
const uMap=seriesUnit.get(msg.widget_id) || {};
for(const p of (msg.points||[])){
if(p.value===null || p.value===undefined) continue;
const s=p.series || 'value';
if(!store[s]) store[s]=[];
ringPush(store[s], {x:t, y:Number(p.value)}, maxPoints);
if(p.unit) uMap[s]=String(p.unit);
}
seriesData.set(msg.widget_id, store);
seriesUnit.set(msg.widget_id, uMap);
const names = Object.keys(store);
chart.data.datasets = names.map(name=>{
chart.data.datasets = Object.keys(store).map(name=>{
const c=colorFor(name);
const data=(store[name]||[]).filter(p=>p.x.getTime()>=minTs);
return {
label:name,
data:store[name],
data,
borderWidth:2,
pointRadius:0,
tension:0.15,
@@ -181,13 +179,58 @@
};
});
chart.options.scales.x.min=minTs;
chart.options.scales.x.max=latestTs;
chart.update('none');
setMeta(msg.widget_id, 'Updated: '+t.toLocaleTimeString());
}
function handleTimeseries(msg){
const t=new Date(msg.ts);
const widgetId=msg.widget_id;
upsertChart(widgetId);
const store=seriesData.get(widgetId) || {};
const uMap=seriesUnit.get(widgetId) || {};
for(const p of (msg.points||[])){
if(p.value===null || p.value===undefined) continue;
const s=p.series || 'value';
if(!store[s]) store[s]=[];
store[s].push({x:t, y:Number(p.value)});
if(p.unit) uMap[s]=String(p.unit);
}
pruneStore(store, t.getTime());
seriesData.set(widgetId, store);
seriesUnit.set(widgetId, uMap);
renderChart(widgetId, t.getTime());
setMeta(widgetId, 'Updated: '+t.toLocaleTimeString()+' · Range: '+(selectedRange.get(widgetId)||'1h'));
}
function initRangeSelectors(){
document.querySelectorAll('[data-range-widget]').forEach((el)=>{
const widgetId=Number(el.getAttribute('data-range-widget'));
selectedRange.set(widgetId, el.value || '1h');
el.addEventListener('change', ()=>{
selectedRange.set(widgetId, el.value || '1h');
const store=seriesData.get(widgetId) || {};
let latestTs=Date.now();
for(const points of Object.values(store)){
if(points.length){
const ts=points[points.length-1].x.getTime();
if(ts>latestTs) latestTs=ts;
}
}
renderChart(widgetId, latestTs);
setMeta(widgetId, 'Updated: '+new Date(latestTs).toLocaleTimeString()+' · Range: '+(selectedRange.get(widgetId)||'1h'));
});
});
}
async function main(){
if(!window.MIKROMON || !window.MIKROMON.dashboardId) return;
await ensureLibs();
initRangeSelectors();
const socket=io({transports:['polling'], upgrade:false});
socket.on('connect', ()=>{
@@ -201,4 +244,4 @@
}
document.addEventListener('DOMContentLoaded', main);
})();
})();