Partnership page: one settings card instead of three, and a Data Transferred card

Windows with runs but no bytes say 'no data moved' rather than 0.00 GB, and rows before the byte-parser fix are excluded rather than summed into a false total.
This commit is contained in:
Gmer4Lfe
2026-08-17 14:36:21 -04:00
parent 49372996ac
commit bce3bf61e9
2 changed files with 160 additions and 20 deletions
+67
View File
@@ -240,6 +240,72 @@ 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];
$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);
}
}
// 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;
}
}
$out['live'] = ['running' => (int)$running, 'share' => $share];
}
return $out;
}
// ── Media seed progress ───────────────────────────────────────────────────────
//
// Phase 3 dispatches Rsync/media_seed.sh detached, because a first seed of a full
@@ -501,6 +567,7 @@ function vv_partnership_all(): array {
'config' => vv_pt_config(),
'nodes' => vv_pt_nodes(),
'sync' => vv_pt_sync(),
'xfer' => vv_pt_transfer_stats(),
'ts' => time(),
];
}
+93 -20
View File
@@ -105,16 +105,6 @@ textarea.vv-pt-set-input { resize:vertical; white-space:pre; }
<div id="vv-pt-config-body" style="color:#444;font-size:12px;">Loading…</div>
</div>
<!-- Host Settings -->
<div class="vv-card" id="vv-pt-hosts-card" style="margin-bottom:12px;display:none;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<h3 style="margin:0;">Host Settings</h3>
<button id="vv-pt-hosts-save" class="vv-pt-action-btn run"
onclick="vvPtSaveHosts(this)" style="display:none;">Save</button>
</div>
<div id="vv-pt-hosts-body"></div>
</div>
<!-- Offline countdown warning (shown only when partner has been unreachable) -->
<div id="vv-pt-offline-warn" style="display:none;margin-bottom:12px;"></div>
@@ -123,8 +113,11 @@ textarea.vv-pt-set-input { resize:vertical; white-space:pre; }
<div style="color:#444;font-size:12px;padding:16px 0;text-align:center;">Loading…</div>
</div>
<!-- Array settings (above sync/actions row) -->
<div id="vv-pt-arrays-wrap" style="margin-bottom:12px;display:none;"></div>
<!-- Data moved between the servers -->
<div class="vv-card" id="vv-pt-xfer-card" style="margin-bottom:12px;">
<h3>Data Transferred</h3>
<div id="vv-pt-xfer-body" style="color:#444;font-size:12px;">Loading…</div>
</div>
<!-- Mirror sync + Actions side by side -->
<div style="display:flex;gap:12px;margin-bottom:12px;align-items:stretch;flex-wrap:wrap;">
@@ -153,9 +146,28 @@ textarea.vv-pt-set-input { resize:vertical; white-space:pre; }
</div>
</div>
<div id="vv-pt-settings-hint" style="font-size:10px;color:#444;margin-top:4px;">
Partnership configuration for this server.
Partnership, host and array configuration for this server.
</div>
<!-- One card, three sections. These were three separate top-level cards — "Host Settings",
"Array Settings" and "Settings" — stacked down the page between the things people
actually come here to look at. They are all the same kind of thing: conf this host owns.
Collapsed together they cost one line until opened. -->
<div id="vv-pt-settings-body" style="display:none;margin-top:10px;">
<div id="vv-pt-hosts-section" style="display:none;margin-bottom:16px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<div class="vv-cl-title" style="margin:0;">Host settings</div>
<button id="vv-pt-hosts-save" class="vv-pt-action-btn run"
onclick="vvPtSaveHosts(this)" style="display:none;">Save</button>
</div>
<div id="vv-pt-hosts-body"></div>
</div>
<div id="vv-pt-arrays-wrap" style="display:none;margin-bottom:16px;"></div>
<div id="vv-pt-settings-conf"></div>
</div>
<div id="vv-pt-settings-body" style="display:none;margin-top:10px;"></div>
</div>
@@ -504,9 +516,9 @@ function _vvRenderArrayCards() {
if (!hasArrays) { wrap.style.display = 'none'; return; }
wrap.style.display = '';
wrap.innerHTML = `<div class="vv-card">
wrap.innerHTML = `<div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<h3 style="margin:0;">Array Settings</h3>
<div class="vv-cl-title" style="margin:0;">Array settings</div>
<button id="vv-pt-arrays-save" class="vv-pt-action-btn run"
onclick="vvPtSaveArrays(this)" style="display:none;">Save Changes</button>
</div>
@@ -570,7 +582,9 @@ function vvPtToggleSettings() {
}
function vvPtLoadSettings() {
const body = document.getElementById('vv-pt-settings-body');
// The conf rows render into their own sub-node, not the whole collapsible body — the body now
// also holds the host and array sections, and writing over it would delete them.
const body = document.getElementById('vv-pt-settings-conf');
body.innerHTML = '<div style="color:#555;font-size:12px;padding:8px 0;">Loading…</div>';
fetch('/plugins/varaverk/api/partnership_settings.php?_=' + Date.now())
.then(r => r.json())
@@ -627,12 +641,12 @@ function vvPtSetChanged(el) {
const changed = el.value !== el.dataset.orig;
el.classList.toggle('changed', changed);
// Show Save button if any field is changed
const any = document.querySelector('#vv-pt-settings-body .vv-pt-set-input.changed');
const any = document.querySelector('#vv-pt-settings-conf .vv-pt-set-input.changed');
document.getElementById('vv-pt-settings-save').style.display = any ? '' : 'none';
}
async function vvPtSaveSettings(btn) {
const changedEls = document.querySelectorAll('#vv-pt-settings-body .vv-pt-set-input.changed');
const changedEls = document.querySelectorAll('#vv-pt-settings-conf .vv-pt-set-input.changed');
if (!changedEls.length) return;
const changes = [];
changedEls.forEach(el => changes.push({
@@ -723,6 +737,64 @@ function _syncToggle(gate, dimmed) {
title="${gate.var} — click to ${on ? 'disable' : 'enable'}">${on ? 'ON' : 'OFF'}</span>`;
}
// ── Data transferred ──────────────────────────────────────────────────────────
//
// 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>`;
}
function _renderSync(sync) {
const jobs = sync.jobs || {};
const gates = sync.gates || {};
@@ -1332,7 +1404,7 @@ function _render(data) {
document.getElementById('vv-pt-config-body').innerHTML = _renderConfig(cfg);
// Host settings — always shown upfront
const hostsCard = document.getElementById('vv-pt-hosts-card');
const hostsCard = document.getElementById("vv-pt-hosts-section");
hostsCard.style.display = nodes.length ? '' : 'none';
document.getElementById('vv-pt-hosts-body').innerHTML = _renderHostSettings(nodes, isOwner);
document.getElementById('vv-pt-hosts-save').style.display = 'none';
@@ -1341,6 +1413,7 @@ function _render(data) {
_renderOfflineWarn(cfg);
// Mirror sync health
_renderXfer(data.xfer || {});
document.getElementById('vv-pt-sync-body').innerHTML = _renderSync(data.sync || {});
// Node grid — columns based on count