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

@ -129,6 +129,39 @@ entry = tomlkit.dumps(ddoc).split("[[device]]")[1]
assert "ip = " not in entry, entry
assert "blocked = true" in entry, entry
# --- accounting: deltas, and counter resets on ruleset reload ----------------
state = tempfile.mkdtemp()
r.TRAFFIC_FILE = os.path.join(state, "traffic.json")
r.LEASES = os.path.join(state, "leases")
open(r.LEASES, "w").write("999 aa:bb:cc:dd:ee:01 10.0.0.50 phone *\n")
samples = []
r.nft_counters = lambda: samples[-1] # noqa: E731 - stand in for the live sets
def tick(up, down):
samples.append({"10.0.0.50": {"up": up, "down": down}})
return r.accounting_tick()["totals"]["aa:bb:cc:dd:ee:01"]
t = tick(100, 200)
assert (t["up"], t["down"]) == (100, 200), t
t = tick(150, 700) # normal growth -> delta added
assert (t["up"], t["down"]) == (150, 700), t
t = tick(10, 5) # counters reset -> whole sample counts
assert (t["up"], t["down"]) == (160, 705), t
t = tick(10, 5) # unchanged -> nothing added
assert (t["up"], t["down"]) == (160, 705), t
assert t["ip"] == "10.0.0.50"
seen = t["last_seen"]
t = tick(10, 5) # idle tick must not refresh last_seen
assert t["last_seen"] == seen, (t["last_seen"], seen)
# an address with no lease is keyed by IP so it still shows up
samples.append({"10.0.0.77": {"up": 5, "down": 5}})
assert "10.0.0.77" in r.accounting_tick()["totals"]
# --- speedtest parsing (bytes/s -> Mbps, ns -> ms) ---------------------------
sample = ('{"timestamp":"2026-08-15 12:00:55","servers":[{"name":"Preston",'
'"latency":18796260,"dl_speed":63607890.17,"ul_speed":8839605.08}]}\n')