mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-07 04:34:05 +08:00
261 lines
11 KiB
HTML
261 lines
11 KiB
HTML
<%
|
|
local http = require "luci.http"
|
|
local uci = require "luci.model.uci".cursor()
|
|
local jsonc = require "luci.jsonc"
|
|
local nsu = require "luci.netstat"
|
|
|
|
local have_vnstat = nsu.vnstat_installed()
|
|
local ifaces, db = nsu.db_interfaces()
|
|
local saved_mode = uci:get("netstats", "@config[0]", "mode") or "daily"
|
|
|
|
-- Requested interface, then the Settings choice / default-route device, as
|
|
-- long as vnStat has a database entry for it; else the first one it has.
|
|
local cur_iface = nsu.resolve_device(http.formvalue("iface"), db)
|
|
or nsu.wan_device(db)
|
|
or ifaces[1] or ""
|
|
|
|
local view_mode = http.formvalue("mode") or saved_mode
|
|
if view_mode ~= "daily" and view_mode ~= "monthly" then view_mode = "daily" end
|
|
|
|
local title = view_mode == "monthly" and translate("Monthly") or translate("Daily")
|
|
|
|
-- Raw vnStat JSON, parsed in the browser by netstat/vnstat-parse.js.
|
|
local raw = ""
|
|
if cur_iface ~= "" then
|
|
raw = nsu.vnstat_json_raw(view_mode == "monthly" and "m" or "d",
|
|
view_mode == "monthly" and 12 or 30, cur_iface) or ""
|
|
end
|
|
local raw_js = (jsonc.stringify(raw) or '""'):gsub("</", "<\\/")
|
|
%>
|
|
|
|
<link rel="stylesheet" href="/luci-static/resources/netstat/netstat-usage.css" />
|
|
|
|
<div class="netstat-dashboard">
|
|
<p class="netstat-intro">
|
|
<%:Usage is displayed using the Interface and View Mode selected in the Settings tab.%>
|
|
</p>
|
|
|
|
<% if have_vnstat and cur_iface ~= "" then %>
|
|
|
|
<div id="netstat-usage-data" style="display:none">
|
|
<div class="netstat-summary">
|
|
<div class="netstat-summary-item">
|
|
<span class="netstat-summary-dot rx"></span>
|
|
<span class="netstat-summary-label"><%:Download%>:</span>
|
|
<strong id="netstat-total-rx">-</strong>
|
|
</div>
|
|
<div class="netstat-summary-item">
|
|
<span class="netstat-summary-dot tx"></span>
|
|
<span class="netstat-summary-label"><%:Upload%>:</span>
|
|
<strong id="netstat-total-tx">-</strong>
|
|
</div>
|
|
<div class="netstat-summary-item netstat-summary-total">
|
|
<span class="netstat-summary-label"><%:Total%>:</span>
|
|
<strong id="netstat-total-all">-</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="netstat-chart-wrap">
|
|
<div class="netstat-chart-header">
|
|
<span class="netstat-chart-title"><%=title%> <%:Usage%></span>
|
|
<span class="netstat-chart-badge"><%=cur_iface%></span>
|
|
</div>
|
|
<div class="netstat-chart-box"><canvas id="trafficChart"></canvas></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="netstat-usage-empty" class="netstat-empty" style="display:none">
|
|
<span class="netstat-empty-icon">📊</span>
|
|
<%:No data available for%> <strong><%=cur_iface%></strong>
|
|
<div class="netstat-hint">
|
|
<%:vnStat may still be collecting its first data. It saves to its database every few minutes, try again shortly.%>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/luci-static/resources/netstat/chart.js"></script>
|
|
<script src="/luci-static/resources/netstat/chartjs-plugin-datalabels"></script>
|
|
<script src="/luci-static/resources/netstat/vnstat-parse.js"></script>
|
|
<script>
|
|
(function () {
|
|
// Chart text follows the theme's own text colour, so it is readable
|
|
// on light and dark themes alike; grid lines are translucent grey.
|
|
var dashboard = document.querySelector('.netstat-dashboard');
|
|
function themeTextColor() {
|
|
return getComputedStyle(dashboard).color || '#888';
|
|
}
|
|
var textColor = themeTextColor();
|
|
var gridColor = 'rgba(128,128,128,0.2)';
|
|
|
|
var usage = window.netstatVnstat.series(<%=raw_js%>, '<%=cur_iface%>', '<%=view_mode%>');
|
|
if (!usage.entries) {
|
|
document.getElementById('netstat-usage-empty').style.display = '';
|
|
return;
|
|
}
|
|
document.getElementById('netstat-usage-data').style.display = '';
|
|
|
|
var labels = usage.labels;
|
|
var rxData = usage.rx;
|
|
var txData = usage.tx;
|
|
|
|
// Height follows the number of rows; width follows the container
|
|
// (Chart.js responsive mode), so the chart never overflows when the
|
|
// theme's sidebar appears or the window is resized.
|
|
var BAR_H = 48;
|
|
var canvas = document.getElementById('trafficChart');
|
|
canvas.parentElement.style.height =
|
|
Math.min(Math.max(labels.length * BAR_H, 280), 700) + 'px';
|
|
|
|
var ctx = canvas.getContext('2d');
|
|
|
|
function rxGradient(c) {
|
|
var area = c.chart.chartArea;
|
|
if (!area) return '#4ade80';
|
|
var g = c.chart.ctx.createLinearGradient(area.left, 0, area.right, 0);
|
|
g.addColorStop(0, '#86efac');
|
|
g.addColorStop(1, '#15803d');
|
|
return g;
|
|
}
|
|
|
|
function fmtBytes(v) {
|
|
if (v >= 1099511627776) return (v/1099511627776).toFixed(2) + ' TB';
|
|
if (v >= 1073741824) return (v/1073741824).toFixed(2) + ' GB';
|
|
if (v >= 1048576) return (v/1048576).toFixed(2) + ' MB';
|
|
if (v >= 1024) return (v/1024).toFixed(2) + ' KB';
|
|
return v + ' B';
|
|
}
|
|
|
|
document.getElementById('netstat-total-rx').textContent = fmtBytes(usage.totalRx);
|
|
document.getElementById('netstat-total-tx').textContent = fmtBytes(usage.totalTx);
|
|
document.getElementById('netstat-total-all').textContent = fmtBytes(usage.totalRx + usage.totalTx);
|
|
|
|
var maxVal = Math.max.apply(null, rxData.concat(txData).concat([1]));
|
|
|
|
var chart = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [
|
|
{
|
|
label: '<%:Download (RX)%>',
|
|
data: rxData,
|
|
backgroundColor: rxGradient,
|
|
borderRadius: 4,
|
|
borderSkipped: false,
|
|
datalabels: {
|
|
color: '#fff',
|
|
anchor: 'center',
|
|
align: 'center',
|
|
font: { size: 11, weight: '600' },
|
|
formatter: fmtBytes,
|
|
display: function(ctx) {
|
|
return maxVal > 0 && ctx.dataset.data[ctx.dataIndex] / maxVal > 0.08;
|
|
}
|
|
}
|
|
},
|
|
{
|
|
label: '<%:Upload (TX)%>',
|
|
data: txData,
|
|
backgroundColor: '#f59e0b',
|
|
borderRadius: 4,
|
|
borderSkipped: false,
|
|
datalabels: {
|
|
color: textColor,
|
|
anchor: 'end',
|
|
align: 'right',
|
|
font: { size: 11, weight: '600' },
|
|
formatter: fmtBytes,
|
|
display: function(ctx) {
|
|
return maxVal > 0 && ctx.dataset.data[ctx.dataIndex] / maxVal > 0.02;
|
|
}
|
|
}
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
indexAxis: 'y',
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: { duration: 400 },
|
|
plugins: {
|
|
legend: {
|
|
position: 'top',
|
|
labels: {
|
|
color: textColor,
|
|
usePointStyle: true,
|
|
pointStyle: 'circle',
|
|
padding: 16
|
|
}
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(ctx) { return ' ' + fmtBytes(ctx.raw); }
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
grid: { color: gridColor },
|
|
ticks: {
|
|
color: textColor,
|
|
callback: fmtBytes,
|
|
maxTicksLimit: 6
|
|
}
|
|
},
|
|
y: {
|
|
grid: { display: false },
|
|
ticks: { color: textColor, autoSkip: false }
|
|
}
|
|
}
|
|
},
|
|
plugins: [ChartDataLabels]
|
|
});
|
|
|
|
// Recolour when the theme switches between light and dark without a
|
|
// page reload (themes toggle a class or inline style on <html>/<body>).
|
|
function recolour() {
|
|
var c = themeTextColor();
|
|
if (c === textColor) return;
|
|
textColor = c;
|
|
chart.options.plugins.legend.labels.color = c;
|
|
chart.options.scales.x.ticks.color = c;
|
|
chart.options.scales.y.ticks.color = c;
|
|
chart.data.datasets[1].datalabels.color = c;
|
|
chart.update('none');
|
|
}
|
|
// Themes may animate the colour change (e.g. "transition: all 0.2s"),
|
|
// so check again once it has settled and whenever a colour
|
|
// transition finishes.
|
|
function recolourSoon() {
|
|
setTimeout(recolour, 50);
|
|
setTimeout(recolour, 500);
|
|
}
|
|
document.addEventListener('transitionend', function (ev) {
|
|
if (ev.propertyName === 'color') recolour();
|
|
});
|
|
if (window.MutationObserver) {
|
|
var mo = new MutationObserver(recolourSoon);
|
|
[document.documentElement, document.body].forEach(function (el) {
|
|
mo.observe(el, { attributes: true, attributeFilter: ['class', 'style', 'data-theme', 'data-darkmode'] });
|
|
});
|
|
var darkStyle = document.getElementById('dark-theme-style');
|
|
if (darkStyle) mo.observe(darkStyle, { childList: true, characterData: true, subtree: true });
|
|
}
|
|
})();
|
|
</script>
|
|
|
|
<% elseif not have_vnstat then %>
|
|
<div class="netstat-empty">
|
|
<span class="netstat-empty-icon">📡</span>
|
|
<%:vnStat is not installed. Install the vnstat2 package to record traffic history.%>
|
|
</div>
|
|
<% else %>
|
|
<div class="netstat-empty">
|
|
<span class="netstat-empty-icon">📡</span>
|
|
<%:vnStat is not tracking any interface yet.%>
|
|
<div class="netstat-hint">
|
|
<%:Interfaces are added automatically when they come up. You can also run "/usr/libexec/netstat-vnstat-track" on the router.%>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|