diff --git a/filebrowser/Makefile b/filebrowser/Makefile index 3c420928e..2d4f6000b 100644 --- a/filebrowser/Makefile +++ b/filebrowser/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=filebrowser -PKG_VERSION:=1.2.3-stable +PKG_VERSION:=1.2.4-stable PKG_RELEASE=1 ifeq ($(ARCH),aarch64) diff --git a/luci-app-mptcp/Makefile b/luci-app-mptcp/Makefile index 7fe6a2590..6b3b16203 100644 --- a/luci-app-mptcp/Makefile +++ b/luci-app-mptcp/Makefile @@ -10,4 +10,11 @@ PKG_LICENSE:=GPLv3 #include $(TOPDIR)/feeds/luci/luci.mk include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-mptcp/install + $(call Package/luci-app-mptcp/install/default,$(1)) + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.mptcp \ + $(1)/usr/libexec/rpcd/luci.mptcp +endef + # call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_check.js b/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_check.js new file mode 100644 index 000000000..1f82b009f --- /dev/null +++ b/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_check.js @@ -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 +}); diff --git a/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_connections.js b/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_connections.js new file mode 100644 index 000000000..5feff6051 --- /dev/null +++ b/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_connections.js @@ -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 +}); diff --git a/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_fullmesh.js b/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_fullmesh.js new file mode 100644 index 000000000..470271375 --- /dev/null +++ b/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_fullmesh.js @@ -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 +}); diff --git a/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_monitor.js b/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_monitor.js new file mode 100644 index 000000000..b5efdf9ef --- /dev/null +++ b/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp_monitor.js @@ -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 +}); diff --git a/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/multipath.js b/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/multipath.js new file mode 100644 index 000000000..1206fcf58 --- /dev/null +++ b/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/multipath.js @@ -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 ? '
' : ' ') + + '(' + 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('\u25bc\u00a0' + _('Total') + ': ' + 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('' + (c ? c.label : itf) + ': ' + self._bwLabel(series[j] || 0, false)); + }); + return lines.length ? lines.join('
') : 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('\u25b2\u00a0' + _('Total') + ': ' + 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('' + (c ? c.label : itf) + ': ' + self._bwLabel(series[j] || 0, false)); + }); + return lines.length ? lines.join('
') : 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 '\u25bc\u00a0' + _('In') + ': ' + 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 '\u25b2\u00a0' + _('Out') + ': ' + 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 +}); diff --git a/luci-app-mptcp/luasrc/controller/mptcp.lua b/luci-app-mptcp/luasrc/controller/mptcp.lua deleted file mode 100644 index 07ba2d67b..000000000 --- a/luci-app-mptcp/luasrc/controller/mptcp.lua +++ /dev/null @@ -1,245 +0,0 @@ --- Copyright 2008 Steven Barth --- Copyright 2011 Jo-Philipp Wich --- Copyright 2018-2023 Ycarus (Yannick Chabanois) --- Licensed to the public under the Apache License 2.0. - -module("luci.controller.mptcp", package.seeall) - - -function index() - local uname = nixio.uname() - entry({"admin", "network", "mptcp"}, alias("admin", "network", "mptcp", "settings"), _("MPTCP")) - if uname ~= nil and uname.release:sub(1,1) == "5" then - entry({"admin", "network", "mptcp", "settings"}, cbi("mptcp"), _("Settings"),2).leaf = true - else - entry({"admin", "network", "mptcp", "settings"}, view("mptcp/mptcp"), _("Settings"),2).leaf = true - end - entry({"admin", "network", "mptcp", "bandwidth"}, template("mptcp/multipath"), _("Bandwidth"), 3).leaf = true - entry({"admin", "network", "mptcp", "multipath_bandwidth"}, call("multipath_bandwidth")).leaf = true - entry({"admin", "network", "mptcp", "interface_bandwidth"}, call("interface_bandwidth")).leaf = true - if uname ~= nil and uname.release:sub(1,1) == "5" then - entry({"admin", "network", "mptcp", "mptcp_check"}, template("mptcp/mptcp_check"), _("MPTCP Support Check"), 4).leaf = true - end - entry({"admin", "network", "mptcp", "mptcp_check_trace"}, post("mptcp_check_trace")).leaf = true - entry({"admin", "network", "mptcp", "mptcp_fullmesh"}, template("mptcp/mptcp_fullmesh"), _("MPTCP Fullmesh"), 5).leaf = true - entry({"admin", "network", "mptcp", "mptcp_fullmesh_data"}, post("mptcp_fullmesh_data")).leaf = true --- entry({"admin", "network", "mptcp", "mptcp_connections"}, template("mptcp/mptcp_connections"), _("Established connections"), 6).leaf = true - entry({"admin", "network", "mptcp", "mptcp_connections_data"}, post("mptcp_connections_data")).leaf = true - entry({"admin", "network", "mptcp", "mptcp_monitor"}, template("mptcp/mptcp_monitor"), _("MPTCP monitoring"), 6).leaf = true - entry({"admin", "network", "mptcp", "mptcp_monitor_data"}, post("mptcp_monitor_data")).leaf = true -end - -function interface_bandwidth(iface) - luci.http.prepare_content("application/json") - local bwc = io.popen("luci-bwc -i %q 2>/dev/null" % iface) - if bwc then - luci.http.write("[") - while true do - local ln = bwc:read("*l") - if not ln then break end - luci.http.write(ln) - end - luci.http.write("]") - bwc:close() - end -end - - -function string.split(input, delimiter) - input = tostring(input) - delimiter = tostring(delimiter) - if (delimiter=='') then return false end - local pos,arr = 0, {} - -- for each divider found - for st,sp in function() return string.find(input, delimiter, pos, true) end do - table.insert(arr, string.sub(input, pos, st - 1)) - pos = sp + 1 - end - table.insert(arr, string.sub(input, pos)) - return arr -end - -function multipath_bandwidth() - local result = { }; - local uci = luci.model.uci.cursor() - local res={ }; - local str=""; - local tmpstr=""; - - uci:foreach("network", "interface", function(s) - local intname = s[".name"] - local label = s["label"] - local dev = get_device(intname) - if dev == "" then - dev = get_device(s["device"]) - if dev == "" then - dev = get_device(s["ifname"]) - end - end - local multipath = s["multipath"] or "" - if dev ~= "lo" and dev ~= "" then - if multipath == "" then - multipath = uci:get("openmptcprouter", intname, "multipath") or "" - end - if multipath == "" then - multipath = "off" - end - if multipath == "on" or multipath == "master" or multipath == "backup" or multipath == "handover" then - local bwc = luci.sys.exec("luci-bwc -i %q 2>/dev/null" % dev) or "" - if bwc ~= nil then - --result[dev] = "[" .. string.gsub(bwc, '[\r\n]', '') .. "]" - if label ~= nil then - result[intname .. " (" .. label .. ")" ] = "[" .. string.gsub(bwc, '[\r\n]', '') .. "]" - else - result[intname] = "[" .. string.gsub(bwc, '[\r\n]', '') .. "]" - end - else - if label ~= nil then - result[intname .. " (" .. label .. ")" ] = "[]" - else - result[intname] = "[]" - end - end - end - end - end) - - res["total"]={ }; - for i=1,60 do - res["total"][i]={} - for j=1,5 do - res["total"][i][j]=0 - end - end - - for key,value in pairs(result) do - res[key]={} - value=(string.gsub(value, "^%[%[", "")) - value=(string.gsub(value, "%]%]", "")) - local temp1 = string.split(value, "],") - if temp1[2] ~= nil then - res[key][1]=temp1[1] - for i=2,60 do - res[key][i]={} - if temp1[i] ~= nil then - res[key][i]=(string.gsub(temp1[i], "%[", " ")) - end - end - for i=1,60 do - res[key][i] = string.split(res[key][i], ",") - for j=1,5 do - res[key][i][j]= tonumber(res[key][i][j]) - res["total"][i][j]= tonumber(res["total"][i][j]) - if j ==1 then - if res[key][i][j] ~= nil and res[key][i][j] > 0 then - res["total"][i][j] = res[key][i][j] - else - res["total"][i][j] = 0 - end - else - if res[key][i][j] ~= nil and res[key][i][j] > 0 then - res["total"][i][j] = res["total"][i][j] + res[key][i][j] - end - end - end - end - end - end - for i=1,60 do - for j=1,5 do - if "number"== type(res["total"][i][j]) then - res["total"][i][j]= tostring(res["total"][i][j]) - end - end - end - for i=1,60 do - if i == 60 then - tmpstr = "["..table.concat(res["total"][i], ",") - else - tmpstr = "["..table.concat(res["total"][i], ",").."]," - end - str = str..tmpstr - end - str = "["..str.."]]" - result["total"]=str - - luci.http.prepare_content("application/json") - luci.http.write_json(result) -end - -function get_device(interface) - local dump = require("luci.util").ubus("network.interface.%s" % interface, "status", {}) - if dump then - return dump['l3_device'] - else - return "" - end -end - -function mptcp_check_trace(iface) - luci.http.prepare_content("text/plain") - local tracebox - local uci = require "luci.model.uci".cursor() - local interface = get_device(iface) - local server = uci:get("shadowsocks-libev", "sss0", "server") or "" - if server == "" then return end - if interface == "" then - tracebox = io.popen("tracebox -s /usr/share/tracebox/omr-mptcp-trace.lua " .. server) - else - tracebox = io.popen("tracebox -s /usr/share/tracebox/omr-mptcp-trace.lua -i " .. interface .. " " .. server) - end - if tracebox then - while true do - local ln = tracebox:read("*l") - if not ln then break end - luci.http.write(ln) - luci.http.write("\n") - end - end - return -end - -function mptcp_fullmesh_data() - luci.http.prepare_content("text/plain") - local fullmesh - fullmesh = io.popen("multipath -f") - if fullmesh then - while true do - local ln = fullmesh:read("*l") - if not ln then break end - luci.http.write(ln) - luci.http.write("\n") - end - end - return -end - -function mptcp_monitor_data() - luci.http.prepare_content("text/plain") - local fullmesh - fullmesh = io.popen("multipath -m") - if fullmesh:read() ~= nil then - while true do - local ln = fullmesh:read("*l") - if not ln then break end - luci.http.write(ln) - luci.http.write("\n") - end - end - return -end - -function mptcp_connections_data() - luci.http.prepare_content("text/plain") - local connections - connections = io.popen("multipath -c") - if connections:read() ~= nil then - while true do - local ln = connections:read("*l") - if not ln then break end - luci.http.write(ln) - luci.http.write("\n") - end - end - return -end diff --git a/luci-app-mptcp/luasrc/model/cbi/mptcp.lua b/luci-app-mptcp/luasrc/model/cbi/mptcp.lua deleted file mode 100644 index dea854e80..000000000 --- a/luci-app-mptcp/luasrc/model/cbi/mptcp.lua +++ /dev/null @@ -1,200 +0,0 @@ -local net = require "luci.model.network".init() -local sys = require "luci.sys" -local ifaces = sys.net:devices() -local m, s, o -local uname = nixio.uname() - -m = Map("network", translate("MPTCP"), translate("Networks MPTCP settings.")) - -local unameinfo = nixio.uname() or { } - -s = m:section(TypedSection, "globals") -o = s:option(ListValue, "multipath", translate("Multipath TCP")) -o:value("enable", translate("enable")) -o:value("disable", translate("disable")) -o = s:option(ListValue, "mptcp_checksum", translate("Multipath TCP checksum")) -o:value(1, translate("enable")) -o:value(0, translate("disable")) - -if uname.release:sub(1,4) ~= "5.15" and uname.release:sub(1,1) ~= "6" then - o = s:option(ListValue, "mptcp_debug", translate("Multipath Debug")) - o:value(1, translate("enable")) - o:value(0, translate("disable")) -end - -o = s:option(ListValue, "mptcp_path_manager", translate("Multipath TCP path-manager"), translate("Default is fullmesh")) -o:value("default", translate("default")) -o:value("fullmesh", "fullmesh") ---if tonumber(uname.release:sub(1,4)) <= 5.15 then -if uname.release:sub(1,4) ~= "5.15" and uname.release:sub(1,1) ~= "6" then - o:value("ndiffports", "ndiffports") - o:value("binder", "binder") - if uname.release:sub(1,4) ~= "4.14" then - o:value("netlink", translate("Netlink")) - end -end -o = s:option(ListValue, "mptcp_scheduler", translate("Multipath TCP scheduler")) -o:value("default", translate("default")) --- if tonumber(uname.release:sub(1,4)) <= 5.15 then -if uname.release:sub(1,4) ~= "5.15" and uname.release:sub(1,1) ~= "6" then - o:value("roundrobin", "round-robin") - o:value("redundant", "redundant") - if uname.release:sub(1,4) ~= "4.14" then - o:value("blest", "BLEST") - o:value("ecf", "ECF") - end -end -if uname.release:sub(1,3) == "6.6" then - for dir in io.popen([[cd /usr/share/bpf/scheduler && ls -1 *.o | sed -e 's/.o//g' -e 's/mptcp_//g']]):lines() do - o:value(dir, dir) - end - -- bpf_burst => same as the default scheduler - -- bpf_red => sends all packets redundantly on all available subflows - -- bpf_first => always picks the first subflow to send data - -- bpf_rr => always picks the next available subflow to send data (round-robin) -end - --- if tonumber(uname.release:sub(1,4)) <= 5.15 then -if uname.release:sub(1,4) ~= "5.15" and uname.release:sub(1,1) ~= "6" then - o = s:option(Value, "mptcp_syn_retries", translate("Multipath TCP SYN retries")) - o.datatype = "uinteger" - o.rmempty = false -end --- if tonumber(uname.release:sub(1,4)) <= 5.15 then -if uname.release:sub(1,4) ~= "5.15" and uname.release:sub(1,1) ~= "6" then - o = s:option(ListValue, "mptcp_version", translate("Multipath TCP version")) - o:value(0, translate("0")) - o:value(1, translate("1")) - o.default = 0 -end -o = s:option(ListValue, "congestion", translate("Congestion Control"),translate("Default is bbr")) -local availablecong = sys.exec("sysctl -n net.ipv4.tcp_available_congestion_control | xargs -n1 | sort | xargs") -for cong in string.gmatch(availablecong, "[^%s]+") do - if cong == "bbr" and string.match(availablecong, "bbr1") then - o:value(cong, "bbr3") - else - o:value(cong, cong) - end -end - --- if tonumber(uname.release:sub(1,4)) >= 5.15 then -if uname.release:sub(1,4) == "5.15" or uname.release:sub(1,1) == "6" then - if uname.release:sub(1,1) == "6" then - -- Only available since 5.19 - o = s:option(ListValue, "mptcp_pm_type", translate("Path Manager type")) - o:value(0, translate("In-kernel path manager")) - o:value(1, translate("Userspace path manager")) - o.default = 0 - end - - o = s:option(ListValue, "mptcp_disable_initial_config", translate("Initial MPTCP configuration")) - o:depends("mptcp_pm_type",1) - o:value("0", translate("enable")) - o:value("1", translate("disable")) - o.default = "0" - - o = s:option(ListValue, "mptcp_force_multipath", translate("Force Multipath configuration")) - o:depends("mptcp_pm_type",1) - o:value("1", translate("enable")) - o:value("0", translate("disable")) - o.default = "1" - - o = s:option(ListValue, "mptcpd_enable", translate("Enable MPTCPd")) - o:depends("mptcp_pm_type",1) - o:value("enable", translate("enable")) - o:value("disable", translate("disable")) - o.default = "disable" - - o = s:option(DynamicList, "mptcpd_path_manager", translate("MPTCPd path managers")) - for dir in io.popen([[cd /usr/lib/mptcpd && ls -1 *.so | sed 's/.so//g']]):lines() do - o:value(dir, dir) - end - o:depends("mptcp_pm_type",1) - - o = s:option(DynamicList, "mptcpd_plugins", translate("MPTCPd plugins")) - for dir in io.popen([[cd /usr/lib/mptcpd && ls -1 *.so | sed 's/.so//g']]):lines() do - o:value(dir, dir) - end - o:depends("mptcp_pm_type",1) - - o = s:option(DynamicList, "mptcpd_addr_flags", translate("MPTCPd Address annoucement flags")) - o:value("subflow","subflow") - o:value("signal","signal") - o:value("backup","backup") - o:value("fullmesh","fullmesh") - o:depends("mptcp_pm_type",1) - - o = s:option(DynamicList, "mptcpd_notify_flags", translate("MPTCPd Address notification flags")) - o:value("existing","existing") - o:value("skip_link_local","skip_link_local") - o:value("skip_loopback","skip_loopback") - o:depends("mptcp_pm_type",1) - - o = s:option(Value, "mptcp_subflows", translate("Max subflows"),translate("specifies the maximum number of additional subflows allowed for each MPTCP connection")) - o.datatype = "uinteger" - o.rmempty = false - o.default = 3 - - o = s:option(Value, "mptcp_stale_loss_cnt", translate("Retranmission intervals"),translate("The number of MPTCP-level retransmission intervals with no traffic and pending outstanding data on a given subflow required to declare it stale. A low stale_loss_cnt value allows for fast active-backup switch-over, an high value maximize links utilization on edge scenarios e.g. lossy link with high BER or peer pausing the data processing.")) - o.datatype = "uinteger" - o.rmempty = false - o.default = 4 - - o = s:option(Value, "mptcp_add_addr_accepted", translate("Max add address"),translate("specifies the maximum number of ADD_ADDR (add address) suboptions accepted for each MPTCP connection")) - o.datatype = "uinteger" - o.rmempty = false - o.default = 1 - - o = s:option(Value, "mptcp_add_addr_timeout", translate("Control message timeout"),translate("Set the timeout after which an ADD_ADDR (add address) control message will be resent to an MPTCP peer that has not acknowledged a previous ADD_ADDR message.")) - o.datatype = "uinteger" - o.rmempty = false - o.default = 120 - -else - o = s:option(Value, "mptcp_fullmesh_num_subflows", translate("Fullmesh subflows for each pair of IP addresses")) - o.datatype = "uinteger" - o.rmempty = false - o.default = 1 - --o:depends("mptcp_path_manager","fullmesh") - - o = s:option(ListValue, "mptcp_fullmesh_create_on_err", translate("Re-create fullmesh subflows after a timeout")) - o:value(1, translate("enable")) - o:value(0, translate("disable")) - --o:depends("mptcp_path_manager","fullmesh") - - o = s:option(Value, "mptcp_ndiffports_num_subflows", translate("ndiffports subflows number")) - o.datatype = "uinteger" - o.rmempty = false - o.default = 1 - --o:depends("mptcp_path_manager","ndiffports") - - o = s:option(ListValue, "mptcp_rr_cwnd_limited", translate("Fill the congestion window on all subflows for round robin")) - o:value("Y", translate("enable")) - o:value("N", translate("disable")) - o.default = "Y" - --o:depends("mptcp_scheduler","roundrobin") - - o = s:option(Value, "mptcp_rr_num_segments", translate("Consecutive segments that should be sent for round robin")) - o.datatype = "uinteger" - o.rmempty = false - o.default = 1 - --o:depends("mptcp_scheduler","roundrobin") -end - -s = m:section(TypedSection, "interface", translate("Interfaces Settings")) -function s.filter(self, section) - return not section:match("^oip.*") and not section:match("^lo.*") and section ~= "omrvpn" and section ~= "omr6in4" -end -o = s:option(ListValue, "multipath", translate("Multipath TCP"), translate("One interface must be set as master")) -o:value("on", translate("enabled")) -o:value("off", translate("disabled")) -o:value("master", translate("master")) -o:value("backup", translate("backup")) ---o:value("handover", translate("handover")) -o.default = "off" - -function m.on_after_apply(self,map) - sys.call('/etc/init.d/mptcp reload') -end - -return m diff --git a/luci-app-mptcp/luasrc/view/mptcp/mptcp_check.htm b/luci-app-mptcp/luasrc/view/mptcp/mptcp_check.htm deleted file mode 100644 index 7cd2a7645..000000000 --- a/luci-app-mptcp/luasrc/view/mptcp/mptcp_check.htm +++ /dev/null @@ -1,75 +0,0 @@ -<%+header%> - -<% - local uci = require("luci.model.uci").cursor() - local sys = require "luci.sys" - local ifaces = sys.net:devices() - local net = require "luci.model.network".init() -%> - - - - -<% if stderr and #stderr > 0 then %>
<%=pcdata(stderr)%>
<% end %> -
-
-

<%:MPTCP Support Check%>

-
<%:Check if MPTCP between interface and server is working.%>
-
- <%:Settings%> -
-
- -
- -
-
- -
-
-
- -
- -
- <%:If you get "TCPOptionMPTCPCapable [...] Sender's Key" at the end, then MPTCP is supported. If there is a "-TCPOptionMPTCPCapable", then it's blocked.%> -
-<%+footer%> diff --git a/luci-app-mptcp/luasrc/view/mptcp/mptcp_connections.htm b/luci-app-mptcp/luasrc/view/mptcp/mptcp_connections.htm deleted file mode 100644 index d155e06a3..000000000 --- a/luci-app-mptcp/luasrc/view/mptcp/mptcp_connections.htm +++ /dev/null @@ -1,49 +0,0 @@ -<%+header%> - -<% - local uci = require("luci.model.uci").cursor() - local sys = require "luci.sys" - local ifaces = sys.net:devices() - local net = require "luci.model.network".init() -%> - - - - -<% if stderr and #stderr > 0 then %>
<%=pcdata(stderr)%>
<% end %> -
- - -
-<%+footer%> diff --git a/luci-app-mptcp/luasrc/view/mptcp/mptcp_fullmesh.htm b/luci-app-mptcp/luasrc/view/mptcp/mptcp_fullmesh.htm deleted file mode 100644 index e8600b9db..000000000 --- a/luci-app-mptcp/luasrc/view/mptcp/mptcp_fullmesh.htm +++ /dev/null @@ -1,49 +0,0 @@ -<%+header%> - -<% - local uci = require("luci.model.uci").cursor() - local sys = require "luci.sys" - local ifaces = sys.net:devices() - local net = require "luci.model.network".init() -%> - - - - -<% if stderr and #stderr > 0 then %>
<%=pcdata(stderr)%>
<% end %> -
- - -
-<%+footer%> diff --git a/luci-app-mptcp/luasrc/view/mptcp/mptcp_monitor.htm b/luci-app-mptcp/luasrc/view/mptcp/mptcp_monitor.htm deleted file mode 100644 index ec4eb5606..000000000 --- a/luci-app-mptcp/luasrc/view/mptcp/mptcp_monitor.htm +++ /dev/null @@ -1,42 +0,0 @@ -<%+header%> - - - - -<% if stderr and #stderr > 0 then %>
<%=pcdata(stderr)%>
<% end %> -
- - -
-<%+footer%> diff --git a/luci-app-mptcp/luasrc/view/mptcp/multipath.htm b/luci-app-mptcp/luasrc/view/mptcp/multipath.htm deleted file mode 100644 index b34dbe663..000000000 --- a/luci-app-mptcp/luasrc/view/mptcp/multipath.htm +++ /dev/null @@ -1,954 +0,0 @@ -<% --- Copyright 2010 Jo-Philipp Wich (jow@openwrt.org) --- Copyright 2015 OVH (OverTheBox@ovh.net) --- Simon Lelievre (simon.lelievre@corp.ovh.com) --- Sebastien Duponcheel (sebastien.duponcheel@ovh.net) --- Copyright 2018-2020 Ycarus - Yannick Chabanois (ycarus@zugaina.org) for OpenMPTCProuter --- --- Part of this file come from OverTheBox for OpenWrt. --- --- OverTheBox is free software: you can redistribute it and/or modify --- it under the terms of the GNU General Public License as published by --- the Free Software Foundation, either version 3 of the License, or --- (at your option) any later version. --- --- OverTheBox is distributed in the hope that it will be useful, --- but WITHOUT ANY WARRANTY; without even the implied warranty of --- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --- GNU General Public License for more details. --- --- You should have received a copy of the GNU General Public License --- along with OverTheBox. If not, see (http://www.gnu.org/licenses/) --%> -<%- - local ntm = require "luci.model.network".init() - local uci = require "luci.model.uci".cursor() - - function get_device(interface) - local dump = require("luci.util").ubus("network.interface.%s" % interface, "status", {}) - if dump and dump['l3_device'] ~= "" then - return dump['l3_device'] - else - return interface - end - end - - local dev - local devices = { "all" } - for _, dev in ipairs(ntm:get_networks()) do - --if dev ~= "lo" and not ntm:ignore_interface(dev) then - local multipath = uci:get("network", dev:name(), "multipath") - if multipath == "on" or multipath == "master" or multipath == "backup" or multipath == "handover" then - devices[#devices+1] = dev:name() - end - --end - end - - local curifname = luci.http.formvalue("dev") or "all" - local bandwidthtotalurl = "admin/network/mptcp/multipath_bandwidth" - local bandwidthintfurl = "admin/network/mptcp/interface_bandwidth" - --%> - -<%+header%> - - - - - -

<%:Realtime Traffic%>

- -
    - <% for _, dev in ipairs(devices) do - local ifname = get_device(dev) - local label = uci:get("network",dev,"label") - if label ~= nil then - %> -
  • "><%=label%>
  • - <% else %> -
  • "><%=pcdata(dev)%>
  • - <% end %> - <% end %> -
- -<% if curifname == "all" then %> - -<%:Download:%> - -
-
- -
- -<%:Upload:%> - -
-
- -
-
- -<% else %> - - -
-
-
- - - - - - - - - - - - - - - - - - - - - - -
<%:Inbound:%>0 <%:kbit/s%>
(0 <%:kB/s%>)
<%:Average:%>0 <%:kbit/s%>
(0 <%:kB/s%>)
<%:Peak:%>0 <%:kbit/s%>
(0 <%:kB/s%>)
<%:Outbound:%>0 <%:kbit/s%>
(0 <%:kB/s%>)
<%:Average:%>0 <%:kbit/s%>
(0 <%:kB/s%>)
<%:Peak:%>0 <%:kbit/s%>
(0 <%:kB/s%>)
- -<% end %> - - -<%+footer%> diff --git a/luci-app-mptcp/root/usr/libexec/rpcd/luci.mptcp b/luci-app-mptcp/root/usr/libexec/rpcd/luci.mptcp new file mode 100755 index 000000000..602ab5052 --- /dev/null +++ b/luci-app-mptcp/root/usr/libexec/rpcd/luci.mptcp @@ -0,0 +1,245 @@ +#!/usr/bin/env lua + +-- Copyright 2018-2023 Ycarus (Yannick Chabanois) +-- Licensed to the public under the Apache License 2.0. +-- +-- rpcd script exposing MPTCP bandwidth and diagnostic data as UBUS/RPC methods, +-- replacing the legacy Lua HTTP controller handlers. + +local jsonc = require "luci.jsonc" +local uci = require "luci.model.uci".cursor() + +-- ------------------------------------------------------------------------- +-- Helpers +-- ------------------------------------------------------------------------- + +local function get_device(interface) + if not interface or interface == "" then return "" end + local dump = require("luci.util").ubus("network.interface." .. interface, "status", {}) + if dump and dump["l3_device"] then + return dump["l3_device"] + end + return "" +end + +local function exec_output(cmd) + local lines = {} + local f = io.popen(cmd) + if f then + for line in f:lines() do + lines[#lines + 1] = line + end + f:close() + end + return table.concat(lines, "\n") +end + +-- Returns an array of [ts, rx_bytes, rx_pkts, tx_bytes, tx_pkts] rows from luci-bwc. +-- luci-bwc outputs one "[ ts, rx, rxp, tx, txp ]," entry per line (no outer brackets). +-- The trailing comma is determined by file position, not by whether the next entry is +-- non-zero: when the circular buffer is partially filled the last valid row still gets +-- a trailing comma. Strip it before wrapping with "[...]" to produce valid JSON. +local function bwc_rows(dev) + local f = io.popen(string.format("luci-bwc -i %q 2>/dev/null", dev)) + if not f then return {} end + local output = f:read("*all") + f:close() + if not output or output:match("^%s*$") then return {} end + output = output:gsub(",%s*$", "") + local ok, rows = pcall(jsonc.parse, "[" .. output .. "]") + if ok and type(rows) == "table" then return rows end + return {} +end + +-- ------------------------------------------------------------------------- +-- RPC methods +-- ------------------------------------------------------------------------- + +local methods = { + + -- Returns per-interface bandwidth arrays plus a "total" entry. + -- Each value is an array of [ts, rx_bytes, rx_pkts, tx_bytes, tx_pkts]. + multipath_bandwidth = { + call = function() + local result = {} + local iface_rows = {} -- interface -> rows (only non-empty) + + uci:foreach("network", "interface", function(s) + local intname = s[".name"] + local dev = get_device(intname) + if dev == "" then dev = s["device"] or s["ifname"] or "" end + + local multipath = s["multipath"] or "" + if dev ~= "lo" and dev ~= "" then + if multipath == "" then + multipath = uci:get("openmptcprouter", intname, "multipath") or "" + end + if multipath == "" then multipath = "off" end + + if multipath == "on" or multipath == "master" or + multipath == "backup" or multipath == "handover" then + local rows = bwc_rows(dev) + result[intname] = rows + if #rows > 0 then + iface_rows[intname] = rows + end + end + end + end) + + -- Build a correctly aligned cumulative total using last-known-value + -- interpolation. The old approach summed raw cumulative counters at + -- each timestamp, but different interfaces record data at different + -- (interleaved) timestamps. When consecutive total entries came from + -- different interfaces, rate = (total[i] - total[i-1]) / dt could be + -- negative or zero even under real traffic. + -- + -- Fix: for every timestamp in the union of all interface timestamps, + -- use the latest known cumulative bytes from EACH interface (carry the + -- last seen value forward). This keeps the total monotonically + -- increasing so JS rate computation stays correct. + local ts_set = {} + local all_ts = {} + for _, rows in pairs(iface_rows) do + for _, row in ipairs(rows) do + local ts = row[1] + if not ts_set[ts] then + ts_set[ts] = true + all_ts[#all_ts + 1] = ts + end + end + end + table.sort(all_ts) + + -- One forward-scan pointer per interface + local iface_ptrs = {} + for itf, rows in pairs(iface_rows) do + iface_ptrs[itf] = { idx = 1, rows = rows } + end + + local iface_last = {} -- latest row seen so far per interface + local total_rows = {} + for _, ts in ipairs(all_ts) do + -- Advance each pointer up to (and including) this timestamp + for itf, ptr in pairs(iface_ptrs) do + while ptr.idx <= #ptr.rows and ptr.rows[ptr.idx][1] <= ts do + iface_last[itf] = ptr.rows[ptr.idx] + ptr.idx = ptr.idx + 1 + end + end + -- Sum latest known cumulative bytes from every interface + local total = { ts, 0, 0, 0, 0 } + for _, last_row in pairs(iface_last) do + for j = 2, 5 do + total[j] = total[j] + (last_row[j] or 0) + end + end + total_rows[#total_rows + 1] = total + end + + result["total"] = total_rows + return result + end + }, + + -- Returns bandwidth rows for a single device as { data: [...] }. + interface_bandwidth = { + args = { iface = "" }, + call = function(args) + return { data = bwc_rows(args.iface or "") } + end + }, + + -- Runs "multipath -f" and returns its output. + mptcp_fullmesh = { + call = function() + return { output = exec_output("multipath -f 2>/dev/null") } + end + }, + + -- Runs "multipath -m" and returns its output. + mptcp_monitor = { + call = function() + return { output = exec_output("multipath -m 2>/dev/null") } + end + }, + + -- Runs "multipath -c" and returns its output. + mptcp_connections = { + call = function() + return { output = exec_output("multipath -c 2>/dev/null") } + end + }, + + -- Runs tracebox against the configured VPN server via the given interface. + mptcp_check_trace = { + args = { iface = "" }, + call = function(args) + local iface = args.iface or "" + local interface = get_device(iface) + local server = uci:get("shadowsocks-libev", "sss0", "server") or "" + if server == "" then return { output = "" } end + local cmd + if interface == "" then + cmd = "tracebox -s /usr/share/tracebox/omr-mptcp-trace.lua " .. server + else + cmd = "tracebox -s /usr/share/tracebox/omr-mptcp-trace.lua -i " .. interface .. " " .. server + end + return { output = exec_output(cmd) } + end + }, +} + +-- ------------------------------------------------------------------------- +-- rpcd dispatch (same boilerplate used by all rpcd Lua scripts in this repo) +-- ------------------------------------------------------------------------- + +local function parseInput() + local parse = jsonc.new() + local done, err + while true do + local chunk = io.read(4096) + if not chunk then break end + if not done and not err then + done, err = parse:parse(chunk) + end + end + if not done then + print(jsonc.stringify({ error = err or "Incomplete input" })) + os.exit(1) + end + return parse:get() +end + +local function validateArgs(func, uargs) + local method = methods[func] + if not method then + print(jsonc.stringify({ error = "Method not found" })) + os.exit(1) + end + if type(uargs) ~= "table" then + print(jsonc.stringify({ error = "Invalid arguments" })) + os.exit(1) + end + uargs.ubus_rpc_session = nil + local margs = method.args or {} + for k, v in pairs(uargs) do + if margs[k] == nil or (v ~= nil and type(v) ~= type(margs[k])) then + print(jsonc.stringify({ error = "Invalid arguments" })) + os.exit(1) + end + end + return method +end + +if arg[1] == "list" then + local rv = {} + for name, method in pairs(methods) do rv[name] = method.args or {} end + print((jsonc.stringify(rv):gsub(":%[%]", ":{}"))) +elseif arg[1] == "call" then + local args = parseInput() + local method = validateArgs(arg[2], args) + local result, code = method.call(args) + print((jsonc.stringify(result):gsub("^%[%]$", "{}"))) + os.exit(code or 0) +end diff --git a/luci-app-mptcp/root/usr/share/luci/menu.d/luci-app-mptcp.json b/luci-app-mptcp/root/usr/share/luci/menu.d/luci-app-mptcp.json index a3e457424..80e5bfc4b 100644 --- a/luci-app-mptcp/root/usr/share/luci/menu.d/luci-app-mptcp.json +++ b/luci-app-mptcp/root/usr/share/luci/menu.d/luci-app-mptcp.json @@ -1,10 +1,65 @@ { "admin/network/mptcp": { + "title": "MPTCP", + "order": 1, + "action": { + "type": "alias", + "path": "admin/network/mptcp/mptcp" + }, + "depends": { + "acl": [ "luci-app-mptcp" ] + } + }, + "admin/network/mptcp/mptcp": { "title": "MPTCP", "order": 10, "action": { - "type": "cbi", - "path": "mptcp" + "type": "view", + "path": "mptcp/mptcp" + }, + "depends": { + "acl": [ "luci-app-mptcp" ] + } + }, + "admin/network/mptcp/bandwidth": { + "title": "Bandwidth", + "order": 20, + "action": { + "type": "view", + "path": "mptcp/multipath" + }, + "depends": { + "acl": [ "luci-app-mptcp" ] + } + }, + "admin/network/mptcp/mptcp_connections": { + "title": "Established connections", + "order": 40, + "action": { + "type": "view", + "path": "mptcp/mptcp_connections" + }, + "depends": { + "acl": [ "luci-app-mptcp" ] + } + }, + "admin/network/mptcp/mptcp_fullmesh": { + "title": "MPTCP Fullmesh", + "order": 50, + "action": { + "type": "view", + "path": "mptcp/mptcp_fullmesh" + }, + "depends": { + "acl": [ "luci-app-mptcp" ] + } + }, + "admin/network/mptcp/mptcp_monitor": { + "title": "MPTCP monitoring", + "order": 60, + "action": { + "type": "view", + "path": "mptcp/mptcp_monitor" }, "depends": { "acl": [ "luci-app-mptcp" ] diff --git a/luci-app-mptcp/root/usr/share/rpcd/acl.d/luci-app-mptcp.json b/luci-app-mptcp/root/usr/share/rpcd/acl.d/luci-app-mptcp.json index eaf813e8c..f8ef2f1f8 100644 --- a/luci-app-mptcp/root/usr/share/rpcd/acl.d/luci-app-mptcp.json +++ b/luci-app-mptcp/root/usr/share/rpcd/acl.d/luci-app-mptcp.json @@ -10,7 +10,16 @@ }, "ubus": { "file": [ "list", "exec" ], - "system": [ "board" ] + "system": [ "board" ], + "network.interface": [ "dump" ], + "luci.mptcp": [ + "multipath_bandwidth", + "interface_bandwidth", + "mptcp_fullmesh", + "mptcp_monitor", + "mptcp_connections", + "mptcp_check_trace" + ] } }, "write": { diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index 55ce50e83..b2494bfc3 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall -PKG_VERSION:=26.4.1 +PKG_VERSION:=26.4.3 PKG_RELEASE:=1 PKG_PO_VERSION:=$(PKG_VERSION) diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/node_config.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/node_config.lua index 380749082..6e39b1b3d 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/node_config.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/node_config.lua @@ -9,6 +9,9 @@ if not arg[1] or not m:get(arg[1]) then luci.http.redirect(m.redirect) end +fs = require "nixio.fs" +formvalue_key = "cbid." .. appname .. "." .. arg[1] .. "." + local header = Template(appname .. "/node_config/header") header.api = api header.section = arg[1] @@ -61,12 +64,18 @@ o.write = function(self, section, value) m:set(section, self.option, value) end -local fs = api.fs local types_dir = "/usr/lib/lua/luci/model/cbi/passwall/client/type/" s.val = {} s.val["type"] = m.uci:get(appname, arg[1], "type") s.val["protocol"] = m.uci:get(appname, arg[1], "protocol") +if luci.http.formvalue("cbi.submit") == "1" then + local formvalue_type = luci.http.formvalue(formvalue_key .. "type") + if formvalue_type then + s.val["type"] = formvalue_type + end +end + o = s:option(ListValue, "type", translate("Type")) if api.is_finded("ipt2socks") then diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua index 24f295d69..3351ff020 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua @@ -25,7 +25,6 @@ local function _n(name) return option_prefix .. name end -local formvalue_key = "cbid." .. appname .. "." .. arg[1] .. "." local formvalue_proto = luci.http.formvalue(formvalue_key .. _n("protocol")) if formvalue_proto then s.val["protocol"] = formvalue_proto end diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua index e2164b82f..58a6a9ab2 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua @@ -25,7 +25,6 @@ local function _n(name) return option_prefix .. name end -local formvalue_key = "cbid." .. appname .. "." .. arg[1] .. "." local formvalue_proto = luci.http.formvalue(formvalue_key .. _n("protocol")) if formvalue_proto then s.val["protocol"] = formvalue_proto end diff --git a/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/luci-app-passwall/luasrc/passwall/util_sing-box.lua index e1029bea5..5ee76b87d 100644 --- a/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -527,7 +527,8 @@ function gen_outbound(flag, node, tag, proxy_table) ["User-Agent"] = node.user_agent } or nil, quic = node.naive_quic == "1" and true or false, - quic_congestion_control = (node.naive_quic == "1" and node.naive_congestion_control) and node.naive_congestion_control or nil + quic_congestion_control = (node.naive_quic == "1" and node.naive_congestion_control) and node.naive_congestion_control or nil, + tls = tls } end diff --git a/luci-app-passwall/luasrc/view/passwall/node_config/footer.htm b/luci-app-passwall/luasrc/view/passwall/node_config/footer.htm index ef8cf3458..4cb267313 100644 --- a/luci-app-passwall/luasrc/view/passwall/node_config/footer.htm +++ b/luci-app-passwall/luasrc/view/passwall/node_config/footer.htm @@ -2,8 +2,6 @@ local api = self.api -%>