From f858d3f14710697e1dcb93429fe13ca7712348fc Mon Sep 17 00:00:00 2001 From: kiddin9 <48883331+kiddin9@users.noreply.github.com> Date: Wed, 11 Nov 2026 15:21:02 +0800 Subject: [PATCH] Update dashboard.patch --- .github/diy/patches/dashboard.patch | 382 +--------------------------- 1 file changed, 1 insertion(+), 381 deletions(-) diff --git a/.github/diy/patches/dashboard.patch b/.github/diy/patches/dashboard.patch index 0206ba6e7..7cf594d48 100644 --- a/.github/diy/patches/dashboard.patch +++ b/.github/diy/patches/dashboard.patch @@ -44,387 +44,7 @@ } .dashboard-donut svg { -@@ -349,8 +349,8 @@ - - .dashboard-bars-stack i.dashboard-bar-ok { background: var(--dashboard-success); } - .dashboard-bars-stack i.dashboard-bar-mid { background: var(--dashboard-warn); } --.dashboard-bars-stack i.dashboard-bar-up { background: var(--dashboard-series-1); } --.dashboard-bars-stack i.dashboard-bar-down { background: var(--dashboard-series-2); } -+.dashboard-bars-stack i.dashboard-bar-up { background: var(--dashboard-series-2); } -+.dashboard-bars-stack i.dashboard-bar-down { background: var(--dashboard-series-1); } - .dashboard-bars-stack i.dashboard-bar-none { background: var(--dashboard-border); } - - .dashboard-bars-label { -@@ -373,8 +373,8 @@ - - .dashboard-legend-inline i.dashboard-bar-ok { background: var(--dashboard-success); } - .dashboard-legend-inline i.dashboard-bar-mid { background: var(--dashboard-warn); } --.dashboard-legend-inline i.dashboard-bar-up { background: var(--dashboard-series-1); } --.dashboard-legend-inline i.dashboard-bar-down { background: var(--dashboard-series-2); } -+.dashboard-legend-inline i.dashboard-bar-up { background: var(--dashboard-series-2); } -+.dashboard-legend-inline i.dashboard-bar-down { background: var(--dashboard-series-1); } - - /* ---- Time series (shares the axis and grid of the bar chart) ---- */ - .dashboard-lines-plot { -@@ -422,6 +422,11 @@ - color: var(--dashboard-text); - } - -+/* ---- WAN traffic: one counter source per plot ---- */ -+.dashboard-traffic-source { -+ min-width: 0; -+} -+ - /* ---- Wireless network boxes (LuCI .ifacebox) ---- */ - .dashboard-net-grid { - display: grid; -diff --git a/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/include/15_traffic.js b/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/include/15_traffic.js -index 4f74e617467b..390f53300d57 100644 ---- a/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/include/15_traffic.js -+++ b/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/include/15_traffic.js -@@ -1,81 +1,86 @@ - 'use strict'; - 'require baseclass'; --'require network'; - 'require view.dashboard.lib.charts as charts'; - 'require view.dashboard.lib.history as history'; - - const NET = 4; - --return baseclass.extend({ -- title: _('WAN traffic'), -+function rate(cur, prev, seconds, name, index) { -+ const a = prev?.[NET]?.[name]; -+ const b = cur?.[NET]?.[name]; - -- widgets: [ -- { id: 'wan', slot: 'charts', title: _('WAN traffic'), order: 10 } -- ], -+ if (!Array.isArray(a) || !Array.isArray(b) || !Number.isFinite(seconds) || seconds <= 0) -+ return null; - -- load() { -- return Promise.all([ -- network.getWANNetworks(), -- network.getWAN6Networks(), -- history.load() -- ]); -- }, -+ // Older rpcd instances provide only two fields. Crossing to the new -+ // format, like a change of ifindex, starts a fresh pair of samples. -+ if (a[2] !== b[2] || a[2] === null || b[2] === null) -+ return null; - -- uplink(wan, wan6) { -- const pick = list => list.slice().sort((a, b) => a.getMetric() - b.getMetric())[0]; -- -- return pick(wan) || pick(wan6) || null; -- }, -+ for (let i = 0; i < 2; i++) -+ if (!Number.isFinite(a[i]) || !Number.isFinite(b[i]) || a[i] < 0 || b[i] < a[i]) -+ return null; - -- // Rates come from the byte counters of two samples and the time between -- // them on the router's clock. Counters that went backwards, as after the -- // interface was brought up again, give no rate and leave a gap. -- rates(name, index) { -- return history.series((cur, prev, seconds) => { -- const a = prev ? prev[NET][name] : null; -- const b = cur[NET][name]; -+ return (b[index] - a[index]) * 8 / seconds; -+} - -- if (a == null || b == null || b[0] < a[0] || b[1] < a[1]) -- return null; -+function selectedRate(cur, prev, seconds, index) { -+ const current = cur?.[5]?.devices, previous = prev?.[5]?.devices; -+ if (current?.length !== 1 || previous?.length !== 1 || current[0] !== previous[0]) -+ return null; -+ return rate(cur, prev, seconds, current[0], index); -+} - -- return (b[index] - a[index]) * 8 / seconds; -- }); -- }, -+function points(index) { -+ const points = history.series((cur, prev, seconds) => selectedRate(cur, prev, seconds, index)); -+ const last = points[points.length - 1]; -+ if (last && history.now - last.t > history.step * 2.5) -+ points.push({ t: history.now, v: null }); -+ return points; -+} - -- render([wan, wan6]) { -- const ifc = this.uplink(wan, wan6); -- const dev = ifc ? ifc.getL3Device() : null; -+return baseclass.extend({ -+ title: _('WAN traffic'), -+ widgets: [ -+ { id: 'wan', slot: 'charts', title: _('WAN traffic'), order: 10 } -+ ], - -- if (dev == null) -- return { charts: [ { id: 'wan', node: charts.card({ title: this.title, body: charts.empty(_('Not connected')) }) } ] }; -+ load() { -+ return history.load(); -+ }, - -- const down = this.rates(dev.getName(), 0); -- const up = this.rates(dev.getName(), 1); -+ render() { -+ const down = points(0); -+ const up = points(1); - const last = points => points.length ? points[points.length - 1].v : null; - const scale = charts.rateScale(Math.max(1, ...down.concat(up).map(p => p.v || 0))); -- const names = (ifc.getName() == dev.getName()) ? [ dev.getName() ] : [ ifc.getName(), dev.getName() ]; -+ const available = down.some(p => p.v != null) || up.some(p => p.v != null); -+ const current = last(down) != null && last(up) != null; -+ const sample = history.samples[history.samples.length - 1]; -+ const device = sample?.[5]?.devices?.[0]; -+ const body = []; -+ -+ if (available) { -+ body.push(charts.lines({ -+ span: history.span(), -+ max: scale.max, -+ ticks: scale.ticks, -+ ariaLabel: this.title, -+ series: [ { values: down, area: true }, { values: up, area: true } ] -+ }), charts.legend([ -+ { className: 'dashboard-series-1', label: _('Download'), value: scale.format(last(down)) }, -+ { className: 'dashboard-series-2', label: _('Upload'), value: scale.format(last(up)) } -+ ])); -+ } -+ if (!current) -+ body.push(charts.empty(history.failed || !sample?.[5]?.devices?.length -+ ? _('No data received') : available ? _('Waiting for a new sample...') : history.status())); - -- return { -- charts: [ { -- id: 'wan', -- node: charts.card({ -- title: this.title, -- desc: names.concat([ scale.unit ]).join(' · '), -- body: down.length ? [ -- charts.lines({ -- span: history.span(), -- max: scale.max, -- ticks: scale.ticks, -- ariaLabel: this.title, -- series: [ { values: down, area: true }, { values: up, area: true } ] -- }), -- charts.legend([ -- { className: 'dashboard-series-1', label: _('Down.'), value: charts.formatRate(last(down)) }, -- { className: 'dashboard-series-2', label: _('Up.'), value: charts.formatRate(last(up)) } -- ]) -- ] : charts.empty(history.status()) -- }) -- } ] -- }; -+ // No null children: older LuCI DOM helpers render them as literal text. -+ return { charts: [ { id: 'wan', node: charts.card({ -+ title: this.title, -+ desc: device ? device + ' · ' + scale.unit : scale.unit, -+ body: [ E('section', { 'class': 'dashboard-traffic-source' }, body) ] -+ }) } ] }; - } - }); -diff --git a/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/include/30_wifi.js b/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/include/30_wifi.js -index ebb772d35eee..4780c6289257 100644 ---- a/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/include/30_wifi.js -+++ b/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/include/30_wifi.js -@@ -165,17 +165,17 @@ return baseclass.extend({ - ticks: [ 0, 1, 2, 3, 4 ].map(n => ({ value: scale.step * n, label: scale.format(scale.step * n) })), - items: devices.map(device => ({ - label: device.hostname.value, -- title: '%s · %s %s · %s %s'.format(device.hostname.value, _('Up.'), device.transferred.value.rx, _('Down.'), device.transferred.value.tx) -+ title: '%s · %s %s · %s %s'.format(device.hostname.value, _('Download'), device.transferred.value.tx, _('Upload'), device.transferred.value.rx) - + (device.connected.value ? ' · %s %s'.format(_('Connected'), device.connected.value) : ''), - values: [ -- { value: device.transferred.value.bytes.rx, className: 'dashboard-bar-up' }, -- { value: device.transferred.value.bytes.tx, className: 'dashboard-bar-down' } -+ { value: device.transferred.value.bytes.tx, className: 'dashboard-bar-down' }, -+ { value: device.transferred.value.bytes.rx, className: 'dashboard-bar-up' } - ] - })) - }), - charts.legend([ -- { className: 'dashboard-bar-up', label: _('Up.') }, -- { className: 'dashboard-bar-down', label: _('Down.') } -+ { className: 'dashboard-bar-down', label: _('Download') }, -+ { className: 'dashboard-bar-up', label: _('Upload') } - ]) - ] : charts.empty(_('No wireless clients connected')) - }); -diff --git a/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/lib/charts.js b/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/lib/charts.js -index 96e629fa799c..6f0705af9c58 100644 ---- a/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/lib/charts.js -+++ b/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/lib/charts.js -@@ -36,13 +36,24 @@ function rateUnit(value) { - } - - return baseclass.extend({ -- formatRate(value) { -+ formatRate(value, targetExp) { - if (value == null) - return '-'; - -- const exp = rateUnit(value); -+ const exp = (targetExp != null) ? targetExp : rateUnit(value); -+ const unit = Math.pow(1000, exp); -+ const num = value / unit; -+ -+ if (num === 0) -+ return '0 %s'.format(RATE_UNITS[exp]); -+ -+ if (num > 0 && num < 0.01 && exp > 0) -+ return '< 0.01 %s'.format(RATE_UNITS[exp]); -+ -+ if (num < 1 && exp > 0) -+ return '%.2f %s'.format(num, RATE_UNITS[exp]); - -- return '%.1f %s'.format(value / Math.pow(1000, exp), RATE_UNITS[exp]); -+ return '%.1f %s'.format(num, RATE_UNITS[exp]); - }, - - rateScale(peak) { -@@ -55,7 +66,8 @@ return baseclass.extend({ - return { - max: step * 4 * unit, - unit: RATE_UNITS[exp], -- ticks: [ 0, 1, 2, 3, 4 ].map(n => ({ value: step * n * unit, label: String(+(step * n).toFixed(2)) })) -+ ticks: [ 0, 1, 2, 3, 4 ].map(n => ({ value: step * n * unit, label: String(+(step * n).toFixed(2)) })), -+ format: value => this.formatRate(value, exp) - }; - }, - -diff --git a/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/lib/history.js b/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/lib/history.js -index a198dbc02d6d..2a8bd6bd4d23 100644 ---- a/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/lib/history.js -+++ b/luci-mod-dashboard/htdocs/luci-static/resources/view/dashboard/lib/history.js -@@ -31,6 +31,7 @@ return baseclass.extend({ - this.samples = []; - - this.id = res.id; -+ this.now = res.now; - this.step = res.step; - this.slots = res.slots; - this.samples = this.samples.concat(res.samples).filter(sample => sample[TIME] > res.now - res.step * res.slots); -@@ -57,7 +58,7 @@ return baseclass.extend({ - }, - - near(prev, cur) { -- return (prev != null && cur[TIME] - prev[TIME] <= 2.5 * this.step); -+ return (prev != null && cur[TIME] > prev[TIME] && cur[TIME] - prev[TIME] <= 2.5 * this.step); - }, - - pair() { -diff --git a/luci-mod-dashboard/root/usr/share/rpcd/ucode/luci.dashboard b/luci-mod-dashboard/root/usr/share/rpcd/ucode/luci.dashboard -index d87679d9b027..e8b3131b0b17 100644 ---- a/luci-mod-dashboard/root/usr/share/rpcd/ucode/luci.dashboard -+++ b/luci-mod-dashboard/root/usr/share/rpcd/ucode/luci.dashboard -@@ -2,10 +2,58 @@ - - import { readfile } from 'fs'; - import * as uloop from 'uloop'; -+import { connect } from 'ubus'; - - const STEP = 5000, SLOTS = 60; - --let ring = [], seq = 0, timer; -+let ring = [], seq = 0, timer, selectedDevice; -+const bus = connect(); -+ -+// Observe one main-table default-route device. Prefer IPv4, then route metric; -+// keep the existing device on equal priority to avoid polling-order switches. -+function sources(interfaces, previous) { -+ let best = null; -+ for (let iface in interfaces) { -+ if (!iface.up) -+ continue; -+ let name = iface.device; -+ if (!name || name == 'lo' || substr(name, 0, 1) == '@') -+ name = iface.l3_device; -+ if (!name || name == 'lo' || substr(name, 0, 1) == '@') -+ continue; -+ -+ for (let route in iface.route ?? []) { -+ if (+route.mask != 0) -+ continue; -+ const family = route.target == '0.0.0.0' ? 4 : route.target == '::' ? 6 : 0; -+ if (!family) -+ continue; -+ // netifd omits route.table when it is inherited from the interface. -+ const table = route.table ?? (family == 4 ? iface.ip4table : iface.ip6table); -+ if (table && table != 254) -+ continue; -+ const metric = +(route.metric ?? iface.metric ?? 0); -+ if (!best || family < best.family || -+ (family == best.family && (metric < best.metric || -+ (metric == best.metric && (name == previous || -+ (best.device != previous && name < best.device)))))) -+ best = { device: name, family, metric }; -+ } -+ } -+ -+ // Use the configured carrier (including a VLAN) without walking to its -+ // physical parent, which may also carry LAN or IPTV traffic. -+ return best ? [ best.device ] : []; -+} -+ -+function traffic_boundary() { -+ const dump = bus.call('network.interface', 'dump'); -+ if (type(dump?.interface) != 'array') -+ return null; -+ const devices = sources(dump.interface, selectedDevice); -+ selectedDevice = devices[0]; -+ return { devices }; -+} - - function now() { - const c = clock(true); -@@ -19,7 +67,10 @@ const id = trim(readfile('/proc/sys/kernel/random/uuid')); - // Counters as the kernel has them. Whoever draws them takes the differences, - // over the time between two samples on the clock they were read by. - function sample() { -- const t = now(), cpus = {}, net = {}; -+ const cpus = {}, net = {}; -+ let boundary = null; -+ try { boundary = traffic_boundary(); } -+ catch (e) { warn(`luci.dashboard: traffic discovery: ${e}\n`); } - - for (let line in split(readfile('/proc/stat') ?? '', '\n')) { - if (substr(line, 0, 3) != 'cpu') -@@ -32,14 +83,26 @@ function sample() { - - const mem = match(readfile('/proc/meminfo', 256) ?? '', /MemAvailable:\s+(\d+)/); - -- for (let line in slice(split(readfile('/proc/net/dev') ?? '', '\n'), 2)) { -- const fields = split(trim(line), /[: ]+/); -- -- if (length(fields) > 9 && fields[0] != 'lo') -- net[fields[0]] = [ +fields[1], +fields[9] ]; -+ // Only the selected WAN consumes network history. Check its identity -+ // on both sides of the counter read to detect device recreation. -+ const name = boundary?.devices?.[0]; -+ const before = name ? int(readfile(`/sys/class/net/${name}/ifindex`)) : null; -+ const t = now(); -+ -+ if (name) { -+ for (let line in slice(split(readfile('/proc/net/dev') ?? '', '\n'), 2)) { -+ const fields = split(trim(line), /[: ]+/); -+ if (length(fields) <= 9 || fields[0] != name) -+ continue; -+ -+ const after = int(readfile(`/sys/class/net/${name}/ifindex`)); -+ net[name] = [ +fields[1], +fields[9], -+ after > 0 && after == before ? after : null ]; -+ break; -+ } - } - -- push(ring, [ ++seq, t, cpus, mem ? +mem[1] * 1024 : null, net ]); -+ push(ring, [ ++seq, t, cpus, mem ? +mem[1] * 1024 : null, net, boundary ]); + while (length(ring) > SLOTS) shift(ring);