Measure the link, not rsync: Data Transferred now reads Tailscale per-peer counters

The old card totalled rsync's own logs, so it reported 'no data moved' across a link that had carried hundreds of gigabytes over SSH, the arr APIs, conf pushes and the Unraid API.
This commit is contained in:
Gmer4Lfe
2026-08-17 16:17:11 -04:00
parent 25e055a7ac
commit fbf6a07da9
4 changed files with 275 additions and 108 deletions
+80 -57
View File
@@ -240,68 +240,91 @@ function vv_pt_remote_system(string $ip, string $sshKey): array {
];
}
// ── Transfer volume ───────────────────────────────────────────────────────────
//
// Reads data/db/bandwidth_history.db, one line per completed rsync:
// YYYY-MM-DD|HH:MM|profile|duration|status|bytes
//
// Rows before VV_BW_EPOCH are excluded outright. Until 2026-08-14 rsync.sh read the byte count
// from the wrong field of rsync --stats and every row was written as 0 — 1,423 of them. Summing
// those produces a confident 0.00 GB for any window that reaches back far enough, which is worse
// than an empty card: it is a measurement that looks taken. The cut is on the fix, not on a
// guess about which rows look plausible.
//
// A window whose rows are all zero reports null, not 0, so the UI can say "no transfers
// recorded" rather than assert nothing moved. Those are different claims and only one of them
// is supported by an all-zero column.
define('VV_BW_EPOCH', '2026-08-14');
function vv_pt_transfer_stats(): array {
$file = DATA_DIR . '/db/bandwidth_history.db';
$out = ['epoch' => VV_BW_EPOCH, 'windows' => [], 'live' => null, 'usable_rows' => 0];
// ── Mesh traffic ──────────────────────────────────────────────────────────────
//
// What has actually crossed the link to each partner, from Tailscale's own per-peer byte
// counters — not from rsync's logs.
//
// The previous version totalled data/db/bandwidth_history.db, so it could only ever describe
// rsync. Everything else using the same link — SSH, the arr APIs, conf pushes, the Unraid API,
// the webhook — was invisible to it, and it reported "no data moved" across a link carrying
// hundreds of gigabytes. Counting bytes on the wire measures the partnership rather than one
// tool's opinion of itself.
//
// Windows come from Tools/mesh_traffic_sample.php, which records the raw counters once a
// minute. A window total is the newest sample minus the oldest one still inside it.
function vv_pt_mesh_traffic(): array {
$file = DATA_DIR . '/db/mesh_traffic.db';
$out = ['peers' => [], 'samples' => 0];
if (!is_file($file)) return $out;
$windows = ['24h' => 86400, '7d' => 604800, '30d' => 2592000];
foreach ($windows as $k => $_) $out['windows'][$k] = ['bytes' => null, 'runs' => 0, 'failed' => 0];
if (is_file($file)) {
$now = time();
$epoch = strtotime(VV_BW_EPOCH . ' 00:00:00');
$fh = fopen($file, 'r');
if ($fh) {
while (($line = fgets($fh)) !== false) {
$p = explode('|', trim($line));
if (count($p) < 6) continue;
$ts = strtotime($p[0] . ' ' . $p[1]);
if (!$ts || $ts < $epoch) continue;
$out['usable_rows']++;
$bytes = (int)$p[5];
$failed = ($p[4] ?? '') !== 'success';
foreach ($windows as $k => $span) {
if ($ts < $now - $span) continue;
$w = &$out['windows'][$k];
$w['runs']++;
if ($failed) $w['failed']++;
if ($bytes > 0) $w['bytes'] = (int)$w['bytes'] + $bytes;
unset($w);
}
}
fclose($fh);
}
$byPeer = [];
$fh = fopen($file, 'r');
if (!$fh) return $out;
while (($line = fgets($fh)) !== false) {
$p = explode('|', trim($line));
if (count($p) < 4) continue;
$byPeer[$p[1]][] = [(int)$p[0], (int)$p[2], (int)$p[3]];
$out['samples']++;
}
fclose($fh);
// Live: an rsync this host is running right now. The lock name carries the share, which is
// the only part of "what is moving" worth showing without parsing progress output.
$running = trim((string)shell_exec("pgrep -c -f '^rsync -av' 2>/dev/null")) ;
if ((int)$running > 0) {
$share = '';
foreach (glob('/tmp/unraid_locks/rsync_*.lock') ?: [] as $l) {
$pid = (int)preg_replace('/\D/', '', (string)@file_get_contents($l));
if ($pid && is_dir("/proc/$pid")) {
$share = preg_replace('/^rsync_|\.lock$/', '', basename($l));
break;
$now = time();
$windows = ['24h' => 86400, '7d' => 604800, '30d' => 2592000];
foreach ($byPeer as $peer => $rows) {
usort($rows, fn($a, $b) => $a[0] <=> $b[0]);
$last = end($rows);
// Live rate from the two most recent samples. Meaningless if they are far apart — a gap
// means the sampler missed runs, and dividing by that gap reports an average over a
// period nobody watched as though it were current.
$live = null;
$n = count($rows);
if ($n >= 2) {
$prev = $rows[$n - 2];
$dt = $last[0] - $prev[0];
if ($dt > 0 && $dt <= 300 && $last[1] >= $prev[1] && $last[2] >= $prev[2]) {
$live = ['tx_bps' => (int)(($last[1] - $prev[1]) / $dt),
'rx_bps' => (int)(($last[2] - $prev[2]) / $dt),
'age' => $now - $last[0]];
}
}
$out['live'] = ['running' => (int)$running, 'share' => $share];
$win = [];
foreach ($windows as $k => $span) {
$from = $now - $span;
// Sum forward through the window rather than subtracting endpoints, so a tailscaled
// restart — which returns the counters to zero — costs one interval instead of
// producing a negative total or a spike the size of the whole previous session.
$tx = 0; $rx = 0; $seen = 0; $oldest = null;
$prev = null;
foreach ($rows as $r) {
if ($r[0] < $from) { $prev = $r; continue; }
if ($oldest === null) $oldest = $r[0];
if ($prev !== null) {
$tx += ($r[1] >= $prev[1]) ? $r[1] - $prev[1] : $r[1];
$rx += ($r[2] >= $prev[2]) ? $r[2] - $prev[2] : $r[2];
$seen++;
}
$prev = $r;
}
$win[$k] = [
'tx' => $tx, 'rx' => $rx, 'intervals' => $seen,
// How far back the data actually reaches. A 30-day figure built from six hours of
// samples is not a 30-day figure, and the card has to be able to say so.
'covers' => $oldest ? $now - $oldest : 0,
];
}
$out['peers'][$peer] = [
'live' => $live,
'windows' => $win,
// Counter totals since tailscaled last started — the longest view available without
// any history at all, and the one that is right on a fresh install.
'session' => ['tx' => $last[1], 'rx' => $last[2]],
];
}
return $out;
}
@@ -624,7 +647,7 @@ function vv_partnership_all(): array {
'config' => vv_pt_config(),
'nodes' => vv_pt_nodes(),
'sync' => vv_pt_sync(),
'xfer' => vv_pt_transfer_stats(),
'xfer' => vv_pt_mesh_traffic(),
'ts' => time(),
];
}