Say when rsync last moved data, and stop colouring successful runs red

last_sync reads the maintenance orchestrators' run records, and those do git
pull, permissions, cleaners and docker updates on schedule whether or not the
global gate is open — so the page reported a healthy 56-minute "daily sync"
while nothing had transferred since 16 July. Both cards also compared status
against 'success' while being fed 'ok', so every clean run rendered in the
failure colour. Adds the assistant, scoped to the tab.
This commit is contained in:
Gmer4Lfe
2026-08-14 16:50:28 -04:00
parent 2cd1384786
commit 7a3e8dd378
2 changed files with 110 additions and 5 deletions
+22
View File
@@ -143,10 +143,31 @@ $warnGb = (float)($vars['BANDWIDTH_WARN_GB'] ?? 50);
$cutoff = date('Y-m-d', strtotime('-30 days'));
$history = [];
// When rsync last actually moved anything, taken from the whole file rather than the window above.
//
// This is the only record of a transfer. last_sync is built from the orchestrators' run records,
// and those orchestrators do a great deal that is not rsync — git pull, permissions, cleaners, arr
// cleanup, docker updates — and run on their schedule whether or not RSYNC_ENABLED is true. So the
// page could report a healthy 56-minute "daily sync" for a subsystem that had not run in a month,
// which is exactly what it was doing: the last entry here is 2026-07-16, the day the global gate
// was closed for the HOST2 rebuild.
//
// Deliberately outside the 30-day cutoff. The last transfer is 29 days old as this is written and
// would have dropped out of the window within days, taking the page from a wrong answer to no
// answer — which on this question is not an improvement.
$lastTransfer = null;
if (file_exists($bwLog)) {
foreach (file($bwLog, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
$p = explode('|', $line);
if (count($p) < 5) continue;
// Recorded before the cutoff test, and unconditionally: the file is chronological, so the
// last line to pass the shape check is the most recent transfer however old it is.
$lastTransfer = [
'ts' => strtotime($p[0] . ' ' . $p[1]) ?: null,
'profile' => $p[2],
'status' => trim($p[4]),
];
if ($p[0] < $cutoff) continue;
$history[] = [
'date' => $p[0],
@@ -186,6 +207,7 @@ echo json_encode([
'windows' => $base['windows'],
'active' => $base['active'],
'last_sync' => $base['last_sync'],
'last_transfer' => $lastTransfer,
'bw_history' => $history,
'bw_warn_gb' => $warnGb,
'win_arrays' => $winArrays,
+88 -5
View File
@@ -37,6 +37,7 @@
// api/flag_toggle.php enable/disable toggles
// api/confform.php inline conf edits
require_once dirname(__DIR__) . '/include/confui.php';
require_once dirname(__DIR__) . '/include/ai_chat.php';
?>
<style>
.vv-ry-card { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:10px 12px;min-width:0; }
@@ -293,6 +294,30 @@ require_once dirname(__DIR__) . '/include/confui.php';
<div id="vv-ry-grid" style="display:grid;grid-template-columns:repeat(8,1fr);gap:12px;margin-bottom:12px;"></div>
<?php if (vv_ai_ui_on()): ?>
<div class="vv-card" id="vv-ry-ai-card" style="margin-bottom:12px;">
<?php
// All three before the markup — the factory, the profile registry and the store are separate
// and none is implied by the others. Omitting them renders a chat that looks complete and dies
// on the first click with VvAiChat undefined.
vv_ai_profiles_script();
vv_ai_chat_store_script();
vv_ai_chat_assets();
// Scoped to the tab, so "why hasn't it run" and "what does this profile copy" resolve here
// rather than asking which of eleven profiles is meant. Varaverk assistant rather than General
// Chat for the same reason as the Watchdog tab: this is a page you open with a question about
// this installation, and the answer should come from its own docs and state with sources.
vv_ai_chat_markup('vv-ry-ai', [
'profile' => 'varaverk',
'compact' => true,
'title' => 'Assistant',
'scopeLabel' => 'Rsync',
'empty' => 'Ask about a profile, a window, or why a sync did or did not run.',
'placeholder' => 'Ask about what is on this page…',
]); ?>
</div>
<?php endif; ?>
<!-- ── Info / Profile-editor layer ───────────────────────────────────────── -->
<div id="vv-ry-layer">
@@ -607,12 +632,44 @@ function vvRyEscAttr(s) {
.replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// ── Two status vocabularies, one colour ───────────────────────────────────────
// Transfers record success/failed; orchestrator run records record ok/warn/running. This page is
// fed both and compared everything against 'success', so every window that had run perfectly well
// rendered in the failure colour — four red "ok"s describing four clean runs.
//
// Written as an allow-list of the good states rather than a test for the bad ones: a status this
// does not recognise should read as a problem and be looked at, not be quietly coloured green.
function _statusCol(s) {
const v = String(s || '').toLowerCase();
if (v === 'success' || v === 'ok') return '#4caf50';
if (v === 'running' || v === 'warn') return '#ffb74d';
return '#ef5350';
}
function _statusOk(s) { return _statusCol(s) === '#4caf50'; }
// ── Status + active card ──────────────────────────────────────────────────────
function _statusCard(data) {
const en = data.enabled;
const dotC = en ? '#4caf50' : '#ef5350';
const active = data.active || [];
// The one figure on this page that answers "has anything actually been copied", as opposed to
// "did the window that contains rsync run". Those diverged the day the global gate closed and
// nothing said so — the windows kept reporting healthy runs because their orchestrators kept
// doing their other work. Aged deliberately: a transfer that is weeks old is the finding.
const xfer = data.last_transfer;
const xferAge = xfer && xfer.ts ? Math.floor(Date.now() / 1000) - xfer.ts : null;
const xferCol = xferAge == null ? '#555'
: xferAge > 86400 * 7 ? '#ef5350'
: xferAge > 86400 * 2 ? '#ffb74d' : '#4caf50';
const xferHtml = !xfer || !xfer.ts
? '<div style="color:#333;font-size:11px;margin-top:4px;">No transfer has ever been recorded</div>'
: `<div style="display:flex;align-items:baseline;gap:7px;margin-top:4px;">
<span style="font-size:12px;color:${xferCol};">${_relTime(xfer.ts)}</span>
<span style="font-size:11px;color:#555;">${vvEscHtml(xfer.profile || '')}</span>
<span style="font-size:10px;color:${_statusCol(xfer.status)};margin-left:auto;">${vvEscHtml(xfer.status || '')}</span>
</div>`;
let activeHtml;
if (active.length === 0) {
activeHtml = '<div style="color:#333;font-size:11px;margin-top:4px;">No transfers running</div>';
@@ -639,6 +696,9 @@ function _statusCard(data) {
<hr class="vv-ry-sep">
<div class="vv-ry-sec">Active transfers</div>
${activeHtml}
<hr class="vv-ry-sep">
<div class="vv-ry-sec">Last transfer</div>
${xferHtml}
</div>`;
}
@@ -652,7 +712,7 @@ function _lastSyncCard(data) {
<span class="vv-ry-lbl">${WIN_META[k]?.label || k}</span>
<span class="vv-ry-val" style="color:#333;">never</span>
</div>`;
const cls = e.status === 'success' ? 'color:#4caf50' : 'color:#ef5350';
const cls = 'color:' + _statusCol(e.status);
return `<div class="vv-ry-row">
<span class="vv-ry-lbl">${WIN_META[k]?.label || k}</span>
<span class="vv-ry-val">
@@ -663,8 +723,16 @@ function _lastSyncCard(data) {
</div>`;
});
// Named for what it measures. These are the maintenance orchestrators that contain the rsync
// step, not the rsync step — daily_sync_maintenance also does the git pull, permissions, the
// cleaners, arr cleanup and docker updates, so its duration is mostly not transfer time and it
// reports "ok" whether or not RSYNC_ENABLED let a single byte move. "Last completed run" read
// as a sync result on a page called Rsync; the transfer figure is in the status card above.
return `<div class="vv-ry-card" style="grid-column:span 4;">
<div class="vv-ry-sec">Last completed run</div>
<div class="vv-ry-sec">Window orchestrator — last run</div>
<div style="font-size:10px;color:#3a3a3a;margin:-2px 0 6px;">
The job that contains the rsync step, not the transfer itself
</div>
${rows.join('')}
</div>`;
}
@@ -685,7 +753,7 @@ function _windowsRow(data) {
let statusHtml = '<div class="vv-ry-win-stat">No run history</div>';
if (ls && ls.ts) {
const okCol = ls.status === 'success' ? '#4caf50' : '#ef5350';
const okCol = _statusCol(ls.status);
statusHtml = `<div class="vv-ry-win-stat" style="color:${okCol};">${ls.status}</div>
<div class="vv-ry-win-dur">${_relTime(ls.ts)}${ls.duration ? ' · ' + _dur(ls.duration) : ''}</div>`;
}
@@ -733,7 +801,7 @@ function _bwSection(data) {
}
const p = profileMap[r.profile];
p.runs++;
if (r.status === 'success') p.success++;
if (_statusOk(r.status)) p.success++;
p.totalDur += r.duration;
p.totalBytes += r.bytes;
}
@@ -756,7 +824,7 @@ function _bwSection(data) {
// Recent runs (last 20, newest first)
const recent = [...history].reverse().slice(0, 20);
const runRows = recent.map(r => {
const stCls = r.status === 'success' ? 'on' : 'err';
const stCls = _statusOk(r.status) ? 'on' : 'err';
const durStr = r.duration >= 3600
? Math.floor(r.duration / 3600) + 'h ' + Math.floor((r.duration % 3600) / 60) + 'm'
: r.duration >= 60
@@ -1853,4 +1921,19 @@ function _vvMsStartPoll(token, btn, stat, out, badge, stopBtn) {
.catch(() => {});
}, 1500);
}
// ── Assistant ────────────────────────────────────────────────────────────────
// Scope is fixed to the tab rather than following a selection. Unlike the Scheduler, nothing here
// is "open" in the sense of one log or one script — the operator is looking at the whole picture
// and asking about part of it, and the worker resolves bare references against the tab name.
if (document.getElementById('vv-ry-ai-chat')) {
VvAiChat({
prefix: 'vv-ry-ai',
profile: 'varaverk',
scopeLabel: 'Rsync',
scope: () => 'Rsync',
resumeProfile: 'varaverk',
empty: 'Ask about a profile, a window, or why a sync did or did not run.',
});
}
</script>