⛄ Sync 2026-10-01 13:24:21

This commit is contained in:
github-actions[bot] committed 2026-10-01 13:24:21 +08:00
1 parent 52a998f86a
commit 9057fa94b1
6 files changed
+168 -82

No files matched your search

@@ -153,7 +153,7 @@
gap: 20px;
margin-block: auto;
/* Override Footstrap's negative SVG container margins to preserve card padding. */
margin-inline: 0;
margin-inline: 0 !important;
}
.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;
@@ -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;
function rate(cur, prev, seconds, name, index) {
const a = prev?.[NET]?.[name];
const b = cur?.[NET]?.[name];
if (!Array.isArray(a) || !Array.isArray(b) || !Number.isFinite(seconds) || seconds <= 0)
return null;
// 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;
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;
return (b[index] - a[index]) * 8 / seconds;
}
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);
}
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;
}
return baseclass.extend({
title: _('WAN traffic'),
widgets: [
{ id: 'wan', slot: 'charts', title: _('WAN traffic'), order: 10 }
],
load() {
return Promise.all([
network.getWANNetworks(),
network.getWAN6Networks(),
history.load()
]);
return history.load();
},
uplink(wan, wan6) {
const pick = list => list.slice().sort((a, b) => a.getMetric() - b.getMetric())[0];
return pick(wan) || pick(wan6) || 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];
if (a == null || b == null || b[0] < a[0] || b[1] < a[1])
return null;
return (b[index] - a[index]) * 8 / seconds;
});
},
render([wan, wan6]) {
const ifc = this.uplink(wan, wan6);
const dev = ifc ? ifc.getL3Device() : null;
if (dev == null)
return { charts: [ { id: 'wan', node: charts.card({ title: this.title, body: charts.empty(_('Not connected')) }) } ] };
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 = [];
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())
})
} ]
};
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()));
// 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) ]
}) } ] };
}
});
@@ -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'))
});
@@ -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;
return '%.1f %s'.format(value / Math.pow(1000, exp), RATE_UNITS[exp]);
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(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)
};
},
@@ -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() {