(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); }; tick(); }); } const charts=new Map(); const seriesData=new Map(); // widgetId -> {name: [{x,y}]} const seriesUnit=new Map(); // widgetId -> {name: unit} const maxPoints=900; 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=1024 && i=1024 && i=100 ? s.v.toFixed(0) : a>=10 ? s.v.toFixed(1) : s.v.toFixed(2); return n + (s.u ? ' '+s.u : ''); } function hash32(str){ let h=2166136261; for(let i=0;i>>0; } function colorFor(name){ const hue = (hash32(String(name)) % 360); return { border:`hsl(${hue} 70% 45%)`, fill:`hsl(${hue} 70% 45% / 0.20)` }; } function upsertChart(widgetId){ const el=document.getElementById('chart-'+widgetId); if(!el) return null; if(charts.has(widgetId)) return charts.get(widgetId); const chart=new Chart(el.getContext('2d'),{ type:'line', data:{datasets:[]}, options:{ animation:false, parsing:false, normalized:true, responsive:true, maintainAspectRatio:false, interaction:{mode:'nearest', intersect:false}, scales:{ x:{type:'time', time:{unit:'minute'}}, y:{ 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)]; const u = uniq.length===1 ? uniq[0] : ''; return u ? fmt(Number(tick), u) : tick; } } } }, plugins:{ legend:{display:true}, tooltip:{ callbacks:{ label:(ctx)=>{ const name=ctx.dataset.label||''; const y=ctx.parsed && isNum(ctx.parsed.y) ? ctx.parsed.y : null; const u=(seriesUnit.get(widgetId)||{})[name] || ''; return name+': '+(y===null?'':fmt(y,u)); } } } }, elements:{point:{radius:0}} } }); charts.set(widgetId, chart); seriesData.set(widgetId, {}); seriesUnit.set(widgetId, {}); return chart; } function setMeta(widgetId, txt){ const m=document.getElementById('meta-'+widgetId); if(m) m.textContent=txt; } function escapeHtml(s){ return String(s) .replaceAll('&','&') .replaceAll('<','<') .replaceAll('>','>') .replaceAll('"','"') .replaceAll("'",'''); } function handleTable(msg){ const tbl=document.querySelector("table[data-table-widget='"+msg.widget_id+"']"); if(!tbl) return; const thead=tbl.querySelector('thead'); const tbody=tbl.querySelector('tbody'); const cols=msg.columns||[]; thead.innerHTML=''+cols.map(c=>''+escapeHtml(c)+'').join('')+''; tbody.innerHTML=(msg.rows||[]).map(r=>''+cols.map(c=>''+escapeHtml(r[c] ?? '')+'').join('')+'').join(''); } function handleTimeseries(msg){ const chart=upsertChart(msg.widget_id); if(!chart) return; 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=>{ const c=colorFor(name); return { label:name, data:store[name], borderWidth:2, pointRadius:0, tension:0.15, fill:true, borderColor:c.border, backgroundColor:c.fill }; }); chart.update('none'); setMeta(msg.widget_id, 'Updated: '+t.toLocaleTimeString()); } async function main(){ if(!window.MIKROMON || !window.MIKROMON.dashboardId) return; await ensureLibs(); const socket=io({transports:['polling'], upgrade:false}); socket.on('connect', ()=>{ socket.emit('join_dashboard', {dashboard_id: window.MIKROMON.dashboardId, public_token: window.MIKROMON.publicToken}); }); socket.on('metric', (msg)=>{ if(!msg || !msg.widget_id) return; if(msg.type==='table') return handleTable(msg); return handleTimeseries(msg); }); } document.addEventListener('DOMContentLoaded', main); })();