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

@ -175,6 +175,44 @@ in
}
'';
};
# Per-device byte counters for the router UI's traffic page.
#
# Dynamic sets keyed on the LAN address, so hosts appear on their own —
# nothing here needs to know which devices exist. prerouting/postrouting
# rather than forward, because forward misses LAN↔router traffic, and on
# this box that includes every Jellyfin stream. Priority -300 puts both
# chains ahead of NAT, so addresses are still the device's own.
#
# Counters reset whenever this table reloads (i.e. every switch that
# touches nftables). router-accounting.service snapshots them each
# minute and accumulates deltas into a file that survives, so the UI's
# totals are continuous even though these counters aren't.
tables.accounting = {
family = "ip";
content = ''
set up {
type ipv4_addr
flags dynamic
counter
timeout 7d
}
set down {
type ipv4_addr
flags dynamic
counter
timeout 7d
}
chain pre {
type filter hook prerouting priority -300; policy accept;
iifname "eth0" update @up { ip saddr }
}
chain post {
type filter hook postrouting priority -300; policy accept;
oifname "eth0" update @down { ip daddr }
}
'';
};
# Use a distinct table name so we don't share `ip nat` with Docker —
# Docker manages its own DOCKER/PREROUTING chains in `ip nat`, and
# NixOS's nftables module rebuilds whichever tables it owns on every