router-ui: per-device traffic, last seen, and saner device saves

- nftables dynamic sets count bytes per LAN address; a 1-min tick folds
  them into totals that survive a ruleset reload
- devices page shows last seen + per-device down/up
- saving no longer pushes every DHCP lease into devices.toml, only rows
  with a reservation, note or block
- empty-state text on the traffic graphs instead of blank space

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-08-15 12:42:50 +01:00
parent 355e696c43
commit c00a1f85bc
4 changed files with 336 additions and 39 deletions

View file

@ -170,6 +170,33 @@ in
};
};
# Snapshot the nftables per-device counters and fold them into totals that
# outlive a ruleset reload. Root, because reading an nft set needs
# NET_ADMIN; the web app only ever reads the resulting file.
systemd.services.router-accounting = {
description = "Accumulate per-device traffic counters";
serviceConfig = {
Type = "oneshot";
ExecStart = "${pythonEnv}/bin/python3 ${../scripts/router-ui.py} tick";
# No StateDirectory: this unit is root, and StateDirectory would chown
# /var/lib/router-ui away from the router-ui user on every tick. The
# script mkdir -p's it, so a cold start before the web app is fine.
};
environment = {
ROUTER_UI_STATE = stateDir;
DNSMASQ_LEASES = "/var/lib/dnsmasq/dnsmasq.leases";
NFT_BIN = "${pkgs.nftables}/bin/nft";
};
};
systemd.timers.router-accounting = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "2min";
OnUnitActiveSec = "1min";
};
};
systemd.services.router-speedtest = {
description = "Record a speedtest result";
serviceConfig = {