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:
@@ -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(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user