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
+73 -51
View File
@@ -597,62 +597,84 @@ function _renderOfflineWarn(cfg) {
// ── Data transferred ──────────────────────────────────────────────────────────
//
// Tailscale's per-peer counters, so this is every byte across the link — rsync, SSH, the arr
// APIs, conf pushes, the Unraid API — not one tool's log of itself.
//
// A window says how far its samples actually reach. The sampler starts collecting the first
// minute this ships, so "30 days" means thirty days of history only after thirty days of it;
// until then the figure is real but covers less than the label, and saying which is the
// difference between a number and a claim.
function _vvBytes(b) {
if (!b) return '0 B';
const u = ['B','KB','MB','GB','TB'];
let i = 0, v = b;
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++; }
return (i >= 3 ? v.toFixed(2) : Math.round(v)) + ' ' + u[i];
}
function _vvDur(s) {
if (!s) return '';
if (s < 3600) return Math.round(s / 60) + 'm';
if (s < 86400) return (s / 3600).toFixed(1) + 'h';
return (s / 86400).toFixed(1) + 'd';
}
function _renderXfer(x) {
const el = document.getElementById('vv-pt-xfer-body');
if (!el) return;
const peers = (x && x.peers) || {};
const names = Object.keys(peers);
if (!names.length) {
el.innerHTML = `<div style="font-size:11px;color:#444;">No samples yet — the mesh sampler
records once a minute. Figures appear within a minute or two.</div>`;
return;
}
let html = '';
for (const p of names) {
const d = peers[p];
const live = d.live;
html += `<div style="margin-bottom:6px;">
<div style="display:flex;justify-content:space-between;align-items:baseline;gap:8px;
padding-bottom:4px;border-bottom:1px solid #1b1b1b;">
<span style="font-size:11px;color:#888;font-family:monospace;">${vvEscHtml(p)}</span>
<span style="font-size:10px;color:${live && (live.tx_bps + live.rx_bps) > 1024 ? '#4caf50' : '#3a3a3a'};font-family:monospace;">
${live ? '↑ ' + _vvBytes(live.tx_bps) + '/s ↓ ' + _vvBytes(live.rx_bps) + '/s' : 'idle'}
</span>
</div>`;
const rows = [['Last 24 hours','24h'], ['Last 7 days','7d'], ['Last 30 days','30d']];
for (const [label, key] of rows) {
const w = (d.windows || {})[key] || {};
const total = (w.tx || 0) + (w.rx || 0);
// A window whose samples do not span it yet is labelled with what it does cover, rather
// than presenting a partial figure under a full-window heading.
const partial = w.covers && w.covers < ({'24h':86400,'7d':604800,'30d':2592000}[key]) * 0.9;
html += `<div style="display:flex;justify-content:space-between;align-items:baseline;gap:10px;
padding:4px 0;border-bottom:1px solid #151515;">
<span style="color:#666;font-size:11px;">${label}${partial
? ` <span style="color:#3a3a3a;font-size:9px;">only ${_vvDur(w.covers)} recorded</span>` : ''}</span>
<span style="font-family:monospace;font-size:11px;color:${total ? '#4caf50' : '#333'};">
${total ? '↑ ' + _vvBytes(w.tx) + ' ↓ ' + _vvBytes(w.rx) : 'nothing'}
</span></div>`;
}
const s = d.session || {};
html += `<div style="display:flex;justify-content:space-between;align-items:baseline;gap:10px;padding:4px 0;">
<span style="color:#666;font-size:11px;">Since tailscaled start</span>
<span style="font-family:monospace;font-size:11px;color:#7a7a7a;">
↑ ${_vvBytes(s.tx)} ↓ ${_vvBytes(s.rx)}</span>
</div></div>`;
}
el.innerHTML = html;
}
//
// A window with runs but no bytes says "no data moved", not "0.00 GB". They are different
// claims: rsync completing with nothing to send is the normal state for a mirror that is
// already in step, and printing a zero total invites the reading that the sync is broken.
// A window with no runs at all says so separately again.
function _renderXfer(x) {
const el = document.getElementById('vv-pt-xfer-body');
if (!el) return;
x = x || {};
const w = x.windows || {};
const fmt = (b) => {
if (b === null || b === undefined) return null;
const u = ['B','KB','MB','GB','TB'];
let i = 0, v = b;
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++; }
return (i >= 3 ? v.toFixed(2) : Math.round(v)) + ' ' + u[i];
};
const row = (label, key) => {
const d = w[key] || {};
const bytes = fmt(d.bytes);
let val, col;
if (!d.runs) { val = 'no syncs ran'; col = '#333'; }
else if (bytes) { val = bytes; col = '#4caf50'; }
else { val = 'no data moved'; col = '#555'; }
const extra = d.runs
? `<span style="color:#333;font-size:9px;">${d.runs} run${d.runs===1?'':'s'}${d.failed?` · ${d.failed} failed`:''}</span>`
: '';
return `<div style="display:flex;justify-content:space-between;align-items:baseline;gap:10px;
padding:5px 0;border-bottom:1px solid #171717;">
<span style="color:#666;font-size:11px;">${label}</span>
<span style="display:flex;align-items:baseline;gap:8px;">
${extra}<span style="color:${col};font-size:12px;font-family:monospace;">${val}</span>
</span></div>`;
};
const live = x.live;
const liveHtml = live
? `<div style="display:flex;justify-content:space-between;align-items:baseline;gap:10px;
padding:5px 0;border-bottom:1px solid #171717;">
<span style="color:#666;font-size:11px;">Live</span>
<span style="color:#ff9800;font-size:12px;font-family:monospace;">⟳ ${live.running} transfer${live.running===1?'':'s'}${live.share?` · ${vvEscHtml(live.share)}`:''}</span>
</div>`
: `<div style="display:flex;justify-content:space-between;align-items:baseline;gap:10px;
padding:5px 0;border-bottom:1px solid #171717;">
<span style="color:#666;font-size:11px;">Live</span>
<span style="color:#333;font-size:12px;font-family:monospace;">idle</span>
</div>`;
el.innerHTML = liveHtml + row('Last 24 hours','24h') + row('Last 7 days','7d') + row('Last 30 days','30d')
+ `<div style="margin-top:7px;font-size:9px;color:#2e2e2e;line-height:1.5;">
Counted from ${vvEscHtml(x.epoch || '')} — before that rsync logged every transfer as
0 bytes, so those rows are excluded rather than summed into a false total.
</div>`;
}
// ── Config bar ────────────────────────────────────────────────────────────────