mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 05:13:56 +08:00
🌴 Sync 2026-04-03 08:49:56
This commit is contained in:
1 parent
c361065f64
commit
687ee81cf2
27 files changed
+1571
-1687
No files matched your search
@@ -0,0 +1,81 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require uci';
|
||||
'require rpc';
|
||||
|
||||
var callMptcpCheckTrace = rpc.declare({
|
||||
object: 'luci.mptcp',
|
||||
method: 'mptcp_check_trace',
|
||||
params: ['iface'],
|
||||
expect: { output: '' }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
uci.load('network'),
|
||||
uci.load('openmptcprouter')
|
||||
]);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var ifaces = [];
|
||||
uci.sections('network', 'interface', function(s) {
|
||||
var name = s['.name'];
|
||||
if (name === 'loopback') return;
|
||||
var multipath = s.multipath ||
|
||||
uci.get('openmptcprouter', name, 'multipath') || 'off';
|
||||
if (multipath !== 'off')
|
||||
ifaces.push(name);
|
||||
});
|
||||
|
||||
var select = E('select', { 'class': 'cbi-input-select', 'id': 'mptcp-check-iface' },
|
||||
ifaces.map(function(n) { return E('option', { 'value': n }, [ n ]); })
|
||||
);
|
||||
|
||||
var pre = E('pre', { 'style': 'white-space:pre-wrap' });
|
||||
|
||||
var btn = E('input', {
|
||||
'type': 'button',
|
||||
'value': _('Test'),
|
||||
'class': 'cbi-button cbi-button-apply',
|
||||
'click': L.bind(this._runCheck, this, select, pre)
|
||||
});
|
||||
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
E('h2', {}, [ _('MPTCP Support Check') ]),
|
||||
E('div', { 'class': 'cbi-map-descr' },
|
||||
[ _('Check if MPTCP between interface and server is working.') ]),
|
||||
E('fieldset', { 'class': 'cbi-section' }, [
|
||||
E('legend', {}, [ _('Settings') ]),
|
||||
E('div', { 'class': 'cbi-value' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, [ _('Interface') ]),
|
||||
E('div', { 'class': 'cbi-value-field' }, [ select ])
|
||||
]),
|
||||
btn
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
pre,
|
||||
E('br'),
|
||||
E('i', {}, [ _('If you get "TCPOptionMPTCPCapable [...] Sender\'s Key" at the end, then MPTCP is supported. If there is a "-TCPOptionMPTCPCapable", then it\'s blocked.') ])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
_runCheck: function(select, pre) {
|
||||
var iface = select.value;
|
||||
if (!iface) return;
|
||||
|
||||
pre.textContent = _('Waiting for command to complete…');
|
||||
|
||||
return callMptcpCheckTrace(iface).then(function(text) {
|
||||
pre.textContent = (text || '').trim() || _('Error');
|
||||
}).catch(function() {
|
||||
pre.textContent = _('Error');
|
||||
});
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
|
||||
var callMptcpConnections = rpc.declare({
|
||||
object: 'luci.mptcp',
|
||||
method: 'mptcp_connections',
|
||||
expect: { output: '' }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
render: function() {
|
||||
var pre = E('pre', { 'id': 'mptcp-connections-output', 'style': 'white-space:pre-wrap' },
|
||||
[ _('Waiting for command to complete…') ]);
|
||||
|
||||
poll.add(L.bind(this._update, this, pre), 10);
|
||||
|
||||
this._update(pre);
|
||||
|
||||
return E('div', { 'class': 'cbi-section' }, [ pre ]);
|
||||
},
|
||||
|
||||
_update: function(pre) {
|
||||
return callMptcpConnections().then(function(text) {
|
||||
pre.textContent = (text || '').trim() || _('No data');
|
||||
}).catch(function() {
|
||||
pre.textContent = _('Error fetching connections data.');
|
||||
});
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
|
||||
var callMptcpFullmesh = rpc.declare({
|
||||
object: 'luci.mptcp',
|
||||
method: 'mptcp_fullmesh',
|
||||
expect: { output: '' }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
render: function() {
|
||||
var pre = E('pre', { 'id': 'mptcp-fullmesh-output', 'style': 'white-space:pre-wrap' },
|
||||
[ _('Waiting for command to complete…') ]);
|
||||
|
||||
poll.add(L.bind(this._update, this, pre), 10);
|
||||
|
||||
this._update(pre);
|
||||
|
||||
return E('div', { 'class': 'cbi-section' }, [ pre ]);
|
||||
},
|
||||
|
||||
_update: function(pre) {
|
||||
return callMptcpFullmesh().then(function(text) {
|
||||
pre.textContent = (text || '').trim() || _('(no output)');
|
||||
}).catch(function() {
|
||||
pre.textContent = _('Error fetching fullmesh data.');
|
||||
});
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
|
||||
var callMptcpMonitor = rpc.declare({
|
||||
object: 'luci.mptcp',
|
||||
method: 'mptcp_monitor',
|
||||
expect: { output: '' }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
render: function() {
|
||||
var pre = E('pre', { 'id': 'mptcp-monitor-output', 'style': 'white-space:pre-wrap' },
|
||||
[ _('Waiting for command to complete…') ]);
|
||||
|
||||
poll.add(L.bind(this._update, this, pre), 10);
|
||||
|
||||
/* Run immediately without waiting for the first poll interval */
|
||||
this._update(pre);
|
||||
|
||||
return E('div', { 'class': 'cbi-section' }, [ pre ]);
|
||||
},
|
||||
|
||||
_update: function(pre) {
|
||||
return callMptcpMonitor().then(function(text) {
|
||||
pre.textContent = (text || '').trim() || _('(no output)');
|
||||
}).catch(function() {
|
||||
pre.textContent = _('Error fetching monitor data.');
|
||||
});
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,973 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
|
||||
var callGetInterfaces = rpc.declare({
|
||||
object: 'network.interface',
|
||||
method: 'dump',
|
||||
expect: { interface: [] }
|
||||
});
|
||||
|
||||
var callMultipathBandwidth = rpc.declare({
|
||||
object: 'luci.mptcp',
|
||||
method: 'multipath_bandwidth',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callIfaceBandwidth = rpc.declare({
|
||||
object: 'luci.mptcp',
|
||||
method: 'interface_bandwidth',
|
||||
params: ['iface'],
|
||||
expect: { data: [] }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
STEP: 5,
|
||||
HEIGHT: 300,
|
||||
WIDTH: 760,
|
||||
|
||||
/* runtime state – reset on every tab switch */
|
||||
_state: null,
|
||||
_smaState: {},
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
uci.load('network'),
|
||||
callGetInterfaces()
|
||||
]);
|
||||
},
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Helpers *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
_ifaceColor: function(name) {
|
||||
var fixed = {
|
||||
'total': 'OrangeRed',
|
||||
'wan1': 'DeepSkyBlue',
|
||||
'wan2': 'SeaGreen',
|
||||
'wan3': 'PaleGreen',
|
||||
'wan4': 'PowderBlue',
|
||||
'wan5': 'Salmon',
|
||||
'wan6': 'LightGreen',
|
||||
'wan7': 'PaleTurquoise',
|
||||
'wan': 'FireBrick'
|
||||
};
|
||||
/* prefix match: wan1x should still get wan1's colour */
|
||||
var keys = Object.keys(fixed).sort(function(a, b) { return b.length - a.length; });
|
||||
for (var i = 0; i < keys.length; i++)
|
||||
if (name.indexOf(keys[i]) === 0) return fixed[keys[i]];
|
||||
/* deterministic hash fallback */
|
||||
var h = 0;
|
||||
for (var j = 0; j < name.length; j++)
|
||||
h = Math.imul(31, h) + name.charCodeAt(j) | 0;
|
||||
var c = '#';
|
||||
for (var k = 0; k < 3; k++)
|
||||
c += ('00' + ((h >> (k * 8)) & 0xFF).toString(16)).slice(-2);
|
||||
return c;
|
||||
},
|
||||
|
||||
_bwLabel: function(bytes, br) {
|
||||
if (bytes < 0) bytes = 0;
|
||||
var kby = bytes / 1024, uby = _('kB/s');
|
||||
if (kby >= 1024) { uby = _('MB/s'); kby /= 1024; }
|
||||
var kbi = bytes * 8 / 1024, ubi = _('kbit/s');
|
||||
if (kbi >= 1024) { ubi = _('Mbit/s'); kbi /= 1024; }
|
||||
return kbi.toFixed(2) + '\u202f' + ubi +
|
||||
(br ? '<br />' : ' ') +
|
||||
'(' + kby.toFixed(2) + '\u202f' + uby + ')';
|
||||
},
|
||||
|
||||
_sma: function(key, period, val) {
|
||||
if (!this._smaState[key]) this._smaState[key] = [];
|
||||
var buf = this._smaState[key];
|
||||
buf.push(val);
|
||||
if (buf.length > period) buf.shift();
|
||||
var s = 0;
|
||||
for (var i = 0; i < buf.length; i++) s += buf[i];
|
||||
return s / buf.length;
|
||||
},
|
||||
|
||||
_domSafeId: function(s) {
|
||||
return String(s == null ? '' : s).replace(/[^A-Za-z0-9_-]/g, '_');
|
||||
},
|
||||
|
||||
/* Returns [{name, label, device}] for multipath-enabled interfaces */
|
||||
_getMultipathIfaces: function(ifaceDump) {
|
||||
var devByName = {};
|
||||
(ifaceDump || []).forEach(function(ifc) {
|
||||
if (ifc['interface'] && ifc['l3_device'])
|
||||
devByName[ifc['interface']] = ifc['l3_device'];
|
||||
});
|
||||
var ifaces = [];
|
||||
uci.sections('network', 'interface', function(s) {
|
||||
var name = s['.name'];
|
||||
if (!name || name === 'loopback') return;
|
||||
var mp = s.multipath || '';
|
||||
if (mp !== 'on' && mp !== 'master' && mp !== 'backup' && mp !== 'handover') return;
|
||||
var dev = devByName[name] || s.device || s.ifname || name;
|
||||
ifaces.push({ name: name, label: s.label || name, device: dev });
|
||||
});
|
||||
return ifaces;
|
||||
},
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* SVG construction *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
_createSVG: function(id, height) {
|
||||
var NS = 'http://www.w3.org/2000/svg';
|
||||
var svg = document.createElementNS(NS, 'svg');
|
||||
svg.id = id;
|
||||
svg.setAttribute('width', '100%');
|
||||
svg.setAttribute('height', String(height));
|
||||
svg.setAttribute('viewBox', '0 0 ' + this.WIDTH + ' ' + height);
|
||||
svg.setAttribute('preserveAspectRatio', 'none');
|
||||
svg.style.cssText = 'display:block;border:1px solid #d6e2ee;border-radius:14px;background:linear-gradient(180deg,#fdfefe 0%,#eef5fb 100%);box-shadow:inset 0 1px 0 rgba(255,255,255,.9);box-sizing:border-box;';
|
||||
|
||||
var g = document.createElementNS(NS, 'g');
|
||||
g.id = id + '_g';
|
||||
svg.appendChild(g);
|
||||
|
||||
/* Scale labels at 25 / 50 / 75 % */
|
||||
[25, 50, 75].forEach(function(pct) {
|
||||
var t = document.createElementNS(NS, 'text');
|
||||
t.id = id + '_lbl' + pct;
|
||||
t.setAttribute('x', '4');
|
||||
t.setAttribute('y', String(height - Math.round(height * pct / 100) - 3));
|
||||
t.setAttribute('style', 'fill:#888;font-size:9px;');
|
||||
t.appendChild(document.createTextNode(''));
|
||||
svg.appendChild(t);
|
||||
});
|
||||
|
||||
return svg;
|
||||
},
|
||||
|
||||
_addTimeLines: function(id, width, height, step) {
|
||||
var NS = 'http://www.w3.org/2000/svg';
|
||||
var g = document.getElementById(id + '_g');
|
||||
if (!g) return;
|
||||
[25, 50, 75].forEach(function(pct) {
|
||||
var y = height - Math.round(height * pct / 100);
|
||||
var hline = document.createElementNS(NS, 'line');
|
||||
hline.setAttribute('x1', 0);
|
||||
hline.setAttribute('y1', y);
|
||||
hline.setAttribute('x2', width);
|
||||
hline.setAttribute('y2', y);
|
||||
hline.setAttribute('style', 'stroke:#d9e5ef;stroke-width:0.8');
|
||||
g.insertBefore(hline, g.firstChild);
|
||||
});
|
||||
for (var x = width % (step * 60); x < width; x += step * 60) {
|
||||
var line = document.createElementNS(NS, 'line');
|
||||
line.setAttribute('x1', x); line.setAttribute('y1', 0);
|
||||
line.setAttribute('x2', x); line.setAttribute('y2', height);
|
||||
line.setAttribute('style', 'stroke:#d9e5ef;stroke-width:0.8');
|
||||
g.insertBefore(line, g.firstChild);
|
||||
var txt = document.createElementNS(NS, 'text');
|
||||
txt.setAttribute('x', x + 3);
|
||||
txt.setAttribute('y', 12);
|
||||
txt.setAttribute('style', 'fill:#8ca0b4;font-size:8px;');
|
||||
txt.appendChild(document.createTextNode(Math.round((width - x) / step / 60) + 'm'));
|
||||
g.insertBefore(txt, g.firstChild);
|
||||
}
|
||||
},
|
||||
|
||||
_scaleLbl: function(id, pct, maxBytes) {
|
||||
var el = document.getElementById(id + '_lbl' + pct);
|
||||
if (el) el.textContent = this._bwLabel(maxBytes * pct / 100);
|
||||
},
|
||||
|
||||
_injectStyles: function() {
|
||||
if (document.getElementById('mptcp-bw-styles'))
|
||||
return;
|
||||
|
||||
var style = document.createElement('style');
|
||||
style.id = 'mptcp-bw-styles';
|
||||
style.textContent = [
|
||||
'.mptcp-summary-card{margin-bottom:18px;padding:18px;}',
|
||||
'.mptcp-summary-card__header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:12px;}',
|
||||
'.mptcp-summary-card__title{font-size:17px;font-weight:700;color:#17324e;line-height:1.2;}',
|
||||
'.mptcp-summary-card__meta{margin-top:4px;font-size:11px;color:#688198;}',
|
||||
'.mptcp-summary-card__legend{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;}',
|
||||
'.mptcp-legend-pill{display:inline-flex;align-items:center;gap:6px;padding:5px 10px;border-radius:999px;background:#fff;border:1px solid #dde7f1;font-size:11px;font-weight:700;color:#29445f;}',
|
||||
'.mptcp-legend-pill__swatch{width:10px;height:10px;border-radius:999px;display:inline-block;}',
|
||||
'.mptcp-summary-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:12px;}',
|
||||
'.mptcp-all-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:18px;align-items:start;}',
|
||||
'.mptcp-wan-card{padding:16px;}',
|
||||
'.mptcp-wan-card__header{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;margin-bottom:12px;}',
|
||||
'.mptcp-wan-card__title{font-size:15px;font-weight:700;color:#16324f;line-height:1.2;}',
|
||||
'.mptcp-wan-card__meta{margin-top:3px;font-size:11px;color:#5b728a;}',
|
||||
'.mptcp-wan-card__badge{display:inline-flex;align-items:center;padding:4px 10px;border-radius:999px;border:1px solid currentColor;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;white-space:nowrap;}',
|
||||
'.mptcp-chart-block + .mptcp-chart-block{margin-top:14px;padding-top:14px;border-top:1px solid #dde7f1;}',
|
||||
'.mptcp-chart-block__title{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;font-size:12px;font-weight:700;color:#29445f;text-transform:uppercase;letter-spacing:.04em;}',
|
||||
'.mptcp-chart-block__stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:10px;}',
|
||||
'.mptcp-stat{background:#fff;border:1px solid #e2ebf3;border-radius:12px;padding:8px 10px;min-width:0;}',
|
||||
'.mptcp-stat__label{display:block;font-size:10px;font-weight:700;color:#6b8298;text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px;}',
|
||||
'.mptcp-stat__value{display:block;font-size:12px;color:#18344f;line-height:1.35;}',
|
||||
'.mptcp-chart-block__meta{margin-top:8px;font-size:11px;color:#6b8298;text-align:right;}',
|
||||
'.mptcp-empty-state{padding:28px;border:1px dashed #c9d7e5;border-radius:14px;background:#f8fbfd;color:#56708a;text-align:center;}',
|
||||
'#mptcp-tooltip{position:fixed;z-index:9999;pointer-events:none;background:rgba(15,23,42,.88);color:#e8f0f8;border-radius:8px;padding:8px 12px;font-size:12px;line-height:1.7;box-shadow:0 4px 16px rgba(0,0,0,.3);white-space:nowrap;display:none;}'
|
||||
].join('');
|
||||
document.head.appendChild(style);
|
||||
},
|
||||
|
||||
_getTooltip: function() {
|
||||
var tip = document.getElementById('mptcp-tooltip');
|
||||
if (!tip) {
|
||||
tip = document.createElement('div');
|
||||
tip.id = 'mptcp-tooltip';
|
||||
document.body.appendChild(tip);
|
||||
}
|
||||
return tip;
|
||||
},
|
||||
|
||||
_xToIndex: function(svgX, seriesLen) {
|
||||
if (!seriesLen) return -1;
|
||||
var startX = this.WIDTH - (seriesLen - 1) * this.STEP;
|
||||
var i = Math.round((svgX - startX) / this.STEP);
|
||||
return Math.max(0, Math.min(seriesLen - 1, i));
|
||||
},
|
||||
|
||||
_addSVGHover: function(svgId, height, getContent) {
|
||||
var self = this;
|
||||
var svg = document.getElementById(svgId);
|
||||
if (!svg) return;
|
||||
var NS = 'http://www.w3.org/2000/svg';
|
||||
var g = document.getElementById(svgId + '_g');
|
||||
var xh = document.createElementNS(NS, 'line');
|
||||
xh.setAttribute('y1', '0');
|
||||
xh.setAttribute('y2', String(height));
|
||||
xh.setAttribute('style', 'stroke:#8ca0b4;stroke-width:1;stroke-dasharray:3,2;visibility:hidden');
|
||||
if (g) g.appendChild(xh);
|
||||
svg.style.cursor = 'crosshair';
|
||||
svg.addEventListener('mousemove', function(ev) {
|
||||
var rect = svg.getBoundingClientRect();
|
||||
if (!rect.width) return;
|
||||
var svgX = (ev.clientX - rect.left) * self.WIDTH / rect.width;
|
||||
svgX = Math.max(0, Math.min(self.WIDTH, svgX));
|
||||
xh.setAttribute('x1', String(svgX));
|
||||
xh.setAttribute('x2', String(svgX));
|
||||
xh.style.visibility = 'visible';
|
||||
var html = getContent(svgX);
|
||||
var tip = self._getTooltip();
|
||||
if (html) {
|
||||
tip.innerHTML = html;
|
||||
var tx = ev.clientX + 14;
|
||||
var ty = ev.clientY - 10;
|
||||
if (tx + 200 > window.innerWidth) tx = ev.clientX - 214;
|
||||
tip.style.left = tx + 'px';
|
||||
tip.style.top = ty + 'px';
|
||||
tip.style.display = 'block';
|
||||
} else {
|
||||
tip.style.display = 'none';
|
||||
}
|
||||
});
|
||||
svg.addEventListener('mouseleave', function() {
|
||||
xh.style.visibility = 'hidden';
|
||||
var tip = document.getElementById('mptcp-tooltip');
|
||||
if (tip) tip.style.display = 'none';
|
||||
});
|
||||
},
|
||||
|
||||
_linePoints: function(series, width, height, step, scale) {
|
||||
if (!series.length) return '';
|
||||
var startX = width - (series.length - 1) * step;
|
||||
var points = '';
|
||||
for (var i = 0; i < series.length; i++) {
|
||||
var x = startX + i * step;
|
||||
var y = height - Math.floor((series[i] || 0) * scale);
|
||||
points += (i ? ' ' : '') + x + ',' + y;
|
||||
}
|
||||
var lastY = height - Math.floor((series[series.length - 1] || 0) * scale);
|
||||
points += ' ' + width + ',' + lastY;
|
||||
return points;
|
||||
},
|
||||
|
||||
_areaPoints: function(series, width, height, step, scale) {
|
||||
if (!series.length) return '';
|
||||
var startX = width - (series.length - 1) * step;
|
||||
var points = startX + ',' + height;
|
||||
for (var i = 0; i < series.length; i++) {
|
||||
var x = startX + i * step;
|
||||
var y = height - Math.floor((series[i] || 0) * scale);
|
||||
points += ' ' + x + ',' + y;
|
||||
}
|
||||
var lastY = height - Math.floor((series[series.length - 1] || 0) * scale);
|
||||
points += ' ' + width + ',' + lastY;
|
||||
points += ' ' + width + ',' + height + ' ' + startX + ',' + height;
|
||||
return points;
|
||||
},
|
||||
|
||||
_seriesStats: function(series) {
|
||||
var current = 0, peak = 0, sum = 0;
|
||||
for (var i = 0; i < series.length; i++) {
|
||||
var val = series[i] || 0;
|
||||
sum += val;
|
||||
peak = Math.max(peak, val);
|
||||
}
|
||||
if (series.length)
|
||||
current = series[series.length - 1] || 0;
|
||||
return {
|
||||
current: current,
|
||||
average: series.length ? (sum / series.length) : 0,
|
||||
peak: peak
|
||||
};
|
||||
},
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Render *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
render: function(data) {
|
||||
var self = this;
|
||||
var ifaces = this._getMultipathIfaces(data[1]);
|
||||
var allTabs = [{ name: 'all', label: _('All interfaces'), device: 'all' }].concat(ifaces);
|
||||
this._allIfaces = ifaces;
|
||||
this._injectStyles();
|
||||
|
||||
var root = E('div', { 'id': 'bw-root' }, [
|
||||
E('ul', { 'class': 'cbi-tabmenu', 'id': 'bw-tabmenu' }),
|
||||
E('div', { 'id': 'bw-area' })
|
||||
]);
|
||||
|
||||
var tabmenu = root.querySelector('#bw-tabmenu');
|
||||
allTabs.forEach(function(tab) {
|
||||
var li = E('li', { 'class': tab.name === 'all' ? 'cbi-tab' : 'cbi-tab-disabled' },
|
||||
E('a', { 'href': '#', 'click': function(ev) {
|
||||
ev.preventDefault();
|
||||
self._switchTab(tab, allTabs);
|
||||
}}, tab.label)
|
||||
);
|
||||
li.id = 'bwtab-' + tab.name;
|
||||
tabmenu.appendChild(li);
|
||||
});
|
||||
|
||||
/* Register the single recurring poll; data arrives after SVG is sized */
|
||||
poll.add(L.bind(this._tick, this), 1);
|
||||
|
||||
/* Build initial chart after first layout pass */
|
||||
requestAnimationFrame(function() {
|
||||
self._switchTab(allTabs[0], allTabs);
|
||||
});
|
||||
|
||||
return root;
|
||||
},
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Tab switching *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
_switchTab: function(tab, allTabs) {
|
||||
var self = this;
|
||||
|
||||
allTabs.forEach(function(t) {
|
||||
var li = document.getElementById('bwtab-' + t.name);
|
||||
if (li) li.className = (t.name === tab.name) ? 'cbi-tab' : 'cbi-tab-disabled';
|
||||
});
|
||||
|
||||
/* Nulling _state makes _tick a no-op until the new state is ready */
|
||||
this._state = null;
|
||||
this._smaState = {};
|
||||
|
||||
var area = document.getElementById('bw-area');
|
||||
if (!area) return;
|
||||
area.innerHTML = '';
|
||||
|
||||
if (tab.name === 'all')
|
||||
this._buildAllCharts(area, this._allIfaces || []);
|
||||
else
|
||||
this._buildIfaceChart(area, tab);
|
||||
},
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* "All interfaces" chart construction *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
_buildAllCharts: function(area, ifaces) {
|
||||
var self = this;
|
||||
ifaces = ifaces || [];
|
||||
|
||||
if (!ifaces.length) {
|
||||
area.appendChild(E('div', { 'class': 'mptcp-empty-state' },
|
||||
_('No multipath WAN interface is currently available.')));
|
||||
return;
|
||||
}
|
||||
|
||||
var totalInColor = '#2f7df6';
|
||||
var totalOutColor = '#0f9d7a';
|
||||
|
||||
var legendPills = [
|
||||
E('span', { 'class': 'mptcp-legend-pill' }, [
|
||||
E('span', { 'class': 'mptcp-legend-pill__swatch', 'style': 'background:' + totalInColor }),
|
||||
_('Total in')
|
||||
]),
|
||||
E('span', { 'class': 'mptcp-legend-pill' }, [
|
||||
E('span', { 'class': 'mptcp-legend-pill__swatch', 'style': 'background:' + totalOutColor }),
|
||||
_('Total out')
|
||||
])
|
||||
];
|
||||
ifaces.forEach(function(iface) {
|
||||
var c = self._ifaceColor(iface.name);
|
||||
legendPills.push(E('span', { 'class': 'mptcp-legend-pill' }, [
|
||||
E('span', { 'class': 'mptcp-legend-pill__swatch', 'style': 'background:' + c }),
|
||||
iface.label
|
||||
]));
|
||||
});
|
||||
|
||||
var makeSummaryStats = function(prefix) {
|
||||
var cells = [];
|
||||
/* header row */
|
||||
['', _('Current'), _('Average'), _('Peak')].forEach(function(h) {
|
||||
cells.push(E('div', { 'class': 'mptcp-stat', 'style': 'background:#eef3f8;' }, [
|
||||
E('span', { 'class': 'mptcp-stat__label', 'style': 'display:block;text-align:center;' }, h)
|
||||
]));
|
||||
});
|
||||
/* one row per WAN interface */
|
||||
ifaces.forEach(function(iface) {
|
||||
var sid = self._domSafeId(iface.name);
|
||||
var c = self._ifaceColor(iface.name);
|
||||
cells.push(E('div', { 'class': 'mptcp-stat' }, [
|
||||
E('span', { 'class': 'mptcp-stat__value', 'style': 'color:' + c + ';font-weight:700;font-size:11px;' }, iface.label)
|
||||
]));
|
||||
['_cur', '_avg', '_peak'].forEach(function(suf) {
|
||||
cells.push(E('div', { 'class': 'mptcp-stat' }, [
|
||||
E('span', { 'id': prefix + '_' + sid + suf, 'class': 'mptcp-stat__value' }, '0\u202f' + _('kbit/s'))
|
||||
]));
|
||||
});
|
||||
});
|
||||
/* total row */
|
||||
cells.push(E('div', { 'class': 'mptcp-stat', 'style': 'background:#eef3f8;' }, [
|
||||
E('span', { 'class': 'mptcp-stat__value', 'style': 'font-weight:700;font-size:11px;' }, _('Total'))
|
||||
]));
|
||||
['_cur', '_avg', '_peak'].forEach(function(suf) {
|
||||
cells.push(E('div', { 'class': 'mptcp-stat', 'style': 'background:#eef3f8;' }, [
|
||||
E('span', { 'id': prefix + suf, 'class': 'mptcp-stat__value' }, '0\u202f' + _('kbit/s'))
|
||||
]));
|
||||
});
|
||||
return E('div', { 'style': 'display:grid;grid-template-columns:minmax(80px,auto) repeat(3,minmax(0,1fr));gap:6px;margin-top:10px;' }, cells);
|
||||
};
|
||||
|
||||
var combinedHeight = Math.max(190, Math.round(this.HEIGHT * 0.72));
|
||||
var totalCard = E('div', { 'class': 'mptcp-summary-card' }, [
|
||||
E('div', { 'class': 'mptcp-summary-card__header' }, [
|
||||
E('div', {}, [
|
||||
E('div', { 'class': 'mptcp-summary-card__title' }, _('All interfaces – combined view')),
|
||||
E('div', { 'class': 'mptcp-summary-card__meta' }, _('All interfaces overlaid, inbound and outbound separately'))
|
||||
]),
|
||||
E('div', { 'class': 'mptcp-summary-card__legend' }, legendPills)
|
||||
]),
|
||||
E('div', { 'class': 'mptcp-chart-block' }, [
|
||||
E('div', { 'class': 'mptcp-chart-block__title' }, _('Inbound – all interfaces')),
|
||||
self._createSVG('total_dn_svg', combinedHeight),
|
||||
makeSummaryStats('total_dn'),
|
||||
E('div', { 'id': 'total_dn_scale', 'class': 'mptcp-chart-block__meta' }, '-')
|
||||
]),
|
||||
E('div', { 'class': 'mptcp-chart-block' }, [
|
||||
E('div', { 'class': 'mptcp-chart-block__title' }, _('Outbound – all interfaces')),
|
||||
self._createSVG('total_up_svg', combinedHeight),
|
||||
makeSummaryStats('total_up'),
|
||||
E('div', { 'id': 'total_up_scale', 'class': 'mptcp-chart-block__meta' }, '-')
|
||||
])
|
||||
]);
|
||||
area.appendChild(totalCard);
|
||||
|
||||
var cards = {};
|
||||
ifaces.forEach(function(iface) {
|
||||
cards[iface.name] = { color: self._ifaceColor(iface.name), label: iface.label };
|
||||
});
|
||||
|
||||
requestAnimationFrame(function() {
|
||||
var totalWidth = self.WIDTH;
|
||||
var totalHeight = Math.max(190, Math.round(self.HEIGHT * 0.72));
|
||||
var step = self.STEP;
|
||||
var wanted = Math.ceil(totalWidth / step);
|
||||
var NS = 'http://www.w3.org/2000/svg';
|
||||
var minWin = Math.round(wanted / 60);
|
||||
|
||||
self._addTimeLines('total_dn_svg', totalWidth, totalHeight, step);
|
||||
self._addTimeLines('total_up_svg', totalWidth, totalHeight, step);
|
||||
|
||||
var totalDnG = document.getElementById('total_dn_svg_g');
|
||||
var totalUpG = document.getElementById('total_up_svg_g');
|
||||
|
||||
var combinedDn = { width: totalWidth, height: totalHeight, id: 'total_dn_svg', ifaceLines: {} };
|
||||
var combinedUp = { width: totalWidth, height: totalHeight, id: 'total_up_svg', ifaceLines: {} };
|
||||
|
||||
/* Per-interface lines (behind total) */
|
||||
ifaces.forEach(function(iface) {
|
||||
var color = self._ifaceColor(iface.name);
|
||||
|
||||
var dnFill = document.createElementNS(NS, 'polyline');
|
||||
dnFill.setAttribute('style', 'fill:' + color + ';fill-opacity:0.10;stroke:none');
|
||||
if (totalDnG) totalDnG.appendChild(dnFill);
|
||||
var dnLine = document.createElementNS(NS, 'polyline');
|
||||
dnLine.setAttribute('style', 'fill:none;stroke:' + color + ';stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;opacity:0.85');
|
||||
if (totalDnG) totalDnG.appendChild(dnLine);
|
||||
combinedDn.ifaceLines[iface.name] = { fill: dnFill, line: dnLine };
|
||||
|
||||
var upFill = document.createElementNS(NS, 'polyline');
|
||||
upFill.setAttribute('style', 'fill:' + color + ';fill-opacity:0.10;stroke:none');
|
||||
if (totalUpG) totalUpG.appendChild(upFill);
|
||||
var upLine = document.createElementNS(NS, 'polyline');
|
||||
upLine.setAttribute('style', 'fill:none;stroke:' + color + ';stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;opacity:0.85');
|
||||
if (totalUpG) totalUpG.appendChild(upLine);
|
||||
combinedUp.ifaceLines[iface.name] = { fill: upFill, line: upLine };
|
||||
});
|
||||
|
||||
/* Total lines on top */
|
||||
var totalDnFill = document.createElementNS(NS, 'polyline');
|
||||
totalDnFill.setAttribute('style', 'fill:' + totalInColor + ';fill-opacity:0.18;stroke:none');
|
||||
if (totalDnG) totalDnG.appendChild(totalDnFill);
|
||||
var totalDnLine = document.createElementNS(NS, 'polyline');
|
||||
totalDnLine.setAttribute('style', 'fill:none;stroke:' + totalInColor + ';stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round');
|
||||
if (totalDnG) totalDnG.appendChild(totalDnLine);
|
||||
combinedDn.totalFill = totalDnFill;
|
||||
combinedDn.totalLine = totalDnLine;
|
||||
|
||||
var totalUpFill = document.createElementNS(NS, 'polyline');
|
||||
totalUpFill.setAttribute('style', 'fill:' + totalOutColor + ';fill-opacity:0.18;stroke:none');
|
||||
if (totalUpG) totalUpG.appendChild(totalUpFill);
|
||||
var totalUpLine = document.createElementNS(NS, 'polyline');
|
||||
totalUpLine.setAttribute('style', 'fill:none;stroke:' + totalOutColor + ';stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round');
|
||||
if (totalUpG) totalUpG.appendChild(totalUpLine);
|
||||
combinedUp.totalFill = totalUpFill;
|
||||
combinedUp.totalLine = totalUpLine;
|
||||
|
||||
var dnScaleEl = document.getElementById('total_dn_scale');
|
||||
var upScaleEl = document.getElementById('total_up_scale');
|
||||
if (dnScaleEl) dnScaleEl.textContent = '(' + minWin + ' ' + _('minutes window, 1 second interval') + ')';
|
||||
if (upScaleEl) upScaleEl.textContent = '(' + minWin + ' ' + _('minutes window, 1 second interval') + ')';
|
||||
|
||||
self._state = {
|
||||
mode: 'all',
|
||||
step: step,
|
||||
wanted: wanted,
|
||||
stamp: 0,
|
||||
stampIface: {},
|
||||
cards: cards,
|
||||
ifaces: ifaces.map(function(iface) { return iface.name; }),
|
||||
combinedDn: combinedDn,
|
||||
combinedUp: combinedUp,
|
||||
dndata: {},
|
||||
updata: {},
|
||||
totalDn: [],
|
||||
totalUp: []
|
||||
};
|
||||
ifaces.forEach(function(iface) {
|
||||
self._state.dndata[iface.name] = [];
|
||||
self._state.updata[iface.name] = [];
|
||||
self._state.stampIface[iface.name] = 0;
|
||||
});
|
||||
|
||||
/* Hover tooltips */
|
||||
self._addSVGHover('total_dn_svg', totalHeight, function(svgX) {
|
||||
var st = self._state;
|
||||
if (!st || st.mode !== 'all') return null;
|
||||
var lines = [];
|
||||
var ti = self._xToIndex(svgX, st.totalDn.length);
|
||||
if (ti >= 0) lines.push('<span style="color:#5aabff">\u25bc\u00a0' + _('Total') + ':</span> ' + self._bwLabel(st.totalDn[ti] || 0, false));
|
||||
st.ifaces.forEach(function(itf) {
|
||||
var series = (st.dndata && st.dndata[itf]) || [];
|
||||
var j = self._xToIndex(svgX, series.length);
|
||||
if (j < 0) return;
|
||||
var c = st.cards[itf];
|
||||
lines.push('<span style="color:' + (c ? c.color : '#aaa') + '">' + (c ? c.label : itf) + ':</span> ' + self._bwLabel(series[j] || 0, false));
|
||||
});
|
||||
return lines.length ? lines.join('<br>') : null;
|
||||
});
|
||||
self._addSVGHover('total_up_svg', totalHeight, function(svgX) {
|
||||
var st = self._state;
|
||||
if (!st || st.mode !== 'all') return null;
|
||||
var lines = [];
|
||||
var ti = self._xToIndex(svgX, st.totalUp.length);
|
||||
if (ti >= 0) lines.push('<span style="color:#3ecfa8">\u25b2\u00a0' + _('Total') + ':</span> ' + self._bwLabel(st.totalUp[ti] || 0, false));
|
||||
st.ifaces.forEach(function(itf) {
|
||||
var series = (st.updata && st.updata[itf]) || [];
|
||||
var j = self._xToIndex(svgX, series.length);
|
||||
if (j < 0) return;
|
||||
var c = st.cards[itf];
|
||||
lines.push('<span style="color:' + (c ? c.color : '#aaa') + '">' + (c ? c.label : itf) + ':</span> ' + self._bwLabel(series[j] || 0, false));
|
||||
});
|
||||
return lines.length ? lines.join('<br>') : null;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Single-interface chart construction *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
_buildIfaceChart: function(area, tab) {
|
||||
var self = this;
|
||||
var color = this._ifaceColor(tab.name);
|
||||
var badgeStyle = 'background:' + color + '18;color:' + color + ';border-color:' + color + '55;';
|
||||
var chartHeight = Math.max(160, Math.round(this.HEIGHT * 0.58));
|
||||
|
||||
var makeStats = function(prefix) {
|
||||
return E('div', { 'class': 'mptcp-chart-block__stats' }, [
|
||||
E('div', { 'class': 'mptcp-stat' }, [
|
||||
E('span', { 'class': 'mptcp-stat__label' }, _('Current')),
|
||||
E('span', { 'id': prefix + '_cur', 'class': 'mptcp-stat__value' }, '0 ' + _('kbit/s'))
|
||||
]),
|
||||
E('div', { 'class': 'mptcp-stat' }, [
|
||||
E('span', { 'class': 'mptcp-stat__label' }, _('Average')),
|
||||
E('span', { 'id': prefix + '_avg', 'class': 'mptcp-stat__value' }, '0 ' + _('kbit/s'))
|
||||
]),
|
||||
E('div', { 'class': 'mptcp-stat' }, [
|
||||
E('span', { 'class': 'mptcp-stat__label' }, _('Peak')),
|
||||
E('span', { 'id': prefix + '_peak', 'class': 'mptcp-stat__value' }, '0 ' + _('kbit/s'))
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
var card = E('div', { 'class': 'mptcp-wan-card' }, [
|
||||
E('div', { 'class': 'mptcp-wan-card__header' }, [
|
||||
E('div', {}, [
|
||||
E('div', { 'class': 'mptcp-wan-card__title' }, tab.label),
|
||||
E('div', { 'class': 'mptcp-wan-card__meta' }, tab.device || '')
|
||||
]),
|
||||
E('span', { 'class': 'mptcp-wan-card__badge', 'style': badgeStyle }, tab.name)
|
||||
]),
|
||||
E('div', { 'class': 'mptcp-chart-block' }, [
|
||||
E('div', { 'class': 'mptcp-chart-block__title' }, _('Inbound traffic')),
|
||||
self._createSVG('bwsvg_dn', chartHeight),
|
||||
makeStats('rx_bw'),
|
||||
E('div', { 'id': 'bwscale_dn', 'class': 'mptcp-chart-block__meta' }, '-')
|
||||
]),
|
||||
E('div', { 'class': 'mptcp-chart-block' }, [
|
||||
E('div', { 'class': 'mptcp-chart-block__title' }, _('Outbound traffic')),
|
||||
self._createSVG('bwsvg_up', chartHeight),
|
||||
makeStats('tx_bw'),
|
||||
E('div', { 'id': 'bwscale_up', 'class': 'mptcp-chart-block__meta' }, '-')
|
||||
])
|
||||
]);
|
||||
area.appendChild(card);
|
||||
|
||||
var NS = 'http://www.w3.org/2000/svg';
|
||||
['dn', 'up'].forEach(function(dir) {
|
||||
var g = document.getElementById('bwsvg_' + dir + '_g');
|
||||
var fill = document.createElementNS(NS, 'polyline');
|
||||
fill.id = 'bwsvg_' + dir + '_fill';
|
||||
fill.setAttribute('style', 'fill:' + color + ';fill-opacity:0.18;stroke:none');
|
||||
if (g) g.appendChild(fill);
|
||||
var line = document.createElementNS(NS, 'polyline');
|
||||
line.id = 'bwsvg_' + dir + '_line';
|
||||
line.setAttribute('style', 'fill:none;stroke:' + color + ';stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round');
|
||||
if (g) g.appendChild(line);
|
||||
});
|
||||
|
||||
requestAnimationFrame(function() {
|
||||
var width = self.WIDTH;
|
||||
var height = chartHeight;
|
||||
var step = self.STEP;
|
||||
var wanted = Math.ceil(width / step);
|
||||
var minWin = Math.round(wanted / 60);
|
||||
|
||||
self._addTimeLines('bwsvg_dn', width, height, step);
|
||||
self._addTimeLines('bwsvg_up', width, height, step);
|
||||
|
||||
var scDn = document.getElementById('bwscale_dn');
|
||||
var scUp = document.getElementById('bwscale_up');
|
||||
if (scDn) scDn.textContent = '(' + minWin + ' ' + _('minutes window, 1 second interval') + ')';
|
||||
if (scUp) scUp.textContent = '(' + minWin + ' ' + _('minutes window, 1 second interval') + ')';
|
||||
|
||||
self._state = {
|
||||
mode: 'iface',
|
||||
device: tab.device,
|
||||
width: width,
|
||||
height: height,
|
||||
step: step,
|
||||
wanted: wanted,
|
||||
stamp: 0,
|
||||
dataRx: [],
|
||||
dataTx: []
|
||||
};
|
||||
|
||||
self._addSVGHover('bwsvg_dn', height, function(svgX) {
|
||||
var st = self._state;
|
||||
if (!st || st.mode !== 'iface') return null;
|
||||
var i = self._xToIndex(svgX, st.dataRx.length);
|
||||
if (i < 0) return null;
|
||||
return '<span style="color:#6699ff">\u25bc\u00a0' + _('In') + ':</span> ' + self._bwLabel(st.dataRx[i] || 0, false);
|
||||
});
|
||||
self._addSVGHover('bwsvg_up', height, function(svgX) {
|
||||
var st = self._state;
|
||||
if (!st || st.mode !== 'iface') return null;
|
||||
var i = self._xToIndex(svgX, st.dataTx.length);
|
||||
if (i < 0) return null;
|
||||
return '<span style="color:#44cc88">\u25b2\u00a0' + _('Out') + ':</span> ' + self._bwLabel(st.dataTx[i] || 0, false);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Poll dispatcher *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
_tick: function() {
|
||||
if (!this._state) return Promise.resolve();
|
||||
if (this._state.mode === 'all') return this._pollAll();
|
||||
if (this._state.mode === 'iface') return this._pollIface();
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* "All interfaces" poll & render *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
_pollAll: function() {
|
||||
var self = this;
|
||||
var st = this._state;
|
||||
|
||||
return callMultipathBandwidth().then(function(raw) {
|
||||
if (!st || st.mode !== 'all') return;
|
||||
if (!raw || typeof raw !== 'object') return;
|
||||
|
||||
/* Accept both direct payload and wrapped { data: ... } payloads */
|
||||
var payload = (raw.data && typeof raw.data === 'object') ? raw.data : raw;
|
||||
|
||||
/* Interface data can arrive as arrays, strings or wrapped arrays */
|
||||
var parsed = {};
|
||||
var keys = Object.keys(payload);
|
||||
var nextStamp = st.stamp;
|
||||
|
||||
keys.forEach(function(itf) {
|
||||
var v = payload[itf];
|
||||
if (Array.isArray(v)) {
|
||||
parsed[itf] = v;
|
||||
}
|
||||
else if (v && Array.isArray(v.data)) {
|
||||
parsed[itf] = v.data;
|
||||
}
|
||||
else if (typeof v === 'string' && v !== '') {
|
||||
try {
|
||||
var j = JSON.parse(v);
|
||||
if (Array.isArray(j))
|
||||
parsed[itf] = j;
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
|
||||
/* Append new data points using per-interface stamps so a slow/short
|
||||
interface is not permanently frozen by a faster one's stamp. */
|
||||
st.ifaces.forEach(function(itf) {
|
||||
var d = parsed[itf];
|
||||
if (!d) return;
|
||||
var prev = st.stampIface[itf] || 0;
|
||||
for (var i = (prev ? 0 : 1); i < d.length; i++) {
|
||||
if (d[i][0] <= prev) continue;
|
||||
if (i > 0) {
|
||||
var dt = d[i][0] - d[i-1][0];
|
||||
if (dt) {
|
||||
st.dndata[itf].push(self._sma('dn_'+itf, 15, Math.max(0, (d[i][1]-d[i-1][1])/dt)));
|
||||
st.updata[itf].push(self._sma('up_'+itf, 15, Math.max(0, (d[i][3]-d[i-1][3])/dt)));
|
||||
}
|
||||
}
|
||||
}
|
||||
st.dndata[itf] = st.dndata[itf].slice(-st.wanted);
|
||||
st.updata[itf] = st.updata[itf].slice(-st.wanted);
|
||||
if (d.length) {
|
||||
st.stampIface[itf] = d[d.length-1][0];
|
||||
nextStamp = Math.max(nextStamp, st.stampIface[itf]);
|
||||
}
|
||||
});
|
||||
|
||||
var totalSeries = parsed.total;
|
||||
if (totalSeries) {
|
||||
var prevTotal = st.stamp;
|
||||
for (var ti = (prevTotal ? 0 : 1); ti < totalSeries.length; ti++) {
|
||||
if (totalSeries[ti][0] <= prevTotal) continue;
|
||||
if (ti > 0) {
|
||||
var tdt = totalSeries[ti][0] - totalSeries[ti - 1][0];
|
||||
if (tdt) {
|
||||
st.totalDn.push(self._sma('dn_total', 15, Math.max(0, (totalSeries[ti][1] - totalSeries[ti - 1][1]) / tdt)));
|
||||
st.totalUp.push(self._sma('up_total', 15, Math.max(0, (totalSeries[ti][3] - totalSeries[ti - 1][3]) / tdt)));
|
||||
}
|
||||
}
|
||||
}
|
||||
st.totalDn = st.totalDn.slice(-st.wanted);
|
||||
st.totalUp = st.totalUp.slice(-st.wanted);
|
||||
nextStamp = Math.max(nextStamp, totalSeries[totalSeries.length - 1][0]);
|
||||
}
|
||||
|
||||
st.stamp = nextStamp;
|
||||
|
||||
if (st.combinedDn && st.combinedUp) {
|
||||
var scDnAll, scUpAll;
|
||||
var cDn = st.combinedDn, cUp = st.combinedUp;
|
||||
|
||||
/* Align all series to globalStamp:
|
||||
1. Right-pad lagging interfaces with their last known value.
|
||||
2. Left-pad shorter histories with 0 so all start at the same x. */
|
||||
var globalStamp = st.stamp;
|
||||
function alignSeries(s, itfStamp) {
|
||||
var lag = (itfStamp > 0) ? Math.max(0, Math.round(globalStamp - itfStamp)) : 0;
|
||||
if (lag === 0) return s;
|
||||
var lastVal = s.length > 0 ? s[s.length - 1] : 0;
|
||||
var out = s.slice();
|
||||
for (var _p = 0; _p < Math.min(lag, st.wanted); _p++) out.push(lastVal);
|
||||
return out;
|
||||
}
|
||||
var alignedDn = {}, alignedUp = {};
|
||||
var refLen = 0;
|
||||
st.ifaces.forEach(function(itf) {
|
||||
var aDn = alignSeries(st.dndata[itf] || [], st.stampIface[itf] || 0);
|
||||
var aUp = alignSeries(st.updata[itf] || [], st.stampIface[itf] || 0);
|
||||
alignedDn[itf] = aDn;
|
||||
alignedUp[itf] = aUp;
|
||||
refLen = Math.max(refLen, aDn.length, aUp.length);
|
||||
});
|
||||
function padLeft(s) {
|
||||
if (s.length >= refLen) return s;
|
||||
var z = new Array(refLen - s.length); for (var _i = 0; _i < z.length; _i++) z[_i] = 0;
|
||||
return z.concat(s);
|
||||
}
|
||||
|
||||
/* Compute total as element-wise sum of per-interface rate series.
|
||||
This avoids the cumulative-counter alignment bug where timestamps
|
||||
from different interfaces interleave, causing negative/zero rates. */
|
||||
var totalDnP = new Array(refLen);
|
||||
var totalUpP = new Array(refLen);
|
||||
for (var _k = 0; _k < refLen; _k++) { totalDnP[_k] = 0; totalUpP[_k] = 0; }
|
||||
st.ifaces.forEach(function(itf) {
|
||||
var aDn = padLeft(alignedDn[itf] || []);
|
||||
var aUp = padLeft(alignedUp[itf] || []);
|
||||
for (var _k = 0; _k < refLen; _k++) {
|
||||
totalDnP[_k] += (aDn[_k] || 0);
|
||||
totalUpP[_k] += (aUp[_k] || 0);
|
||||
}
|
||||
});
|
||||
|
||||
var totalDnStats = self._seriesStats(totalDnP);
|
||||
var totalUpStats = self._seriesStats(totalUpP);
|
||||
var maxDnAll = Math.max(totalDnStats.peak, 1);
|
||||
var maxUpAll = Math.max(totalUpStats.peak, 1);
|
||||
scDnAll = st.combinedDn.height / (maxDnAll * 1.15);
|
||||
scUpAll = st.combinedUp.height / (maxUpAll * 1.15);
|
||||
if (cDn.totalFill) cDn.totalFill.setAttribute('points', self._areaPoints(totalDnP, cDn.width, cDn.height, st.step, scDnAll));
|
||||
if (cDn.totalLine) cDn.totalLine.setAttribute('points', self._linePoints(totalDnP, cDn.width, cDn.height, st.step, scDnAll));
|
||||
if (cUp.totalFill) cUp.totalFill.setAttribute('points', self._areaPoints(totalUpP, cUp.width, cUp.height, st.step, scUpAll));
|
||||
if (cUp.totalLine) cUp.totalLine.setAttribute('points', self._linePoints(totalUpP, cUp.width, cUp.height, st.step, scUpAll));
|
||||
|
||||
st.ifaces.forEach(function(itf) {
|
||||
var dnSeries = padLeft(alignedDn[itf] || []);
|
||||
var upSeries = padLeft(alignedUp[itf] || []);
|
||||
var il = cDn.ifaceLines[itf];
|
||||
var ul = cUp.ifaceLines[itf];
|
||||
if (il) {
|
||||
if (il.fill) il.fill.setAttribute('points', self._areaPoints(dnSeries, cDn.width, cDn.height, st.step, scDnAll));
|
||||
if (il.line) il.line.setAttribute('points', self._linePoints(dnSeries, cDn.width, cDn.height, st.step, scDnAll));
|
||||
}
|
||||
if (ul) {
|
||||
if (ul.fill) ul.fill.setAttribute('points', self._areaPoints(upSeries, cUp.width, cUp.height, st.step, scUpAll));
|
||||
if (ul.line) ul.line.setAttribute('points', self._linePoints(upSeries, cUp.width, cUp.height, st.step, scUpAll));
|
||||
}
|
||||
});
|
||||
|
||||
/* per-interface stats update */
|
||||
st.ifaces.forEach(function(itf) {
|
||||
var sid = self._domSafeId(itf);
|
||||
var dnSer = padLeft(alignedDn[itf] || []);
|
||||
var upSer = padLeft(alignedUp[itf] || []);
|
||||
var dnSt = self._seriesStats(dnSer);
|
||||
var upSt = self._seriesStats(upSer);
|
||||
[
|
||||
{ id: 'total_dn_' + sid + '_cur', v: dnSt.current },
|
||||
{ id: 'total_dn_' + sid + '_avg', v: dnSt.average },
|
||||
{ id: 'total_dn_' + sid + '_peak', v: dnSt.peak },
|
||||
{ id: 'total_up_' + sid + '_cur', v: upSt.current },
|
||||
{ id: 'total_up_' + sid + '_avg', v: upSt.average },
|
||||
{ id: 'total_up_' + sid + '_peak', v: upSt.peak }
|
||||
].forEach(function(entry) {
|
||||
var el = document.getElementById(entry.id);
|
||||
if (el) el.innerHTML = self._bwLabel(entry.v, true);
|
||||
});
|
||||
});
|
||||
|
||||
[25, 50, 75].forEach(function(pct) {
|
||||
self._scaleLbl(cDn.id, pct, maxDnAll * 1.15);
|
||||
self._scaleLbl(cUp.id, pct, maxUpAll * 1.15);
|
||||
});
|
||||
|
||||
[
|
||||
{ id: 'total_dn_cur', v: totalDnStats.current },
|
||||
{ id: 'total_dn_avg', v: totalDnStats.average },
|
||||
{ id: 'total_dn_peak', v: totalDnStats.peak },
|
||||
{ id: 'total_up_cur', v: totalUpStats.current },
|
||||
{ id: 'total_up_avg', v: totalUpStats.average },
|
||||
{ id: 'total_up_peak', v: totalUpStats.peak }
|
||||
].forEach(function(entry) {
|
||||
var el = document.getElementById(entry.id);
|
||||
if (el) el.innerHTML = self._bwLabel(entry.v, true);
|
||||
});
|
||||
}
|
||||
}).catch(function() {});
|
||||
},
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Single-interface poll & render *
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
_pollIface: function() {
|
||||
var self = this;
|
||||
var st = this._state;
|
||||
|
||||
return callIfaceBandwidth(st.device).then(function(data) {
|
||||
if (!st || st.mode !== 'iface') return;
|
||||
if (!Array.isArray(data) || !data.length) return;
|
||||
|
||||
for (var i = (st.stamp ? 0 : 1); i < data.length; i++) {
|
||||
if (data[i][0] <= st.stamp) continue;
|
||||
if (i > 0) {
|
||||
var dt = data[i][0] - data[i-1][0];
|
||||
if (dt) {
|
||||
st.dataRx.push((data[i][1] - data[i-1][1]) / dt);
|
||||
st.dataTx.push((data[i][3] - data[i-1][3]) / dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
st.dataRx = st.dataRx.slice(-st.wanted);
|
||||
st.dataTx = st.dataTx.slice(-st.wanted);
|
||||
st.stamp = data[data.length-1][0];
|
||||
|
||||
var rxStats = self._seriesStats(st.dataRx);
|
||||
var txStats = self._seriesStats(st.dataTx);
|
||||
var maxRx = Math.max(rxStats.peak, 1);
|
||||
var maxTx = Math.max(txStats.peak, 1);
|
||||
var scRx = st.height / (maxRx * 1.15);
|
||||
var scTx = st.height / (maxTx * 1.15);
|
||||
|
||||
var dnFill = document.getElementById('bwsvg_dn_fill');
|
||||
var dnLine = document.getElementById('bwsvg_dn_line');
|
||||
var upFill = document.getElementById('bwsvg_up_fill');
|
||||
var upLine = document.getElementById('bwsvg_up_line');
|
||||
if (dnFill) dnFill.setAttribute('points', self._areaPoints(st.dataRx, st.width, st.height, st.step, scRx));
|
||||
if (dnLine) dnLine.setAttribute('points', self._linePoints(st.dataRx, st.width, st.height, st.step, scRx));
|
||||
if (upFill) upFill.setAttribute('points', self._areaPoints(st.dataTx, st.width, st.height, st.step, scTx));
|
||||
if (upLine) upLine.setAttribute('points', self._linePoints(st.dataTx, st.width, st.height, st.step, scTx));
|
||||
|
||||
[25, 50, 75].forEach(function(pct) {
|
||||
self._scaleLbl('bwsvg_dn', pct, maxRx * 1.15);
|
||||
self._scaleLbl('bwsvg_up', pct, maxTx * 1.15);
|
||||
});
|
||||
|
||||
[
|
||||
{ id: 'rx_bw_cur', v: rxStats.current },
|
||||
{ id: 'rx_bw_avg', v: rxStats.average },
|
||||
{ id: 'rx_bw_peak', v: rxStats.peak },
|
||||
{ id: 'tx_bw_cur', v: txStats.current },
|
||||
{ id: 'tx_bw_avg', v: txStats.average },
|
||||
{ id: 'tx_bw_peak', v: txStats.peak }
|
||||
].forEach(function(c) {
|
||||
var el = document.getElementById(c.id);
|
||||
if (el) el.innerHTML = self._bwLabel(c.v, true);
|
||||
});
|
||||
}).catch(function() {});
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
Reference in new issue
Block a user