430
Network Watchdog

431

432	      ${cfg.net_wd_enabled ? _pill('enabled', 'ok') : _pill('disabled', '')}
433	      ${_pill('Tailscale check ' + (cfg.ts_check ? 'on' : 'off'), cfg.ts_check ? '' : '')}
434	      ${npmUrl ? _pill('NPM ' + npmStr + '/' + cfg.npm_strike_lim + ' strikes', npmCls) : ''}
435

436	    ${ddnsHtml}
437

438	    ${npmHtml}
439
`;
440	}
441
442	// ── Main render ───────────────────────────────────────────────────────────────
443	function _render(data) {
444	  const nodes = data.nodes || [];
445	  const cfg   = data.cfg   || {};
446	  let html = '';
447
448	  // Per-host: pressure alert (if active) then all 5 watchdog cards in one equal-spaced row
449	  for (const node of nodes) {
450	    html += _pressureCard(node);
451	    html += `

452	      ${_systemCard(node, cfg)}
453	      ${_dockerCard(node, cfg)}
454	      ${_stabilityCard(node, cfg)}
455	      ${_storageCard(node, cfg)}
456	      ${_networkCard(node, cfg)}
457
`;
458	  }
459
460	  // Config inventory — separate row, each node span 4 (2 nodes = full row)
461	  for (const node of nodes) html += _configCard(node);
462
463	  if (!html) html = '
No nodes configured.
';
464
465	  document.getElementById('vv-wd-grid').innerHTML = html;
466
467	  const ts = data.ts
468	    ? new Date(data.ts * 1000).toLocaleString([], {
469	        month:'numeric', day:'numeric', year:'numeric',
470	        hour:'2-digit', minute:'2-digit', second:'2-digit'})
471	    : '';
472	  document.getElementById('vv-wd-ts').textContent = ts ? 'Updated: ' + ts : '';
473	}
474
475	function vvWdLoad() {
476	  fetch('/plugins/varaverk/api/watchdog.php')
477	    .then(r => r.json())
478	    .then(_render)
479	    .catch(() => {
480	      document.getElementById('vv-wd-grid').innerHTML =
481	        '
Error loading watchdog data — check API
';
482	    });
483	}
484

Network Watchdog

      ${cfg.net_wd_enabled ? _pill('enabled', 'ok') : _pill('disabled', '')}
      ${_pill('Tailscale check ' + (cfg.ts_check ? 'on' : 'off'), cfg.ts_check ? '' : '')}
      ${npmUrl ? _pill('NPM ' + npmStr + '/' + cfg.npm_strike_lim + ' strikes', npmCls) : ''}

    ${ddnsHtml}

    ${npmHtml}

`;
}

// ── Main render ───────────────────────────────────────────────────────────────
function _render(data) {
  const nodes = data.nodes || [];
  const cfg   = data.cfg   || {};
  let html = '';

  // Per-host: pressure alert (if active) then all 5 watchdog cards in one equal-spaced row
  for (const node of nodes) {
    html += _pressureCard(node);
    html += `

      ${_systemCard(node, cfg)}
      ${_dockerCard(node, cfg)}
      ${_stabilityCard(node, cfg)}
      ${_storageCard(node, cfg)}
      ${_networkCard(node, cfg)}

`;
  }

  // Config inventory — separate row, each node span 4 (2 nodes = full row)
  for (const node of nodes) html += _configCard(node);

  if (!html) html = '
No nodes configured.
';

  document.getElementById('vv-wd-grid').innerHTML = html;

  const ts = data.ts
    ? new Date(data.ts * 1000).toLocaleString([], {
        month:'numeric', day:'numeric', year:'numeric',
        hour:'2-digit', minute:'2-digit', second:'2-digit'})
    : '';
  document.getElementById('vv-wd-ts').textContent = ts ? 'Updated: ' + ts : '';
}

function vvWdLoad() {
  fetch('/plugins/varaverk/api/watchdog.php')
    .then(r => r.json())
    .then(_render)
    .catch(() => {
      document.getElementById('vv-wd-grid').innerHTML =
        '
Error loading watchdog data — check API
';
    });
}


Bash

grep -n "_fmtBytes\|_configCard\|_dockerCard\|_stabilityCard\|_storageCard\|_networkCard\|function _" /boot/config/plugins/varaverk/Plugin/unraid/pages/watchdog.php | head -40

Find all function definitions and references in watchdog page


56:function _dur(s) {
63:function _row(lbl, val) {
67:function _bar(pct, col) {
71:function _pill(label, cls) {
75:function _levelLabel(level) {
78:function _levelCls(level) {
83:function _pressureCard(node) {
107:function _systemCard(node, cfg) {
153:    ${_row('RAM free', `<span style="color:${memCol}">${_fmtBytes(sys.mem_avail)}</span> / ${_fmtBytes(sys.mem_total)}`)}
171:function _dockerCard(node, cfg) {
234:function _stabilityCard(node, cfg) {
270:function _configCard(node) {
330:function _storageCard(node, cfg) {
407:function _networkCard(node, cfg) {
443:function _render(data) {
453:      ${_dockerCard(node, cfg)}
454:      ${_stabilityCard(node, cfg)}
455:      ${_storageCard(node, cfg)}
456:      ${_networkCard(node, cfg)}
461:  for (const node of nodes) html += _configCard(node);


Found it — `_fmtBytes` is called on line 153 but never defined. That throws a ReferenceError inside `_render`, which `.catch()` swallows and shows the generic error message.


grep -n "_fmtBytes\|fmtBytes" /boot/config/plugins/varaverk/Plugin/unraid/pages/watchdog.php

Confirm all _fmtBytes usages


153:    ${_row('RAM free', `<span style="color:${memCol}">${_fmtBytes(sys.mem_avail)}</span> / ${_fmtBytes(sys.mem_total)}`)}


grep -n "fmtBytes\|GB\s*=" /boot/config/plugins/varaverk/Plugin/unraid/pages/monitor.php | head -20

Check how other pages define fmtBytes and GB constant


1299:        function _fmtBytes(b) {
1333:            const bytes = v.bytes > 0 ? _fmtBytes(v.bytes) : '';


1299	        function _fmtBytes(b) {
1300	          if (!b) return '';
1301	          if (b >= 1073741824) return (b/1073741824).toFixed(1) + 'G';
1302	          if (b >= 1048576)    return (b/1048576).toFixed(0) + 'M';
1303	          return (b/1024).toFixed(0) + 'K';
1304	        }
1305
1306	        const gCol = enabled ? '#4caf50' : '#555';
1307
1308	        // ── Header: gate dot + window badges ──────────────────────────────────

        function _fmtBytes(b) {
          if (!b) return '';
          if (b >= 1073741824) return (b/1073741824).toFixed(1) + 'G';
          if (b >= 1048576)    return (b/1048576).toFixed(0) + 'M';
          return (b/1024).toFixed(0) + 'K';
        }

        const gCol = enabled ? '#4caf50' : '#555';

        // ── Header: gate dot + window badges ──────────────────────────────────


grep -n "^const GB\|^  const GB\|const GB " /boot/config/plugins/varaverk/Plugin/unraid/pages/watchdog.php

Find GB constant definition in watchdog page


(Bash completed with no output)


`GB` is also referenced (line 120) but not defined. Both are missing. I'll add them right after the helper functions at the top of the `<script>` block:


Edit

function _dur(s) {
  const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60);
  if (d) return d + 'd ' + h + 'h';
  if (h) return h + 'h ' + m + 'm';
  return m + 'm';
}

const GB = 1073741824;

function _fmtBytes(b) {
  if (!b) return '0';
  if (b >= GB)      return (b / GB).toFixed(1) + 'G';
  if (b >= 1048576) return (b / 1048576).toFixed(0) + 'M';
  return (b / 1024).toFixed(0) + 'K';
}

function _dur(s) {
  const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60);
  if (d) return d + 'd ' + h + 'h';
  if (h) return h + 'h ' + m + 'm';
  return m + 'm';
}


The file /boot/config/plugins/varaverk/Plugin/unraid/pages/watchdog.php has been updated successfully. (file state is current in your context — no need to Read it back)

const GB = 1073741824;

function _fmtBytes(b) {
  if (!b) return '0';
  if (b >= GB)      return (b / GB).toFixed(1) + 'G';
  if (b >= 1048576) return (b / 1048576).toFixed(0) + 'M';
  return (b / 1024).toFixed(0) + 'K';
}

function _dur(s) {
  const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60);
  if (d) return d + 'd ' + h + 'h';
  if (h) return h + 'h ' + m + 'm';
  return m + 'm';
}

+  if (b >= GB)      return (b / GB).toFixed(1) + 'G';


Now verify no other undefined references remain by checking all function calls vs definitions:


Bash

node -e "
const fs = require('fs');
const src = fs.readFileSync('/boot/config/plugins/varaverk/Plugin/unraid/pages/watchdog.php', 'utf8');
const scriptMatch = src.match(/<script>([\s\S]*?)<\/script>/);
if (!scriptMatch) { console.log('No script block found'); process.exit(1); }
const js = scriptMatch[1];
// Quick syntax check by trying to parse known patterns
const defs = [...js.matchAll(/function (\w+)\s*\(/g)].map(m => m[1]);
const constDefs = [...js.matchAll(/const (\w+)\s*=/g)].map(m => m[1]);
const allDefined = new Set([...defs, ...constDefs, 'Math','Date','document','fetch','setInterval','console','Boolean','parseInt','FormData']);
// Find function calls
const calls = [...js.matchAll(/\b(_[a-z]\w+)\s*\(/g)].map(m => m[1]);
const missing = [...new Set(calls)].filter(c => !allDefined.has(c));
console.log('Defined:', defs.join(', '));
console.log('Missing:', missing.length ? missing.join(', ') : 'none');
" 2>&1

Check for undefined function references in watchdog page script


Defined: _fmtBytes, _dur, _row, _bar, _pill, _levelLabel, _levelCls, _pressureCard, _systemCard, _dockerCard, _stabilityCard, _configCard, _storageCard, _networkCard, _render, vvWdLoad
Missing: _relTime


grep -n "_relTime" /boot/config/plugins/varaverk/Plugin/unraid/pages/watchdog.php

Find all _relTime usages


262:    : reboots.map(ts => `<div class="vv-wd-reboot-ts">${_relTime(ts)}</div>`).join('');


grep -n "_relTime\|relTime" /boot/config/plugins/varaverk/Plugin/unraid/pages/monitor.php | head -5

Check how _relTime is defined in monitor page


Two missing functions. I'll add `_relTime` alongside `_fmtBytes`:


Edit

function _fmtBytes(b) {
  if (!b) return '0';
  if (b >= GB)      return (b / GB).toFixed(1) + 'G';
  if (b >= 1048576) return (b / 1048576).toFixed(0) + 'M';
  return (b / 1024).toFixed(0) + 'K';
}

function _fmtBytes(b) {
  if (!b) return '0';
  if (b >= GB)      return (b / GB).toFixed(1) + 'G';
  if (b >= 1048576) return (b / 1048576).toFixed(0) + 'M';
  return (b / 1024).toFixed(0) + 'K';
}

function _relTime(ts) {
  if (!ts) return '—';
  const s = Math.floor(Date.now() / 1000) - ts;
  if (s < 60)    return 'just now';
  if (s < 3600)  return Math.floor(s / 60) + 'm ago';
  if (s < 86400) return Math.floor(s / 3600) + 'h ago';
  return Math.floor(s / 86400) + 'd ago';
}


function _fmtBytes(b) {
  if (!b) return '0';
  if (b >= GB)      return (b / GB).toFixed(1) + 'G';
  if (b >= 1048576) return (b / 1048576).toFixed(0) + 'M';
  return (b / 1024).toFixed(0) + 'K';
}

function _relTime(ts) {
  if (!ts) return '—';
  const s = Math.floor(Date.now() / 1000) - ts;
  if (s < 60)    return 'just now';
  if (s < 3600)  return Math.floor(s / 60) + 'm ago';
  if (s < 86400) return Math.floor(s / 3600) + 'h ago';
  return Math.floor(s / 86400) + 'd ago';
}

+  const s = Math.floor(Date.now() / 1000) - ts;

+  if (s < 3600)  return Math.floor(s / 60) + 'm ago';

+  if (s < 86400) return Math.floor(s / 3600) + 'h ago';

+  return Math.floor(s / 86400) + 'd ago';


Bash

node -e "
const fs = require('fs');
const src = fs.readFileSync('/boot/config/plugins/varaverk/Plugin/unraid/pages/watchdog.php', 'utf8');
const js = src.match(/<script>([\s\S]*?)<\/script>/)[1];
const defs = new Set([...[...js.matchAll(/function (\w+)\s*\(/g)].map(m=>m[1]),...[...js.matchAll(/const (\w+)\s*=/g)].map(m=>m[1]),'Math','Date','document','fetch','setInterval','Boolean','parseInt']);
const missing = [...new Set([...js.matchAll(/\b(_[a-z]\w+)\s*\(/g)].map(m=>m[1]))].filter(c=>!defs.has(c));
console.log(missing.length ? 'Still missing: ' + missing.join(', ') : 'All functions resolved');
" 2>&1

Verify no more missing functions


All functions resolved


All resolved. The watchdog page was throwing `ReferenceError: GB is not defined` inside `_render`, which `.catch()` was swallowing and showing as the generic "check API" message. Refresh the watchdog page — it should render now.


Auth page fixes: NPM certs tab now pulls live from NPM, Authelia ACL tab rewired to pure PHP, watchdog page fixed. No broken items remain — open the UI and verify each tab loads. (disable recaps in /config)


rsync page.move settings card to row 4 on the far right


grep -n "settings\|Settings\|grid\|row\|col\|span\|card" /boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php | head -60

Find grid layout and settings card in rsync page


2:.vv-ry-card    { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:10px 12px;min-width:0; }
3:.vv-ry-sec     { font-size:10px;font-weight:bold;color:#444;letter-spacing:.07em;text-transform:uppercase;margin-bottom:6px; }
4:.vv-ry-row     { display:flex;justify-content:space-between;align-items:baseline;gap:6px;margin:2px 0; }
5:.vv-ry-lbl     { font-size:11px;color:#444;white-space:nowrap; }
6:.vv-ry-val     { font-size:12px;color:#bbb;text-align:right; }
9:.vv-ry-pill.on   { background:#0d1f0d;color:#4caf50;border:1px solid #1a3a1a; }
10:.vv-ry-pill.off  { background:#1e1e1e;color:#555;border:1px solid #2a2a2a; }
11:.vv-ry-pill.warn { background:#1f1500;color:#ffb74d;border:1px solid #3a2800; }
12:.vv-ry-pill.err  { background:#200d0d;color:#ef5350;border:1px solid #3a1a1a; }
13:.vv-ry-pill.run  { background:#0a1a2a;color:#4a9eff;border:1px solid #1a3a5a; }
16:/* Window cards row */
17:.vv-ry-wins    { grid-column:1/-1;display:grid;grid-template-columns:repeat(5,1fr);gap:10px; }
18:@media (max-width:900px) { .vv-ry-wins { grid-template-columns:repeat(2,1fr); } }
21:.vv-ry-win.enabled  { border-color:#1a3a1a; }
23:.vv-ry-win-lbl { font-size:10px;font-weight:bold;letter-spacing:.08em;text-transform:uppercase;color:#555;margin-bottom:6px; }
24:.vv-ry-win-name{ font-size:15px;font-weight:bold;color:#888;margin-bottom:6px; }
25:.vv-ry-win-stat{ font-size:11px;color:#3a3a3a;margin-top:4px; }
26:.vv-ry-win-dur { font-size:10px;color:#3a3a3a; }
29:.vv-ry-bw      { grid-column:1/-1;display:grid;grid-template-columns:5fr 3fr;gap:10px; }
30:@media (max-width:900px) { .vv-ry-bw { grid-template-columns:1fr; } }
31:.vv-ry-prof-row{ display:flex;align-items:center;gap:8px;margin:3px 0; }
32:.vv-ry-prof-nm { font-size:11px;color:#888;width:120px;flex-shrink:0; }
35:.vv-ry-prof-cnt{ font-size:10px;color:#555;width:60px;text-align:right;flex-shrink:0; }
36:.vv-ry-run-row { display:grid;grid-template-columns:80px 90px 1fr 55px 48px;gap:4px;align-items:center;padding:2px 0;border-bottom:1px solid #1a1a1a; }
37:.vv-ry-run-row:last-child { border-bottom:none; }
40:.vv-ry-run-dt  { font-size:10px;color:#3a3a3a; }
41:.vv-ry-run-tm  { font-size:10px;color:#333; }
42:.vv-ry-run-pr  { font-size:11px;color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; }
43:.vv-ry-run-dur { font-size:10px;color:#555;text-align:right; }
46:/* Settings */
47:.vv-ry-set-grid { display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin-top:6px; }
48:@media (max-width:900px) { .vv-ry-set-grid { grid-template-columns:repeat(2,1fr); } }
50:.vv-ry-toggle-track { width:32px;height:18px;border-radius:9px;background:#222;border:1px solid #333;position:relative;transition:background .15s,border-color .15s;flex-shrink:0; }
51:.vv-ry-toggle-track.on { background:#1a3a1a;border-color:#2d5a2d; }
54:.vv-ry-toggle-lbl { font-size:11px;color:#666; }
55:.vv-ry-set-row { display:flex;align-items:center;gap:10px;margin:4px 0; }
56:.vv-ry-set-lbl { font-size:11px;color:#555;width:130px;flex-shrink:0; }
57:.vv-ry-set-inp { background:#111;border:1px solid #2a2a2a;border-radius:3px;color:#bbb;font-size:12px;padding:3px 7px;width:80px;outline:none; }
58:.vv-ry-set-inp:focus { border-color:#444; }
59:.vv-ry-set-hint { font-size:10px;color:#3a3a3a; }
60:.vv-ry-save-btn { background:#1a2a1a;border:1px solid #2d4a2d;color:#4caf50;font-size:11px;padding:4px 14px;border-radius:3px;cursor:pointer;transition:background .15s; }
68:                   color:#666;display:flex;align-items:center;gap:6px;user-select:none; }
69:.vv-ry-info-hdr:hover { color:#999;background:#141414; }
70:.vv-ry-info-chev { font-size:10px;color:#333;flex-shrink:0; }
72:.vv-ry-info-cols { display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:8px 20px; }
73:.vv-ry-info-cols > div { font-size:11px;color:#555;line-height:1.5; }
74:.vv-ry-info-cols strong { color:#777; }
75:.vv-ry-info-cols code { font-size:10px;color:#4a7a4a;background:#080808;padding:1px 4px;border-radius:2px;font-family:monospace; }
80:                    font-size:10px;color:#555;user-select:none;transition:border-color .1s; }
81:.vv-rp-flag:hover { border-color:#2a3a2a;color:#888; }
82:.vv-rp-flag input[type=checkbox] { accent-color:#4caf50;width:11px;height:11px;cursor:pointer; }
83:.vv-rp-flag input:checked ~ span { color:#4caf50; }
84:.vv-rp-flag:has(input:checked) { border-color:#1a3a1a;background:#080f08; }
85:.vv-rp-flag em { font-style:normal;color:#2e4e2e; }
87:/* ── Manual Sync card ──────────────────────────────────────────────── */
88:.vv-ms-row     { display:flex;gap:10px;margin-bottom:8px;align-items:flex-start;flex-wrap:wrap; }
89:.vv-ms-col     { display:flex;flex-direction:column;gap:4px;flex:1 1 200px;min-width:0; }
90:.vv-ms-lbl     { font-size:9px;font-weight:700;color:#444;text-transform:uppercase;letter-spacing:.07em; }
91:.vv-ms-inp     { background:#0d0d0d;border:1px solid #252525;border-radius:3px;color:#ccc;
93:.vv-ms-inp:focus { border-color:#3a5a3a; }


grep -n "grid-column\|vv-ry-card\|Settings\|settings\|id=\"vv" /boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php | head -60

Find card elements and grid column assignments


2:.vv-ry-card    { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:10px 12px;min-width:0; }
17:.vv-ry-wins    { grid-column:1/-1;display:grid;grid-template-columns:repeat(5,1fr);gap:10px; }
29:.vv-ry-bw      { grid-column:1/-1;display:grid;grid-template-columns:5fr 3fr;gap:10px; }
46:/* Settings */
121:
125:

129:
132:                                          border-radius:3px;padding:2px 10px;cursor:pointer;" id="vv-ms-tog">▾ Show</button>
134:

143:          <input class="vv-ms-inp" id="vv-ms-local" type="text" placeholder="/mnt/user/…"
148:          <button onclick="vvMsBrowseLocal()" id="vv-ms-lbrowse-btn"
152:

153:
162:            <select class="vv-ms-sel" id="vv-ms-host" onchange="vvMsHostChanged()">
165:
169:            <select class="vv-ms-sel" id="vv-ms-user">
176:          <input class="vv-ms-inp" id="vv-ms-rpath" type="text" placeholder="/mnt/user/…"
181:          <button onclick="vvMsBrowse()" id="vv-ms-browse-btn"
185:

186:
206:        <input type="checkbox" id="vv-ms-dryrun" data-flag="--dry-run" onchange="vvMsUpdatePreview()">--dry-run
211:
212:
221:        <input class="vv-ms-inp" id="vv-ms-bw" type="number" min="0" value="0"
226:        <input type="checkbox" id="vv-ms-usekey" checked>
227:        SSH key: <code id="vv-ms-key-info"
234:      <button class="vv-ms-run" id="vv-ms-run-btn" onclick="vvMsRun()">▶ Run Rsync</button>
235:      <button id="vv-ms-show-btn" onclick="vvMsToggleOutput()"
238:      <button id="vv-ms-stop-btn" onclick="vvMsStop()"
241:
245:

249:

252:

255:

272:
Settings → Global gate — RSYNC_ENABLED in master.conf. Master switch; turn off during rebuilds or maintenance.

273:
Settings → Tier toggles — CRITICAL/DAILY/etc._RSYNC_ENABLED. Disable one tier without affecting others.

341:

348:        <select id="vv-rp-selector"
361:

366:

367:

372:          <input id="vv-rp-name" type="text"
376:
386:

401:            <input id="vv-rp-opts-extra" type="text"
410:
412:          <input type="hidden" id="vv-rp-rsync_opts">
420:            <input class="vv-ry-set-inp" id="vv-rp-bw_limit" type="number" min="0" style="width:80px;">
424:            <input class="vv-ry-set-inp" id="vv-rp-retry_count" type="number" min="0" max="20" style="width:60px;">
428:            <input class="vv-ry-set-inp" id="vv-rp-sleep" type="number" min="0" style="width:70px;">
432:            <input class="vv-ry-set-inp" id="vv-rp-container_delay" type="number" min="0" style="width:70px;">
441:            <input class="vv-ry-set-inp" id="vv-rp-critical_containers" type="text" style="width:100%;box-sizing:border-box;font-family:monospace;">
445:            <input class="vv-ry-set-inp" id="vv-rp-delayed_containers" type="text" style="width:100%;box-sizing:border-box;font-family:monospace;">
449:            <input class="vv-ry-set-inp" id="vv-rp-exclude_dirs" type="text" style="width:100%;box-sizing:border-box;font-family:monospace;">
453:            <input class="vv-ry-set-inp" id="vv-rp-remote_restart" type="text" style="width:100%;box-sizing:border-box;font-family:monospace;">
459:          <button id="vv-rp-del-btn" onclick="vvRpDelete()"
462:
564:  return `

601:  return `

655:    return `


grep -n "span\|grid-column\|Settings\|_settingsCard\|settingsCard\|function _" /boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php | head -50

Find the settings card function and grid placement


17:.vv-ry-wins    { grid-column:1/-1;display:grid;grid-template-columns:repeat(5,1fr);gap:10px; }
29:.vv-ry-bw      { grid-column:1/-1;display:grid;grid-template-columns:5fr 3fr;gap:10px; }
46:/* Settings */
83:.vv-rp-flag input:checked ~ span { color:#4caf50; }
107:.vv-ms-chk span { font-size:11px;color:#555; }
120:  Rsync
121:
128:      Manual Rsync
129:
153:
154:        trailing / = sync contents · without / = sync the folder
165:
186:
193:        does not inherit defaults — check every desired flag
196:        <input type="checkbox" data-flag="-av" checked onchange="vvMsUpdatePreview()">-av
197:        <input type="checkbox" data-flag="--human-readable" checked onchange="vvMsUpdatePreview()">--human-readable
198:        <input type="checkbox" data-flag="--stats" checked onchange="vvMsUpdatePreview()">--stats
199:
200:        <input type="checkbox" data-flag="--delete" onchange="vvMsUpdatePreview()">--delete
201:        <input type="checkbox" data-flag="--inplace" onchange="vvMsUpdatePreview()">--inplace
202:        <input type="checkbox" data-flag="--no-whole-file" onchange="vvMsUpdatePreview()">--no-whole-file
203:        <input type="checkbox" data-flag="--checksum" onchange="vvMsUpdatePreview()">--checksum
204:        <input type="checkbox" data-flag="--compress" onchange="vvMsUpdatePreview()">--compress
205:
206:        <input type="checkbox" id="vv-ms-dryrun" data-flag="--dry-run" onchange="vvMsUpdatePreview()">--dry-run
210:        rsync
211:
212:
213:        …src host:dst
220:        BW limit
223:        KB/s (0 = unlimited)
227:        SSH key: loading…
241:
257:      Rsync Reference
266:        ▾ How to use this page
272:
Settings → Global gate — RSYNC_ENABLED in master.conf. Master switch; turn off during rebuilds or maintenance.

273:
Settings → Tier toggles — CRITICAL/DAILY/etc._RSYNC_ENABLED. Disable one tier without affecting others.

284:        ▸ Sync windows
301:        ▸ Profile system
323:        ▸ Common flag combinations
346:      Profile Editor
371:          Profile name
376:
384:            does NOT inherit defaults — list every desired flag
387:            <input type="checkbox" data-flag="-av" onchange="vvRpBuildOpts()">-av
388:            <input type="checkbox" data-flag="--human-readable" onchange="vvRpBuildOpts()">--human-readable
389:            <input type="checkbox" data-flag="--info=progress2" onchange="vvRpBuildOpts()">--info=progress2
390:
391:            <input type="checkbox" data-flag="--delete" onchange="vvRpBuildOpts()">--delete


grep -n "function _\|settingsCard\|Settings.*card\|_render\b" /boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php | head -40

Find all JS function definitions and the render function


481:function _vvRyViewPanelInner(key, cfg) {
537:function _dur(s) {
546:function _statusCard(data) {
581:function _lastSyncCard(data) {
608:function _windowsRow(data) {
652:function _bwSection(data) {
725:function _toggle(name, currentVal, label) {
734:function _settingsSection(data) {
795:function _render(data) {
828:    .then(_render)
919:function _vvRyBuildEditPanel(key) {
1113:function _vvRyLoadProfiles(key) {
1220:function _vvRpApplyOpts(opts) {
1257:function _vvRpPopulateSelector() {
1265:function _vvRpFillForm(name) {
1283:function _vvRpClearForm() {
1427:function _vvMsLoadHosts() {
1530:function _vvMsRenderDirs(dirsElId, dirs, parent, navFn) {
1687:function _vvMsStartPoll(token, btn, stat, out, badge, stopBtn) {


795	function _render(data) {
796	  _vvRyLastData = data;
797	  if (_vvRyEditMode) return;   // don't clobber the edit UI during a poll
798
799	  let html = '';
800	  html += _statusCard(data);
801	  html += _lastSyncCard(data);
802	  html += _windowsRow(data);
803	  html += _bwSection(data);
804	  html += _settingsSection(data);
805	  document.getElementById('vv-ry-grid').innerHTML = html;
806
807	  // Restore any window panels that were open before the re-render
808	  _vvRyWinOpen.forEach(key => {
809	    const card = document.querySelector(`.vv-ry-win[data-win-key="${key}"]`);
810	    if (!card) return;
811	    const panel = card.querySelector('.vv-ry-win-panel');
812	    const chev  = card.querySelector('.vv-ry-win-chev');
813	    if (panel) panel.style.display = '';
814	    if (chev)  chev.textContent    = '▾';
815	  });
816
817	  const ts = data.ts
818	    ? new Date(data.ts * 1000).toLocaleString([], {
819	        month:'numeric', day:'numeric', year:'numeric',
820	        hour:'2-digit', minute:'2-digit', second:'2-digit'})
821	    : '';
822	  document.getElementById('vv-ry-ts').textContent = ts ? 'Updated: ' + ts : '';
823	}
824
825	function vvRyLoad() {
826	  fetch('/plugins/varaverk/api/rsync.php')
827	    .then(r => r.json())
828	    .then(_render)
829	    .catch(() => {});
830	}
831
832	vvRyLoad();
833	setInterval(vvRyLoad, 30000);
834
835	})();
836
837	// ── Window card expand/collapse ───────────────────────────────────────────────
838	function vvRyWinExpand(card) {
839	  const key   = card.dataset.winKey;
840	  const panel = card.querySelector('.vv-ry-win-panel');
841	  const chev  = card.querySelector('.vv-ry-win-chev');
842	  if (!panel) return;
843	  const open = panel.style.display !== 'none';
844	  panel.style.display = open ? 'none' : '';
845	  if (chev) chev.textContent = open ? '▸' : '▾';
846	  if (key)  open ? _vvRyWinOpen.delete(key) : _vvRyWinOpen.add(key);
847	}
848
849	// ── Window card edit ──────────────────────────────────────────────────────────
850
851	function vvRyWinEdit(key) {
852	  const cfg = (_vvRyLastData?.win_arrays || {})[key] || {};
853	  _vvRyWinEState[key] = {
854	    scripts: (cfg.scripts || []).map(s => ({id: s, enabled: true})),

function _render(data) {
  _vvRyLastData = data;
  if (_vvRyEditMode) return;   // don't clobber the edit UI during a poll

  let html = '';
  html += _statusCard(data);
  html += _lastSyncCard(data);
  html += _windowsRow(data);
  html += _bwSection(data);
  html += _settingsSection(data);
  document.getElementById('vv-ry-grid').innerHTML = html;

  // Restore any window panels that were open before the re-render
  _vvRyWinOpen.forEach(key => {
    const card = document.querySelector(`.vv-ry-win[data-win-key="${key}"]`);
    if (!card) return;
    const panel = card.querySelector('.vv-ry-win-panel');
    const chev  = card.querySelector('.vv-ry-win-chev');
    if (panel) panel.style.display = '';
    if (chev)  chev.textContent    = '▾';
  });

  const ts = data.ts
    ? new Date(data.ts * 1000).toLocaleString([], {
        month:'numeric', day:'numeric', year:'numeric',
        hour:'2-digit', minute:'2-digit', second:'2-digit'})
    : '';
  document.getElementById('vv-ry-ts').textContent = ts ? 'Updated: ' + ts : '';
}

function vvRyLoad() {
  fetch('/plugins/varaverk/api/rsync.php')
    .then(r => r.json())
    .then(_render)
    .catch(() => {});
}

vvRyLoad();
setInterval(vvRyLoad, 30000);

})();

// ── Window card expand/collapse ───────────────────────────────────────────────
function vvRyWinExpand(card) {
  const key   = card.dataset.winKey;
  const panel = card.querySelector('.vv-ry-win-panel');
  const chev  = card.querySelector('.vv-ry-win-chev');
  if (!panel) return;
  const open = panel.style.display !== 'none';
  panel.style.display = open ? 'none' : '';
  if (chev) chev.textContent = open ? '▸' : '▾';
  if (key)  open ? _vvRyWinOpen.delete(key) : _vvRyWinOpen.add(key);
}

// ── Window card edit ──────────────────────────────────────────────────────────

function vvRyWinEdit(key) {
  const cfg = (_vvRyLastData?.win_arrays || {})[key] || {};
  _vvRyWinEState[key] = {
    scripts: (cfg.scripts || []).map(s => ({id: s, enabled: true})),


734	function _settingsSection(data) {
735	  const w  = data.windows  || {};
736	  const s  = data.settings || {};
737	  const en = data.enabled;
738
739	  const mbit = s.bw_limit ? ' (~' + (s.bw_limit / 125).toFixed(0) + ' Mbit/s)' : '';
740
741	  return `

742
Settings

743
744

745
Global gate

746	      ${_toggle('RSYNC_ENABLED', en, 'RSYNC_ENABLED — blocks all syncs when off')}
747

748
749

750
751

752
Sync windows

753

754	        ${_toggle('CRITICAL_RSYNC_ENABLED',     w.critical,     'Critical')}
755	        ${_toggle('INTERMEDIATE_RSYNC_ENABLED', w.intermediate, 'Intermediate')}
756	        ${_toggle('DAILY_RSYNC_ENABLED',        w.daily,        'Daily')}
757	        ${_toggle('WEEKLY_RSYNC_ENABLED',       w.weekly,       'Weekly')}
758	        ${_toggle('FALLBACK_RSYNC_ENABLED',     w.fallback,     'Fallback')}
759

760

761
762

763
764

765
Defaults

766

767	        Bandwidth limit
768	        <input class="vv-ry-set-inp" id="vv-ry-bwlimit" type="number" min="0" value="${s.bw_limit || 0}">
769	        KB/s${mbit}
770

771

772	        Retry count
773	        <input class="vv-ry-set-inp" id="vv-ry-retry" type="number" min="0" max="20" value="${s.retry_count ?? 3}">
774	        attempts
775

776

777	        Retry delay
778	        <input class="vv-ry-set-inp" id="vv-ry-sleep" type="number" min="0" value="${s.sleep ?? 300}">
779	        seconds between attempts
780

781

782	        BW warn threshold
783	        <input class="vv-ry-set-inp" id="vv-ry-bwwarn" type="number" min="0" value="${s.bw_warn_gb ?? 50}">
784	        GB — flag large transfers in weekly report
785

786

787	        <button class="vv-ry-save-btn" id="vv-ry-save" onclick="vvRySaveDefaults()">Save</button>
788
789

790

791
`;
792	}
793
794	// ── Main render ───────────────────────────────────────────────────────────────
795	function _render(data) {
796	  _vvRyLastData = data;
797	  if (_vvRyEditMode) return;   // don't clobber the edit UI during a poll
798

function _settingsSection(data) {
  const w  = data.windows  || {};
  const s  = data.settings || {};
  const en = data.enabled;

  const mbit = s.bw_limit ? ' (~' + (s.bw_limit / 125).toFixed(0) + ' Mbit/s)' : '';

  return `

Settings

Global gate

      ${_toggle('RSYNC_ENABLED', en, 'RSYNC_ENABLED — blocks all syncs when off')}

Sync windows

        ${_toggle('CRITICAL_RSYNC_ENABLED',     w.critical,     'Critical')}
        ${_toggle('INTERMEDIATE_RSYNC_ENABLED', w.intermediate, 'Intermediate')}
        ${_toggle('DAILY_RSYNC_ENABLED',        w.daily,        'Daily')}
        ${_toggle('WEEKLY_RSYNC_ENABLED',       w.weekly,       'Weekly')}
        ${_toggle('FALLBACK_RSYNC_ENABLED',     w.fallback,     'Fallback')}

Defaults

        Bandwidth limit
        <input class="vv-ry-set-inp" id="vv-ry-bwlimit" type="number" min="0" value="${s.bw_limit || 0}">
        KB/s${mbit}

        Retry count
        <input class="vv-ry-set-inp" id="vv-ry-retry" type="number" min="0" max="20" value="${s.retry_count ?? 3}">
        attempts

        Retry delay
        <input class="vv-ry-set-inp" id="vv-ry-sleep" type="number" min="0" value="${s.sleep ?? 300}">
        seconds between attempts

        BW warn threshold
        <input class="vv-ry-set-inp" id="vv-ry-bwwarn" type="number" min="0" value="${s.bw_warn_gb ?? 50}">
        GB — flag large transfers in weekly report

        <button class="vv-ry-save-btn" id="vv-ry-save" onclick="vvRySaveDefaults()">Save</button>

`;
}

// ── Main render ───────────────────────────────────────────────────────────────
function _render(data) {
  _vvRyLastData = data;
  if (_vvRyEditMode) return;   // don't clobber the edit UI during a poll


546	function _statusCard(data) {
547	  const en    = data.enabled;
548	  const dotC  = en ? '#4caf50' : '#ef5350';
549	  const active = data.active || [];
550
551	  let activeHtml;
552	  if (active.length === 0) {
553	    activeHtml = '
No transfers running
';
554	  } else {
555	    activeHtml = active.map(a =>
556	      `

557	        RUNNING
558	        ${a.profile}
559	        ${_dur(a.elapsed)}
560
`
561	    ).join('');
562	  }
563
564	  return `

565

566
567
568	        ${en ? 'RSYNC ENABLED' : 'RSYNC DISABLED'}
569
570
571	        ${en ? 'global gate open' : 'all syncs blocked'}
572
573

574

575
Active transfers

576	    ${activeHtml}
577
`;
578	}
579
580	// ── Last sync summary card ────────────────────────────────────────────────────
581	function _lastSyncCard(data) {
582	  const ls = data.last_sync || {};
583	  const wins = ['critical','intermediate','daily','weekly'];
584	  const rows = wins.map(k => {
585	    const e = ls[k];
586	    if (!e || !e.ts) return `

587	      ${WIN_META[k]?.label || k}
588	      never
589
`;
590	    const cls = e.status === 'success' ? 'color:#4caf50' : 'color:#ef5350';
591	    return `

592	      ${WIN_META[k]?.label || k}
593
594	        ${e.status}
595	        ${_relTime(e.ts)}
596	        ${e.duration ? `<span style="color:#3a3a3a;margin-left:4px;">${_dur(e.duration)}</span>` : ''}
597
598
`;
599	  });
600
601	  return `

602
Last completed run

603	    ${rows.join('')}
604
`;
605	}
606
607	// ── Window cards row ──────────────────────────────────────────────────────────
608	function _windowsRow(data) {
609	  const windows  = data.windows    || {};
610	  const lastSync = data.last_sync  || {};
611	  const winArr   = data.win_arrays || {};
612
613	  const cards = Object.entries(WIN_META).map(([key, meta]) => {
614	    const enabled = windows[key] !== false;
615	    const ls      = lastSync[key];

function _statusCard(data) {
  const en    = data.enabled;
  const dotC  = en ? '#4caf50' : '#ef5350';
  const active = data.active || [];

  let activeHtml;
  if (active.length === 0) {
    activeHtml = '
No transfers running
';
  } else {
    activeHtml = active.map(a =>
      `

        RUNNING
        ${a.profile}
        ${_dur(a.elapsed)}

`
    ).join('');
  }

  return `

        ${en ? 'RSYNC ENABLED' : 'RSYNC DISABLED'}

        ${en ? 'global gate open' : 'all syncs blocked'}

Active transfers

    ${activeHtml}

`;
}

// ── Last sync summary card ────────────────────────────────────────────────────
function _lastSyncCard(data) {
  const ls = data.last_sync || {};
  const wins = ['critical','intermediate','daily','weekly'];
  const rows = wins.map(k => {
    const e = ls[k];
    if (!e || !e.ts) return `

      ${WIN_META[k]?.label || k}
      never

`;
    const cls = e.status === 'success' ? 'color:#4caf50' : 'color:#ef5350';
    return `

      ${WIN_META[k]?.label || k}

        ${e.status}
        ${_relTime(e.ts)}
        ${e.duration ? `<span style="color:#3a3a3a;margin-left:4px;">${_dur(e.duration)}</span>` : ''}

`;
  });

  return `

Last completed run

    ${rows.join('')}

`;
}

// ── Window cards row ──────────────────────────────────────────────────────────
function _windowsRow(data) {
  const windows  = data.windows    || {};
  const lastSync = data.last_sync  || {};
  const winArr   = data.win_arrays || {};

  const cards = Object.entries(WIN_META).map(([key, meta]) => {
    const enabled = windows[key] !== false;
    const ls      = lastSync[key];


652	function _bwSection(data) {
653	  const history = data.bw_history || [];
654	  if (!history.length) {
655	    return `

656
Bandwidth history

657
No data in last 30 days

658
`;
659	  }
660
661	  // Per-profile stats
662	  const profileMap = {};
663	  const profileOrder = [];
664	  for (const r of history) {
665	    if (!profileMap[r.profile]) {
666	      profileMap[r.profile] = { runs: 0, success: 0, totalDur: 0, totalBytes: 0 };
667	      profileOrder.push(r.profile);
668	    }
669	    const p = profileMap[r.profile];
670	    p.runs++;
671	    if (r.status === 'success') p.success++;
672	    p.totalDur   += r.duration;
673	    p.totalBytes += r.bytes;
674	  }
675
676	  const maxDur = Math.max(...Object.values(profileMap).map(p => p.totalDur), 1);
677
678	  const profRows = profileOrder.map((name, i) => {
679	    const p    = profileMap[name];
680	    const col  = PROF_COLORS[i % PROF_COLORS.length];
681	    const pct  = Math.round((p.totalDur / maxDur) * 100);
682	    const rate = Math.round((p.success / p.runs) * 100);
683	    const byteStr = p.totalBytes > 0 ? _fmtBytes(p.totalBytes) : _dur(p.totalDur) + ' total';
684	    return `

685	      ${name}
686

687	      ${p.runs} runs · ${byteStr}
688
`;
689	  });
690
691	  // Recent runs (last 20, newest first)
692	  const recent = [...history].reverse().slice(0, 20);
693	  const runRows = recent.map(r => {
694	    const stCls = r.status === 'success' ? 'on' : 'err';
695	    const durStr = r.duration >= 3600
696	      ? Math.floor(r.duration / 3600) + 'h ' + Math.floor((r.duration % 3600) / 60) + 'm'
697	      : r.duration >= 60
698	        ? Math.floor(r.duration / 60) + 'm'
699	        : r.duration + 's';
700	    return `

701	      ${r.date}
702	      ${r.time}
703	      ${r.profile}
704	      ${durStr}
705	      ${r.status}
706
`;
707	  });
708
709	  return `

710

711
Profile activity — last 30 days

712	      ${profRows.join('')}
713

714

715
Recent runs

716

717	        DateTimeProfileDuration
718

719
${runRows.join('')}

720

721
`;
722	}
723
724	// ── Settings section ──────────────────────────────────────────────────────────
725	function _toggle(name, currentVal, label) {
726	  const id = 'vv-ry-tog-' + name;
727	  return `
728

730	    ${label}
731	  `;
732	}
733
734	function _settingsSection(data) {
735	  const w  = data.windows  || {};
736	  const s  = data.settings || {};

function _bwSection(data) {
  const history = data.bw_history || [];
  if (!history.length) {
    return `

Bandwidth history

No data in last 30 days

`;
  }

  // Per-profile stats
  const profileMap = {};
  const profileOrder = [];
  for (const r of history) {
    if (!profileMap[r.profile]) {
      profileMap[r.profile] = { runs: 0, success: 0, totalDur: 0, totalBytes: 0 };
      profileOrder.push(r.profile);
    }
    const p = profileMap[r.profile];
    p.runs++;
    if (r.status === 'success') p.success++;
    p.totalDur   += r.duration;
    p.totalBytes += r.bytes;
  }

  const maxDur = Math.max(...Object.values(profileMap).map(p => p.totalDur), 1);

  const profRows = profileOrder.map((name, i) => {
    const p    = profileMap[name];
    const col  = PROF_COLORS[i % PROF_COLORS.length];
    const pct  = Math.round((p.totalDur / maxDur) * 100);
    const rate = Math.round((p.success / p.runs) * 100);
    const byteStr = p.totalBytes > 0 ? _fmtBytes(p.totalBytes) : _dur(p.totalDur) + ' total';
    return `

      ${name}

      ${p.runs} runs · ${byteStr}

`;
  });

  // 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 durStr = r.duration >= 3600
      ? Math.floor(r.duration / 3600) + 'h ' + Math.floor((r.duration % 3600) / 60) + 'm'
      : r.duration >= 60
        ? Math.floor(r.duration / 60) + 'm'
        : r.duration + 's';
    return `

      ${r.date}
      ${r.time}
      ${r.profile}
      ${durStr}
      ${r.status}

`;
  });

  return `

Profile activity — last 30 days

      ${profRows.join('')}

Recent runs

        DateTimeProfileDuration

${runRows.join('')}

`;
}

// ── Settings section ──────────────────────────────────────────────────────────
function _toggle(name, currentVal, label) {
  const id = 'vv-ry-tog-' + name;
  return `

    ${label}
  `;
}

function _settingsSection(data) {
  const w  = data.windows  || {};
  const s  = data.settings || {};


The grid is 8 columns. Settings is currently `grid-column:1/-1` (full width). I'll pin it to the far right (`grid-column:6/-1` = columns 6–8) and tighten the inner toggle grid to 2 columns so the 5 window toggles wrap cleanly at that width.


Edit

.vv-ry-set-grid { display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin-top:6px; }
@media (max-width:900px) { .vv-ry-set-grid { grid-template-columns:repeat(2,1fr); } }

.vv-ry-set-grid { display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-top:6px; }


The file /boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php has been updated successfully. (file state is current in your context — no need to Read it back)

.vv-ry-set-grid { display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin-top:6px; }
@media (max-width:900px) { .vv-ry-set-grid { grid-template-columns:repeat(2,1fr); } }

-.vv-ry-set-grid { display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin-top:6px; }

-@media (max-width:900px) { .vv-ry-set-grid { grid-template-columns:repeat(2,1fr); } }

+.vv-ry-set-grid { display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-top:6px; }

.vv-ry-toggle  { display:inline-flex;align-items:center;gap:8px;cursor:pointer;user-select:none; }

.vv-ry-toggle-track { width:32px;height:18px;border-radius:9px;background:#222;border:1px solid #333;position:relative;transition:background .15s,border-color .15s;flex-shrink:0; }

.vv-ry-toggle-track.on { background:#1a3a1a;border-color:#2d5a2d; }


const mbit = s.bw_limit ? ' (~' + (s.bw_limit / 125).toFixed(0) + ' Mbit/s)' : '';

-  return `<div class="vv-ry-card" style="grid-column:1/-1;">

+  return `<div class="vv-ry-card" style="grid-column:6/-1;">


Done. Settings card is now `grid-column: 6 / -1` — the rightmost 3 columns of the 8-column grid, in its own row below the bandwidth section. The toggle grid is now 2 columns so the 5 window toggles wrap 2+2+1 instead of cramming 5 across.


Fixed the auth page stack across three sessions: NPM auth, lldap queries, Authelia ACL parser (now pure PHP), certs tab auto-pulling from NPM, watchdog page JS errors, and rsync settings card repositioned. No open issues — the page is ready to test. (disable recaps in /config)


push everything


Bash

cd /boot/config/plugins/varaverk && git status && git diff --stat

Check git status and diff summary


On branch main
Your branch is up to date with 'origin/main'.

You are in a sparse checkout with 100% of tracked files present.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   Fallback/fallback.sh
	modified:   Orchestrators/array_started.sh
	modified:   Plugin/unraid/System_Essentials/unraid_api_key_renew.sh
	modified:   Plugin/unraid/api/cert.php
	modified:   Plugin/unraid/api/stop.php
	modified:   Plugin/unraid/include/auth.php
	modified:   Plugin/unraid/include/config.php
	modified:   Plugin/unraid/js/varaverk.js
	modified:   Plugin/unraid/pages/arrs.php
	modified:   Plugin/unraid/pages/auth.php
	modified:   Plugin/unraid/pages/docker.php
	modified:   Plugin/unraid/pages/fallback.php
	modified:   Plugin/unraid/pages/partnership.php
	modified:   Plugin/unraid/pages/rsync.php
	modified:   Plugin/unraid/pages/setup.php
	modified:   Plugin/unraid/pages/watchdog.php
	modified:   System_Essentials/conf_sync.sh
	modified:   Tools/claude_startup.sh
	modified:   common.sh

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	CLAUDE.md

no changes added to commit (use "git add" and/or "git commit -a")
 Fallback/fallback.sh                               |   4 +-
 Orchestrators/array_started.sh                     |   7 +
 .../System_Essentials/unraid_api_key_renew.sh      |  19 +-
 Plugin/unraid/api/cert.php                         |  38 ++++
 Plugin/unraid/api/stop.php                         |  13 +-
 Plugin/unraid/include/auth.php                     | 227 ++++++++++++++++-----
 Plugin/unraid/include/config.php                   |   4 +-
 Plugin/unraid/js/varaverk.js                       |  41 ++++
 Plugin/unraid/pages/arrs.php                       |  33 +--
 Plugin/unraid/pages/auth.php                       |  61 +++---
 Plugin/unraid/pages/docker.php                     |   6 +-
 Plugin/unraid/pages/fallback.php                   |   5 +-
 Plugin/unraid/pages/partnership.php                |  20 +-
 Plugin/unraid/pages/rsync.php                      |  26 +--
 Plugin/unraid/pages/setup.php                      |   4 +-
 Plugin/unraid/pages/watchdog.php                   |  28 +--
 System_Essentials/conf_sync.sh                     |   6 +
 Tools/claude_startup.sh                            |  11 +
 common.sh                                          |   5 +-
 19 files changed, 384 insertions(+), 174 deletions(-)


Check recent commit style


9c3ace9 Auth stack certs tab, arrs db fallbacks, cert monitor cache, conf parser fix
17012bc refactor: move unraid-specific tools into Plugin/unraid/tools/
bdcdfea feat: build smart_long_test.sh — SMART extended self-test for monthly maintenance
c441ac4 feat: add --pull-only to conf_sync, wire into intermediate orchestrator
f9fe13b fix: use STATE_DIR directly — TRANSCODE_STATE_FILE contains unexpanded bash var


git add Plugin/unraid/api/cert.php Plugin/unraid/include/auth.php Plugin/unraid/pages/auth.php Plugin/unraid/pages/watchdog.php Plugin/unraid/pages/rsync.php CLAUDE.md Tools/claude_startup.sh

Stage the files changed this session


(Bash completed with no output)


Confirm what's staged


 CLAUDE.md                        | 151 ++++++++++++++++++++++++++
 Plugin/unraid/api/cert.php       |  38 +++++++
 Plugin/unraid/include/auth.php   | 227 +++++++++++++++++++++++++++++++--------
 Plugin/unraid/pages/auth.php     |  61 +++++------
 Plugin/unraid/pages/rsync.php    |  26 +----
 Plugin/unraid/pages/watchdog.php |  28 ++---
 Tools/claude_startup.sh          |  11 ++
 7 files changed, 427 insertions(+), 115 deletions(-)


git commit -m "$(cat <<'EOF'
Auth page: pure-PHP Authelia ACL parser, NPM-sourced certs tab, watchdog JS fixes, rsync settings layout

- Replace python3/PyYAML Authelia ACL parser with pure PHP (no deps available on Unraid)
- Cert tab now pulls live from NPM API instead of cert_monitor.sh — auto-discovers all managed certs sorted by urgency
- Watchdog page: add missing GB constant and _fmtBytes/_relTime functions that were causing silent render failure
- Rsync settings card: pin to far-right 3 columns (grid-column:6/-1), toggle grid narrowed to 2 columns
- Add CLAUDE.md project context file on /boot for session persistence across reboots
- claude_startup.sh: symlink CLAUDE.md into /root on array start
EOF
)"

Commit all session changes


[main bac0f7c] Auth page: pure-PHP Authelia ACL parser, NPM-sourced certs tab, watchdog JS fixes, rsync settings layout
 7 files changed, 427 insertions(+), 115 deletions(-)
 create mode 100644 CLAUDE.md


Push to remote


remote: . Processing 1 references
remote: Processed 1 references in total
To ssh://192.168.50.2:221/FailedProxy/Varaverk.git
   9c3ace9..bac0f7c  main -> main


Pushed. 7 files, 1 new (CLAUDE.md). The other 12 modified files in the working tree are from earlier sessions — left unstaged since you didn't ask for those.


left of setting lets creat a log window that shown live running syncs, and keeps showing that last ran sync, then on new sync we see live running sync.... etc


Let me read the API and current state data structure before designing this:


php /boot/config/plugins/varaverk/Plugin/unraid/api/rsync.php 2>/dev/null | python3 -m json.tool 2>/dev/null | grep -A3 '"active"\|"last_sync"\|"running"\|"log"\|"output"' | head -40

Check what active/log data the rsync API returns


php /boot/config/plugins/varaverk/Plugin/unraid/api/rsync.php 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(list(d.keys()))"

Get top-level keys from rsync API


Error: Exit code 127
/bin/bash: line 1: python3: command not found


php /boot/config/plugins/varaverk/Plugin/unraid/api/rsync.php 2>/dev/null | php -r "
\$d = json_decode(file_get_contents('php://stdin'), true);
echo 'Keys: ' . implode(', ', array_keys(\$d)) . PHP_EOL;
echo 'active: ' . json_encode(\$d['active'] ?? null) . PHP_EOL;
echo 'last_sync keys: ' . implode(', ', array_keys(\$d['last_sync'] ?? [])) . PHP_EOL;
"

Inspect rsync API response structure


Keys: enabled, windows, active, last_sync, bw_history, bw_warn_gb, win_arrays, settings, ts
active: []
last_sync keys:


Read the rsync API to understand data sources


<?php
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';
require_once dirname(__DIR__) . '/include/monitor.php';

$base = vv_rsync_status();
$vars = vv_conf_vars();

$base['windows']['fallback'] = ($vars['FALLBACK_RSYNC_ENABLED'] ?? 'true') !== 'false';

// Bandwidth history — last 30 days
$bwLog    = DATA_DIR . '/bandwidth_history.db';
$warnGb   = (float)($vars['BANDWIDTH_WARN_GB'] ?? 50);
$cutoff   = date('Y-m-d', strtotime('-30 days'));
$history  = [];

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;
        if ($p[0] < $cutoff) continue;
        $history[] = [
            'date'     => $p[0],
            'time'     => $p[1],
            'profile'  => $p[2],
            'duration' => (int)$p[3],
            'status'   => trim($p[4]),
            'bytes'    => isset($p[5]) ? (int)$p[5] : 0,
        ];
    }
}

// Per-window orch arrays (scripts + sync shares)
$masterRaw = vv_read_conf_raw('master.conf');
$myId      = strtoupper(vv_detect_host());
$hostRaw   = vv_read_conf_raw(vv_detect_host() . '.conf');

$winArrayDefs = [
    'critical'     => ['CRITICAL_MAINTENANCE_SCRIPTS',     "{$myId}_CRITICAL_SYNC_SHARES"],
    'intermediate' => ['INTERMEDIATE_MAINTENANCE_SCRIPTS', "{$myId}_INTERMEDIATE_SYNC_SHARES"],
    'daily'        => ['DAILY_MAINTENANCE_SCRIPTS',        "{$myId}_DAILY_SYNC_SHARES"],
    'weekly'       => ['WEEKLY_MAINTENANCE_SCRIPTS',       "{$myId}_WEEKLY_SYNC_SHARES"],
    'fallback'     => [null,                               null],
];
$winArrays = [];
foreach ($winArrayDefs as $win => [$sv, $shv]) {
    $winArrays[$win] = [
        'scripts' => $sv  ? vv_parse_bash_array($masterRaw, $sv)  : [],
        'shares'  => $shv ? vv_parse_bash_array($hostRaw,   $shv) : [],
    ];
}

echo json_encode([
    'enabled'    => $base['enabled'],
    'windows'    => $base['windows'],
    'active'     => $base['active'],
    'last_sync'  => $base['last_sync'],
    'bw_history' => $history,
    'bw_warn_gb' => $warnGb,
    'win_arrays' => $winArrays,
    'settings'   => [
        'bw_limit'     => (int)($vars['BW_LIMIT']          ?? 0),
        'retry_count'  => (int)($vars['RETRY_COUNT']        ?? 3),
        'sleep'        => (int)($vars['SLEEP']              ?? 300),
        'bw_warn_gb'   => (float)($vars['BANDWIDTH_WARN_GB'] ?? 50),
    ],
    'ts'         => time(),
]);


grep -n "vv_rsync_status\|active\|last_sync\|running\|log\|token" /boot/config/plugins/varaverk/Plugin/unraid/include/monitor.php | head -40

Find rsync status function and how active syncs are tracked


91:function vv_fallback_active(): array {
105:    $running = [];
109:        if ($c) $running[strtolower($c['n'])] = $c['i'];
126:        $active = [];
128:            if (isset($running[strtolower($name)]))
129:                $active[] = ['name' => $name, 'image' => $running[strtolower($name)]];
132:        if ($active) $result[] = ['host_id' => $covered, 'hostname' => $coveredHostname, 'containers' => $active];
195:    $growthStrikes = []; $logStrikes = [];
205:        elseif (str_starts_with($key, 'appdata_log_'))
206:            $logStrikes[substr($key, strlen('appdata_log_'))] = $count;
240:    $healthy = empty($ctrStrikes) && empty($stabStrikes) && empty($growthStrikes) && empty($logStrikes)
257:            'log_pct'     => $dfPct('/var/log'),
270:            'log_strikes'    => $logStrikes,
279:    $logDir    = LOG_DIR;
281:        glob("$logDir/*.json")   ?: [],
282:        glob("$logDir/*/*.json") ?: []
290:        // Stale running — PID gone (crash or reboot with no cleanup)
291:        if ($status === 'running' && !empty($stat['pid'])) {
304:            'running'  => $status === 'running',
316:        'running_count' => count(array_filter($scripts, fn($s) => $s['status'] === 'running')),
323:function vv_rsync_status(): array {
337:    $active  = [];
343:        $active[] = [
350:    // Last completed run per window (orchestrator log files)
359:        $logFile = LOG_DIR . "/$scriptName.json";
360:        if (!file_exists($logFile)) continue;
361:        $stat = json_decode(@file_get_contents($logFile) ?: '{}', true) ?: [];
392:        'active'     => $active,
393:        'last_sync'  => $lastSync,


323	function vv_rsync_status(): array {
324	    $vars = vv_conf_vars();
325
326	    $enabled = ($vars['RSYNC_ENABLED'] ?? 'true') !== 'false';
327
328	    $windows = [
329	        'critical'     => ($vars['CRITICAL_RSYNC_ENABLED']     ?? 'false') !== 'false',
330	        'daily'        => ($vars['DAILY_RSYNC_ENABLED']        ?? 'false') !== 'false',
331	        'intermediate' => ($vars['INTERMEDIATE_RSYNC_ENABLED'] ?? 'true')  !== 'false',
332	        'weekly'       => ($vars['WEEKLY_RSYNC_ENABLED']       ?? 'false') !== 'false',
333	    ];
334
335	    // Active rsync profiles — from lock files
336	    $lockDir = '/tmp/unraid_locks';
337	    $active  = [];
338	    foreach (glob("$lockDir/rsync_*.lock") ?: [] as $lf) {
339	        $content = trim(@file_get_contents($lf) ?: '');
340	        [$pid, $locked_name] = array_pad(explode(':', $content, 2), 2, '');
341	        if (!$pid || !file_exists("/proc/$pid")) continue;
342	        $profile  = preg_replace('/^rsync_/', '', $locked_name ?: basename($lf, '.lock'));
343	        $active[] = [
344	            'profile' => $profile,
345	            'pid'     => (int)$pid,
346	            'elapsed' => time() - (int)filemtime($lf),
347	        ];
348	    }
349
350	    // Last completed run per window (orchestrator log files)
351	    $scriptMap = [
352	        'critical'     => 'critical_sync_maintenance',
353	        'daily'        => 'daily_sync_maintenance',
354	        'intermediate' => 'intermediate_sync_maintenance',
355	        'weekly'       => 'weekly_sync_maintenance',
356	    ];
357	    $lastSync = [];
358	    foreach ($scriptMap as $key => $scriptName) {
359	        $logFile = LOG_DIR . "/$scriptName.json";
360	        if (!file_exists($logFile)) continue;
361	        $stat = json_decode(@file_get_contents($logFile) ?: '{}', true) ?: [];
362	        $lastSync[$key] = [
363	            'ts'       => (int)($stat['end'] ?? $stat['start'] ?? 0),
364	            'status'   => $stat['status'] ?? 'unknown',
365	            'duration' => isset($stat['start'], $stat['end'])
366	                ? (int)$stat['end'] - (int)$stat['start'] : null,
367	        ];
368	    }
369
370	    // Profile activity — last 7 days, aggregated per profile
371	    $bwLog   = DATA_DIR . '/bandwidth_history.db';
372	    $cutoff7 = date('Y-m-d', strtotime('-7 days'));
373	    $profiles = [];
374	    if (file_exists($bwLog)) {
375	        foreach (file($bwLog, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
376	            $p = explode('|', $line);
377	            if (count($p) < 4 || ($p[0] ?? '') < $cutoff7) continue;
378	            $name = $p[2] ?? '';
379	            if (!$name) continue;
380	            if (!isset($profiles[$name])) $profiles[$name] = ['runs' => 0, 'dur' => 0, 'bytes' => 0];
381	            $profiles[$name]['runs']++;
382	            $profiles[$name]['dur']   += (int)($p[3] ?? 0);
383	            $profiles[$name]['bytes'] += (int)($p[5] ?? 0);
384	        }
385	    }
386	    arsort($profiles); // sort by run count descending
387	    $bwSummary = array_slice($profiles, 0, 6, true);
388
389	    return [
390	        'enabled'    => $enabled,
391	        'windows'    => $windows,
392	        'active'     => $active,
393	        'last_sync'  => $lastSync,
394	        'bw_summary' => $bwSummary,
395	    ];
396	}
397

function vv_rsync_status(): array {
    $vars = vv_conf_vars();

    $enabled = ($vars['RSYNC_ENABLED'] ?? 'true') !== 'false';

    $windows = [
        'critical'     => ($vars['CRITICAL_RSYNC_ENABLED']     ?? 'false') !== 'false',
        'daily'        => ($vars['DAILY_RSYNC_ENABLED']        ?? 'false') !== 'false',
        'intermediate' => ($vars['INTERMEDIATE_RSYNC_ENABLED'] ?? 'true')  !== 'false',
        'weekly'       => ($vars['WEEKLY_RSYNC_ENABLED']       ?? 'false') !== 'false',
    ];

    // Active rsync profiles — from lock files
    $lockDir = '/tmp/unraid_locks';
    $active  = [];
    foreach (glob("$lockDir/rsync_*.lock") ?: [] as $lf) {
        $content = trim(@file_get_contents($lf) ?: '');
        [$pid, $locked_name] = array_pad(explode(':', $content, 2), 2, '');
        if (!$pid || !file_exists("/proc/$pid")) continue;
        $profile  = preg_replace('/^rsync_/', '', $locked_name ?: basename($lf, '.lock'));
        $active[] = [
            'profile' => $profile,
            'pid'     => (int)$pid,
            'elapsed' => time() - (int)filemtime($lf),
        ];
    }

    // Last completed run per window (orchestrator log files)
    $scriptMap = [
        'critical'     => 'critical_sync_maintenance',
        'daily'        => 'daily_sync_maintenance',
        'intermediate' => 'intermediate_sync_maintenance',
        'weekly'       => 'weekly_sync_maintenance',
    ];
    $lastSync = [];
    foreach ($scriptMap as $key => $scriptName) {
        $logFile = LOG_DIR . "/$scriptName.json";
        if (!file_exists($logFile)) continue;
        $stat = json_decode(@file_get_contents($logFile) ?: '{}', true) ?: [];
        $lastSync[$key] = [
            'ts'       => (int)($stat['end'] ?? $stat['start'] ?? 0),
            'status'   => $stat['status'] ?? 'unknown',
            'duration' => isset($stat['start'], $stat['end'])
                ? (int)$stat['end'] - (int)$stat['start'] : null,
        ];
    }

    // Profile activity — last 7 days, aggregated per profile
    $bwLog   = DATA_DIR . '/bandwidth_history.db';
    $cutoff7 = date('Y-m-d', strtotime('-7 days'));
    $profiles = [];
    if (file_exists($bwLog)) {
        foreach (file($bwLog, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
            $p = explode('|', $line);
            if (count($p) < 4 || ($p[0] ?? '') < $cutoff7) continue;
            $name = $p[2] ?? '';
            if (!$name) continue;
            if (!isset($profiles[$name])) $profiles[$name] = ['runs' => 0, 'dur' => 0, 'bytes' => 0];
            $profiles[$name]['runs']++;
            $profiles[$name]['dur']   += (int)($p[3] ?? 0);
            $profiles[$name]['bytes'] += (int)($p[5] ?? 0);
        }
    }
    arsort($profiles); // sort by run count descending
    $bwSummary = array_slice($profiles, 0, 6, true);

    return [
        'enabled'    => $enabled,
        'windows'    => $windows,
        'active'     => $active,
        'last_sync'  => $lastSync,
        'bw_summary' => $bwSummary,
    ];
}


# Check how rsync.sh writes its output — find the log/output file for a running sync
ls /tmp/unraid_locks/ 2>/dev/null && echo "---" || echo "no locks dir"
# Check what log files rsync writes during a run
grep -rn "log\|output\|tee\|>/tmp\|STATE_DIR\|LOCK\|lock" /boot/config/plugins/varaverk/Rsync/rsync.sh 2>/dev/null | grep -v "^#" | head -30

Find where rsync.sh writes live output


fallback.lock
rsync_active_count
---
/boot/config/plugins/varaverk/Rsync/rsync.sh:16:# After each sync, logs transfer data to bandwidth_monitor.sh for the weekly
/boot/config/plugins/varaverk/Rsync/rsync.sh:17:# bandwidth report. Silent on success — only failures produce visible output.
/boot/config/plugins/varaverk/Rsync/rsync.sh:39:# Bandwidth logging: after each sync, logs profile/duration/status/bytes to
/boot/config/plugins/varaverk/Rsync/rsync.sh:40:# bandwidth_monitor.sh --log-transfer. Bytes captured from rsync --stats via awk
/boot/config/plugins/varaverk/Rsync/rsync.sh:50:# Partnership Blocklist
/boot/config/plugins/varaverk/Rsync/rsync.sh:51:#   Refuses to sync if REMOTE_SERVER_NAME appears in the partnership blocklist.
/boot/config/plugins/varaverk/Rsync/rsync.sh:72:#   acquire_rsync_lock() — per-profile lock prevents parallel runs of the same profile.
/boot/config/plugins/varaverk/Rsync/rsync.sh:128:# rsync.sh /path/to/share --log
/boot/config/plugins/varaverk/Rsync/rsync.sh:129:#     Verbose output throughout — every decision logged.
/boot/config/plugins/varaverk/Rsync/rsync.sh:154:    error "Usage: rsync.sh <dir> [--dry-run] [--log] [--profile=name]"
/boot/config/plugins/varaverk/Rsync/rsync.sh:166:acquire_lock
/boot/config/plugins/varaverk/Rsync/rsync.sh:186:# Blocklist gate — refuse to sync with a partner blocked after offboard
/boot/config/plugins/varaverk/Rsync/rsync.sh:187:BLOCKLIST_FILE="${PARTNERSHIP_BLOCKLIST_FILE:-${STATE_DIR:-/boot/config}/partnership_blocklist.db}"
/boot/config/plugins/varaverk/Rsync/rsync.sh:188:if [[ -f "$BLOCKLIST_FILE" ]] && grep -q "^${REMOTE_SERVER_NAME}|" "$BLOCKLIST_FILE" 2>/dev/null; then
/boot/config/plugins/varaverk/Rsync/rsync.sh:189:    error "Rsync blocked — $REMOTE_SERVER_NAME is on the partnership blocklist"
/boot/config/plugins/varaverk/Rsync/rsync.sh:199:    log "Profile override: $PROFILE_NAME"
/boot/config/plugins/varaverk/Rsync/rsync.sh:202:    log "Profile inferred: $PROFILE_NAME"
/boot/config/plugins/varaverk/Rsync/rsync.sh:205:# Acquire per-profile lock and check global concurrent limit
/boot/config/plugins/varaverk/Rsync/rsync.sh:206:acquire_rsync_lock "$PROFILE_NAME"
/boot/config/plugins/varaverk/Rsync/rsync.sh:222:log "$ICON_GEAR Config: profile=${PROFILE_NAME} bw-limit=${BW_LIMIT}KB/s retry=${RETRY_COUNT} sleep=${SLEEP}s container-delay=${CONTAINER_DELAY}s"
/boot/config/plugins/varaverk/Rsync/rsync.sh:223:log "$ICON_GEAR Containers: critical=${CRITICAL_CONTAINER_NAMES[*]:-none} delayed=${DELAYED_CONTAINERS[*]:-none} remote-restart=${REMOTE_RESTART_CONTAINERS[*]:-none}"
/boot/config/plugins/varaverk/Rsync/rsync.sh:245:    log "Drive temps OK — $TEMP_CHECK_RESULT"
/boot/config/plugins/varaverk/Rsync/rsync.sh:306:# Add --stats to capture bytes transferred for bandwidth logging
/boot/config/plugins/varaverk/Rsync/rsync.sh:317:    log "$ICON_RETRY Attempt $ATTEMPT of $RETRY_COUNT..."
/boot/config/plugins/varaverk/Rsync/rsync.sh:327:        # Parse bytes transferred from --stats output
/boot/config/plugins/varaverk/Rsync/rsync.sh:332:        log "$ICON_DONE Rsync complete — $BYTES_TRANSFERRED bytes transferred"
/boot/config/plugins/varaverk/Rsync/rsync.sh:337:        log "Exit code: $RSYNC_EXIT"
/boot/config/plugins/varaverk/Rsync/rsync.sh:339:            log "Retrying in ${SLEEP}s..."
/boot/config/plugins/varaverk/Rsync/rsync.sh:375:    log "Restarting configured containers on $REMOTE_SERVER_NAME..."
/boot/config/plugins/varaverk/Rsync/rsync.sh:392:                log "$container not running on $REMOTE_SERVER_NAME — skipping remote restart" && \


grep -n "LOG_FILE\|log_file\|STATE_DIR\|>/tmp\|tee " /boot/config/plugins/varaverk/Rsync/rsync.sh | head -20
echo "---"
grep -n "acquire_rsync_lock\|LOCK_FILE\|lock_file" /boot/config/plugins/varaverk/Rsync/rsync.sh | head -20

Find the lock/log file paths rsync.sh uses


187:BLOCKLIST_FILE="${PARTNERSHIP_BLOCKLIST_FILE:-${STATE_DIR:-/boot/config}/partnership_blocklist.db}"
---
72:#   acquire_rsync_lock() — per-profile lock prevents parallel runs of the same profile.
206:acquire_rsync_lock "$PROFILE_NAME"


grep -n "acquire_rsync_lock\|lock\|LOCK" /boot/config/plugins/varaverk/common.sh | head -30

Find the lock file mechanism in common.sh


19:#   Locking          — acquire_lock, acquire_rsync_lock — concurrent execution protection
34:#            Function header comment blocks standardised across all functions
47:#            Script locking system added — acquire_lock(), acquire_rsync_lock()
155:ICON_UNLOCKED="🔓"      # permissions successfully applied to a share
156:ICON_LOCK="🔏"          # script instance lock — acquired/released
849:# Checks /sys/block/<device>/queue/rotational — 0=SSD, 1=HDD.
853:    [[ "$(cat "/sys/block/$base/queue/rotational" 2>/dev/null)" == "0" ]]
1318:# ── SCRIPT LOCKING ────────────────────────────────────────────────────────────────────────────
1321:# All lock files live in /tmp/unraid_locks/ — auto-cleared on reboot.
1325:#   wait        — wait LOCK_WAIT_TIMEOUT seconds then exit if still locked
1329:#   Stale lock detection — if lock PID is dead, clears and proceeds
1330:#   PID reuse protection — lock stores PID:scriptname, validates both
1331:#   Age warning — warns if lock older than LOCK_WARN_AGE (skipped for continuous)
1332:#   EXIT trap — lock always released on exit, crash, or kill signal
1334:LOCK_DIR="/tmp/unraid_locks"
1345:RSYNC_COUNT_FILE="$LOCK_DIR/rsync_active_count"
1347:LOCK_WARN_AGE=300       # seconds — warn if lock older than this (5min default)
1348:LOCK_WAIT_TIMEOUT=30    # seconds — how long "wait" mode waits before giving up
1350:# Registry of all lock files acquired by this process — released together on exit.
1351:# Prevents the single-trap-per-acquire problem: each new acquire_lock/acquire_rsync_lock
1352:# call used to overwrite the EXIT trap, orphaning the previous lock file.
1353:declare -ga _LOCK_FILES=()
1354:_release_all_locks() {
1356:    for lf in "${_LOCK_FILES[@]}"; do
1360:_register_lock() {
1361:    _LOCK_FILES+=("$1")
1362:    trap '_release_all_locks' EXIT
1365:# Internal — script name used as lock identifier
1366:_lock_name() {
1370:# Internal — lock file path for this script


grep -n "acquire_rsync_lock\|RSYNC_LOG\|rsync_log\|output_file\|OUT_FILE" /boot/config/plugins/varaverk/common.sh | head -20
echo "---"
grep -n "RSYNC_LOG\|rsync_log\|>/tmp.*rsync\|output_file" /boot/config/plugins/varaverk/Rsync/rsync.sh | head -20

Find if rsync.sh writes a live output file


19:#   Locking          — acquire_lock, acquire_rsync_lock — concurrent execution protection
47:#            Script locking system added — acquire_lock(), acquire_rsync_lock()
1351:# Prevents the single-trap-per-acquire problem: each new acquire_lock/acquire_rsync_lock
1462:# Usage: acquire_rsync_lock "$PROFILE_NAME"
1463:acquire_rsync_lock() {
---


ls /mnt/user/logs/varaverk/ 2>/dev/null | grep -i rsync | head -20
echo "---"
ls /tmp/unraid_locks/ 2>/dev/null
echo "---"
# Check if rsync.sh writes any live output somewhere
grep -n "log\b\|tee\|redirect\|>/\|>>" /boot/config/plugins/varaverk/Rsync/rsync.sh | grep -v "^#\|log(" | head -20

Find existing rsync log files and check if rsync.sh tees output


---
fallback.lock
rsync_active_count
---
40:# bandwidth_monitor.sh --log-transfer. Bytes captured from rsync --stats via awk
128:# rsync.sh /path/to/share --log
154:    error "Usage: rsync.sh <dir> [--dry-run] [--log] [--profile=name]"
168:if ! command -v docker &>/dev/null; then
188:if [[ -f "$BLOCKLIST_FILE" ]] && grep -q "^${REMOTE_SERVER_NAME}|" "$BLOCKLIST_FILE" 2>/dev/null; then
199:    log "Profile override: $PROFILE_NAME"
202:    log "Profile inferred: $PROFILE_NAME"
222:log "$ICON_GEAR Config: profile=${PROFILE_NAME} bw-limit=${BW_LIMIT}KB/s retry=${RETRY_COUNT} sleep=${SLEEP}s container-delay=${CONTAINER_DELAY}s"
223:log "$ICON_GEAR Containers: critical=${CRITICAL_CONTAINER_NAMES[*]:-none} delayed=${DELAYED_CONTAINERS[*]:-none} remote-restart=${REMOTE_RESTART_CONTAINERS[*]:-none}"
245:    log "Drive temps OK — $TEMP_CHECK_RESULT"
317:    log "$ICON_RETRY Attempt $ATTEMPT of $RETRY_COUNT..."
332:        log "$ICON_DONE Rsync complete — $BYTES_TRANSFERRED bytes transferred"
337:        log "Exit code: $RSYNC_EXIT"
339:            log "Retrying in ${SLEEP}s..."
375:    log "Restarting configured containers on $REMOTE_SERVER_NAME..."
390:                "docker inspect -f '{{.State.Running}}' $container 2>/dev/null" 2>/dev/null)
392:                log "$container not running on $REMOTE_SERVER_NAME — skipping remote restart" && \
402:            "docker restart $container" >/dev/null 2>&1 && \
403:            log "$ICON_STARTED $container restarted on $REMOTE_SERVER_NAME ✅" || \
421:    bash "$BANDWIDTH_MONITOR" --log-transfer \


# Check what log() function writes to in rsync context
grep -n "^log()\|^log ()\|^function log\|LOG_FILE\|VARAVERK_LOG\|log_file\b" /boot/config/plugins/varaverk/common.sh | head -20
echo "---"
# Check what log files the orchestrators produce
ls /mnt/user/logs/varaverk/ 2>/dev/null | head -20

Find where log() writes and existing log files


208:log() {
---


208	log() {
209	    [[ "${ENABLE_LOGGING:-false}" == true ]] && echo "[LOG] $*"
210	    return 0
211	}
212
213	# ==============================================================================================
214	# ── NOTIFICATION ──────────────────────────────────────────────────────────────────────────────
215	# ==============================================================================================
216	# Sends a notification via unRAID native system and/or Discord webhook.
217	# Both channels are optional and independently controlled:
218	#   NOTIFY_UNRAID — shared toggle in master.conf
219	#   MY_DISCORD_WEBHOOK — per-host in host*.conf, set by detect_hosts()
220	#
221	# Severity levels: normal, warning, alert
222	# Usage: notify "message" "subject" "severity"
223
224	notify() {
225	    local message="$1"
226	    local subject="${2:-unRAID Notification}"
227	    local severity="${3:-normal}"

log() {
    [[ "${ENABLE_LOGGING:-false}" == true ]] && echo "[LOG] $*"
    return 0
}

# ==============================================================================================
# ── NOTIFICATION ──────────────────────────────────────────────────────────────────────────────
# ==============================================================================================
# Sends a notification via unRAID native system and/or Discord webhook.
# Both channels are optional and independently controlled:
#   NOTIFY_UNRAID — shared toggle in master.conf
#   MY_DISCORD_WEBHOOK — per-host in host*.conf, set by detect_hosts()
#
# Severity levels: normal, warning, alert
# Usage: notify "message" "subject" "severity"

notify() {
    local message="$1"
    local subject="${2:-unRAID Notification}"
    local severity="${3:-normal}"


# Check the rsync_standalone api - the manual rsync uses token polling, maybe it writes a file
grep -n "token\|output\|log\|file\|write" /boot/config/plugins/varaverk/Plugin/unraid/api/rsync_standalone.php | head -30

Check rsync_standalone for token/output file pattern


(Bash completed with no output)


Read the standalone rsync API


<?php
// Save rsync standalone config (location + cron) for a specific rsync tier.
// POST: flag_name (e.g. "DAILY_RSYNC_ENABLED"), orch_id, location, cron
// Stored in schedule.json under "__rsync_{FLAG_NAME}".
// Triggers a cron rebuild so the standalone entry takes effect immediately.
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';
require_once dirname(__DIR__) . '/include/scheduler.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    echo json_encode(['ok' => false, 'error' => 'POST only']);
    exit;
}

$flagName = trim($_POST['flag_name'] ?? '');
$orchId   = trim($_POST['orch_id']   ?? '');
$location = trim($_POST['location']  ?? '');
$cron     = trim($_POST['cron']      ?? '');

if (!$flagName || !preg_match('/^[A-Z_]+_RSYNC_ENABLED$/', $flagName)) {
    echo json_encode(['ok' => false, 'error' => 'Invalid flag_name']);
    exit;
}
if ($orchId && (str_contains($orchId, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $orchId))) {
    echo json_encode(['ok' => false, 'error' => 'Invalid orch_id']);
    exit;
}
if ($location && (!str_starts_with($location, '/') || str_contains($location, '..') || preg_match('/[\x00\n\r]/', $location))) {
    echo json_encode(['ok' => false, 'error' => 'Invalid location']);
    exit;
}

$key      = '__rsync_' . $flagName;
$schedule = vv_schedule_load();
$schedule[$key] = [
    'flag_name' => $flagName,
    'orch_id'   => $orchId,
    'location'  => $location,
    'cron'      => $cron,
];
if (!vv_schedule_save($schedule)) {
    echo json_encode(['ok' => false, 'error' => 'Write failed']);
    exit;
}

vv_cron_rebuild($schedule);
echo json_encode(['ok' => true]);


140	RAW_ARGS=()
141
142	for ARG in "$@"; do
143	    case "$ARG" in
144	        --profile=*) PROFILE_OVERRIDE="${ARG#--profile=}" ;;
145	        --*|*=*)     RAW_ARGS+=("$ARG") ;;
146	        *)           [[ -z "$DIRECTORY" ]] && DIRECTORY="$ARG" || RAW_ARGS+=("$ARG") ;;
147	    esac
148	done
149
150	parse_args "${RAW_ARGS[@]}"
151
152	[[ -z "$DIRECTORY" ]] && {
153	    error "No directory specified"
154	    error "Usage: rsync.sh <dir> [--dry-run] [--log] [--profile=name]"
155	    exit 1
156	}
157
158	# ==============================================================================================
159	# ━━━ Setup ━━━
160	# ==============================================================================================
161	if [[ "$EUID" -ne 0 ]]; then
162	    error "Must be run as root"
163	    exit 1
164	fi
165
166	acquire_lock
167
168	if ! command -v docker &>/dev/null; then
169	    error "Docker command not found"
170	    exit 1
171	fi
172
173	platform_require_cmd \
174	    "/usr/local/emhttp/plugins/dynamix/scripts/notify" \
175	    "" "" \
176	    "unRAID notify script" || warn "unRAID notify script not found — native notifications disabled"
177
178	detect_hosts
179
180	# Tier 1 global gate — Tier 2 (per-orchestrator) checked by caller
181	if ! check_rsync_enabled; then
182	    warn "RSYNC_ENABLED=false — exiting cleanly"
183	    exit 0
184	fi
185
186	# Blocklist gate — refuse to sync with a partner blocked after offboard
187	BLOCKLIST_FILE="${PARTNERSHIP_BLOCKLIST_FILE:-${STATE_DIR:-/boot/config}/partnership_blocklist.db}"
188	if [[ -f "$BLOCKLIST_FILE" ]] && grep -q "^${REMOTE_SERVER_NAME}|" "$BLOCKLIST_FILE" 2>/dev/null; then
189	    error "Rsync blocked — $REMOTE_SERVER_NAME is on the partnership blocklist"
190	    error "Re-onboard the partnership to restore access: partnership_manager.sh --onboard"
191	    exit 1
192	fi
193
194	resolve_remote_ip
195
196	# ── Profile inference ─────────────────────────────────────────────────────────────────────────
197	if [[ -n "$PROFILE_OVERRIDE" ]]; then
198	    PROFILE_NAME="$PROFILE_OVERRIDE"
199	    log "Profile override: $PROFILE_NAME"
200	else
201	    PROFILE_NAME=$(basename "$DIRECTORY" | tr '[:upper:]' '[:lower:]')
202	    log "Profile inferred: $PROFILE_NAME"
203	fi
204
205	# Acquire per-profile lock and check global concurrent limit
206	acquire_rsync_lock "$PROFILE_NAME"
207
208	# ── Load profile settings ─────────────────────────────────────────────────────────────────────
209	BW_LIMIT=${PROFILE_BW_LIMIT[$PROFILE_NAME]:-$BW_LIMIT}
210	RETRY_COUNT=${PROFILE_RETRY_COUNT[$PROFILE_NAME]:-$RETRY_COUNT}
211	SLEEP=${PROFILE_SLEEP[$PROFILE_NAME]:-$SLEEP}
212	CONTAINER_DELAY=${PROFILE_CONTAINER_DELAY[$PROFILE_NAME]:-$CONTAINER_DELAY}
213
214	read -r -a CRITICAL_CONTAINER_NAMES       <<< "${PROFILE_CRITICAL_CONTAINER_NAMES[$PROFILE_NAME]:-}"
215	read -r -a DELAYED_CONTAINERS             <<< "${PROFILE_DELAYED_CONTAINERS[$PROFILE_NAME]:-}"
216	read -r -a EXCLUDE_DIRS                   <<< "${PROFILE_EXCLUDE_DIRS[$PROFILE_NAME]:-}"
217	read -r -a REMOTE_RESTART_CONTAINERS      <<< "${PROFILE_REMOTE_RESTART_CONTAINERS[$PROFILE_NAME]:-}"
218
219	# Local containers use same names as remote (mirrored naming scheme)
220	LOCAL_CRITICAL_CONTAINER_NAMES=("${CRITICAL_CONTAINER_NAMES[@]}")
221
222	log "$ICON_GEAR Config: profile=${PROFILE_NAME} bw-limit=${BW_LIMIT}KB/s retry=${RETRY_COUNT} sleep=${SLEEP}s container-delay=${CONTAINER_DELAY}s"
223	log "$ICON_GEAR Containers: critical=${CRITICAL_CONTAINER_NAMES[*]:-none} delayed=${DELAYED_CONTAINERS[*]:-none} remote-restart=${REMOTE_RESTART_CONTAINERS[*]:-none}"
224
225	[[ "$SHOW_STATUS" == true ]] && show_status && exit 0
226	[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
227
228	# ==============================================================================================
229	# ━━━ Pre-flight Checks ━━━
230	# ==============================================================================================
231	echo ""
232	echo "━━━ $ICON_SHIELD Pre-flight ━━━"
233
234	# Disk temp — before touching remote or moving data
235	# Exit 1 = skip this profile | Exit 2 = abort all remaining profiles
236	check_local_disk_temps
237	TEMP_RESULT=$?
238	if [[ "$TEMP_RESULT" -eq 2 ]]; then
239	    error "Drive temps CRITICAL — aborting all remaining syncs"

RAW_ARGS=()

for ARG in "$@"; do
    case "$ARG" in
        --profile=*) PROFILE_OVERRIDE="${ARG#--profile=}" ;;
        --*|*=*)     RAW_ARGS+=("$ARG") ;;
        *)           [[ -z "$DIRECTORY" ]] && DIRECTORY="$ARG" || RAW_ARGS+=("$ARG") ;;
    esac
done

parse_args "${RAW_ARGS[@]}"

[[ -z "$DIRECTORY" ]] && {
    error "No directory specified"
    error "Usage: rsync.sh <dir> [--dry-run] [--log] [--profile=name]"
    exit 1
}

# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
    error "Must be run as root"
    exit 1
fi

acquire_lock

if ! command -v docker &>/dev/null; then
    error "Docker command not found"
    exit 1
fi

platform_require_cmd \
    "/usr/local/emhttp/plugins/dynamix/scripts/notify" \
    "" "" \
    "unRAID notify script" || warn "unRAID notify script not found — native notifications disabled"

detect_hosts

# Tier 1 global gate — Tier 2 (per-orchestrator) checked by caller
if ! check_rsync_enabled; then
    warn "RSYNC_ENABLED=false — exiting cleanly"
    exit 0
fi

# Blocklist gate — refuse to sync with a partner blocked after offboard
BLOCKLIST_FILE="${PARTNERSHIP_BLOCKLIST_FILE:-${STATE_DIR:-/boot/config}/partnership_blocklist.db}"
if [[ -f "$BLOCKLIST_FILE" ]] && grep -q "^${REMOTE_SERVER_NAME}|" "$BLOCKLIST_FILE" 2>/dev/null; then
    error "Rsync blocked — $REMOTE_SERVER_NAME is on the partnership blocklist"
    error "Re-onboard the partnership to restore access: partnership_manager.sh --onboard"
    exit 1
fi

resolve_remote_ip

# ── Profile inference ─────────────────────────────────────────────────────────────────────────
if [[ -n "$PROFILE_OVERRIDE" ]]; then
    PROFILE_NAME="$PROFILE_OVERRIDE"
    log "Profile override: $PROFILE_NAME"
else
    PROFILE_NAME=$(basename "$DIRECTORY" | tr '[:upper:]' '[:lower:]')
    log "Profile inferred: $PROFILE_NAME"
fi

# Acquire per-profile lock and check global concurrent limit
acquire_rsync_lock "$PROFILE_NAME"

# ── Load profile settings ─────────────────────────────────────────────────────────────────────
BW_LIMIT=${PROFILE_BW_LIMIT[$PROFILE_NAME]:-$BW_LIMIT}
RETRY_COUNT=${PROFILE_RETRY_COUNT[$PROFILE_NAME]:-$RETRY_COUNT}
SLEEP=${PROFILE_SLEEP[$PROFILE_NAME]:-$SLEEP}
CONTAINER_DELAY=${PROFILE_CONTAINER_DELAY[$PROFILE_NAME]:-$CONTAINER_DELAY}

read -r -a CRITICAL_CONTAINER_NAMES       <<< "${PROFILE_CRITICAL_CONTAINER_NAMES[$PROFILE_NAME]:-}"
read -r -a DELAYED_CONTAINERS             <<< "${PROFILE_DELAYED_CONTAINERS[$PROFILE_NAME]:-}"
read -r -a EXCLUDE_DIRS                   <<< "${PROFILE_EXCLUDE_DIRS[$PROFILE_NAME]:-}"
read -r -a REMOTE_RESTART_CONTAINERS      <<< "${PROFILE_REMOTE_RESTART_CONTAINERS[$PROFILE_NAME]:-}"

# Local containers use same names as remote (mirrored naming scheme)
LOCAL_CRITICAL_CONTAINER_NAMES=("${CRITICAL_CONTAINER_NAMES[@]}")

log "$ICON_GEAR Config: profile=${PROFILE_NAME} bw-limit=${BW_LIMIT}KB/s retry=${RETRY_COUNT} sleep=${SLEEP}s container-delay=${CONTAINER_DELAY}s"
log "$ICON_GEAR Containers: critical=${CRITICAL_CONTAINER_NAMES[*]:-none} delayed=${DELAYED_CONTAINERS[*]:-none} remote-restart=${REMOTE_RESTART_CONTAINERS[*]:-none}"

[[ "$SHOW_STATUS" == true ]] && show_status && exit 0
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"

# ==============================================================================================
# ━━━ Pre-flight Checks ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_SHIELD Pre-flight ━━━"

# Disk temp — before touching remote or moving data
# Exit 1 = skip this profile | Exit 2 = abort all remaining profiles
check_local_disk_temps
TEMP_RESULT=$?
if [[ "$TEMP_RESULT" -eq 2 ]]; then
    error "Drive temps CRITICAL — aborting all remaining syncs"


Got a clear picture. Active syncs tracked via lock files, history in `bandwidth_history.db` — both already in the main API response. No changes to rsync.sh needed. I'll:

- Add a `_syncLogCard` at `grid-column: span 5` (pairs with settings at `6/-1` in row 4)
- Show live RUNNING rows with client-side elapsed ticker
- Show recent history below
- Fast-poll every 5s while a sync is active, back to 30s when idle


46	/* Settings */
47	.vv-ry-set-grid { display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-top:6px; }
48	.vv-ry-toggle  { display:inline-flex;align-items:center;gap:8px;cursor:pointer;user-select:none; }
49	.vv-ry-toggle-track { width:32px;height:18px;border-radius:9px;background:#222;border:1px solid #333;position:relative;transition:background .15s,border-color .15s;flex-shrink:0; }
50	.vv-ry-toggle-track.on { background:#1a3a1a;border-color:#2d5a2d; }
51	.vv-ry-toggle-track::after { content:'';position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:#555;transition:left .15s,background .15s; }
52	.vv-ry-toggle-track.on::after { left:16px;background:#4caf50; }
53	.vv-ry-toggle-lbl { font-size:11px;color:#666; }
54	.vv-ry-set-row { display:flex;align-items:center;gap:10px;margin:4px 0; }
55	.vv-ry-set-lbl { font-size:11px;color:#555;width:130px;flex-shrink:0; }
56	.vv-ry-set-inp { background:#111;border:1px solid #2a2a2a;border-radius:3px;color:#bbb;font-size:12px;padding:3px 7px;width:80px;outline:none; }
57	.vv-ry-set-inp:focus { border-color:#444; }
58	.vv-ry-set-hint { font-size:10px;color:#3a3a3a; }
59	.vv-ry-save-btn { background:#1a2a1a;border:1px solid #2d4a2d;color:#4caf50;font-size:11px;padding:4px 14px;border-radius:3px;cursor:pointer;transition:background .15s; }
60	.vv-ry-save-btn:hover { background:#223a22; }
61	.vv-ry-save-btn:disabled { opacity:.4;cursor:default; }
62	.vv-ry-feedback { font-size:11px;margin-left:8px; }
63
64	/* ── Info layer ──────────────────────────────────────────────────── */
65	.vv-ry-info-block { border:1px solid #1a1a1a;border-radius:4px;margin-bottom:6px;overflow:hidden; }

/* Settings */
.vv-ry-set-grid { display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-top:6px; }
.vv-ry-toggle  { display:inline-flex;align-items:center;gap:8px;cursor:pointer;user-select:none; }
.vv-ry-toggle-track { width:32px;height:18px;border-radius:9px;background:#222;border:1px solid #333;position:relative;transition:background .15s,border-color .15s;flex-shrink:0; }
.vv-ry-toggle-track.on { background:#1a3a1a;border-color:#2d5a2d; }
.vv-ry-toggle-track::after { content:'';position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:#555;transition:left .15s,background .15s; }
.vv-ry-toggle-track.on::after { left:16px;background:#4caf50; }
.vv-ry-toggle-lbl { font-size:11px;color:#666; }
.vv-ry-set-row { display:flex;align-items:center;gap:10px;margin:4px 0; }
.vv-ry-set-lbl { font-size:11px;color:#555;width:130px;flex-shrink:0; }
.vv-ry-set-inp { background:#111;border:1px solid #2a2a2a;border-radius:3px;color:#bbb;font-size:12px;padding:3px 7px;width:80px;outline:none; }
.vv-ry-set-inp:focus { border-color:#444; }
.vv-ry-set-hint { font-size:10px;color:#3a3a3a; }
.vv-ry-save-btn { background:#1a2a1a;border:1px solid #2d4a2d;color:#4caf50;font-size:11px;padding:4px 14px;border-radius:3px;cursor:pointer;transition:background .15s; }
.vv-ry-save-btn:hover { background:#223a22; }
.vv-ry-save-btn:disabled { opacity:.4;cursor:default; }
.vv-ry-feedback { font-size:11px;margin-left:8px; }

/* ── Info layer ──────────────────────────────────────────────────── */
.vv-ry-info-block { border:1px solid #1a1a1a;border-radius:4px;margin-bottom:6px;overflow:hidden; }


Edit

/* Settings */
.vv-ry-set-grid { display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-top:6px; }

/* Sync log card */
.vv-ry-slog-row  { display:grid;grid-template-columns:38px 1fr 70px 50px 46px;gap:6px;align-items:center;
                   padding:3px 0;border-bottom:1px solid #111; }
.vv-ry-slog-row:last-child { border-bottom:none; }
.vv-ry-slog-name { font-size:11px;color:#888;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
.vv-ry-slog-dt   { font-size:10px;color:#3a3a3a; }
.vv-ry-slog-dur  { font-size:10px;color:#555;text-align:right; }
.vv-ry-slog-byt  { font-size:10px;color:#555;text-align:right; }
.vv-ry-slog-scrl { overflow-y:auto;max-height:200px; }

/* Settings */
.vv-ry-set-grid { display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-top:6px; }


The file /boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php has been updated successfully. (file state is current in your context — no need to Read it back)

/* Sync log card */
.vv-ry-slog-row  { display:grid;grid-template-columns:38px 1fr 70px 50px 46px;gap:6px;align-items:center;
                   padding:3px 0;border-bottom:1px solid #111; }
.vv-ry-slog-row:last-child { border-bottom:none; }
.vv-ry-slog-name { font-size:11px;color:#888;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
.vv-ry-slog-dt   { font-size:10px;color:#3a3a3a; }
.vv-ry-slog-dur  { font-size:10px;color:#555;text-align:right; }
.vv-ry-slog-byt  { font-size:10px;color:#555;text-align:right; }
.vv-ry-slog-scrl { overflow-y:auto;max-height:200px; }

/* Settings */
.vv-ry-set-grid { display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-top:6px; }

.vv-ry-run-dur { font-size:10px;color:#555;text-align:right; }

+.vv-ry-slog-row  { display:grid;grid-template-columns:38px 1fr 70px 50px 46px;gap:6px;align-items:center;

+                   padding:3px 0;border-bottom:1px solid #111; }

+.vv-ry-slog-row:last-child { border-bottom:none; }

+.vv-ry-slog-name { font-size:11px;color:#888;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }

+.vv-ry-slog-dt   { font-size:10px;color:#3a3a3a; }

+.vv-ry-slog-dur  { font-size:10px;color:#555;text-align:right; }

+.vv-ry-slog-byt  { font-size:10px;color:#555;text-align:right; }

+.vv-ry-slog-scrl { overflow-y:auto;max-height:200px; }


Now add the card function and wire it into `_render`:


// ── Settings section ──────────────────────────────────────────────────────────

// ── Sync log card ─────────────────────────────────────────────────────────────
let _liveTicker   = null;
let _fastPollTimer = null;

function _syncLogCard(data) {
  const active  = data.active      || [];
  const history = data.bw_history  || [];
  const recent  = [...history].reverse().slice(0, 14);
  const now     = Math.floor(Date.now() / 1000);

  const activeRows = active.map(a => {
    const startedTs = now - (a.elapsed || 0);
    return `

      LIVE
      ${_esc(a.profile)}
      ${_dur(a.elapsed || 0)}

`;
  }).join('');

  const histRows = recent.map(r => {
    const cls = r.status === 'success' ? 'on' : 'err';
    const bytes = r.bytes > 0 ? _fmtBytes(r.bytes) : '';
    return `

      ${r.status === 'success' ? 'ok' : 'fail'}
      ${_esc(r.profile)}
      ${r.date} ${r.time.slice(0,5)}
      ${_dur(r.duration)}
      ${bytes}

`;
  }).join('');

  return `

Sync Activity

    ${activeRows}
    ${activeRows && histRows ? '
' : ''}

      ${histRows || '
No sync history
'}

`;
}

// ── Settings section ──────────────────────────────────────────────────────────


// ── Sync log card ─────────────────────────────────────────────────────────────
let _liveTicker   = null;
let _fastPollTimer = null;

function _syncLogCard(data) {
  const active  = data.active      || [];
  const history = data.bw_history  || [];
  const recent  = [...history].reverse().slice(0, 14);
  const now     = Math.floor(Date.now() / 1000);

  const activeRows = active.map(a => {
    const startedTs = now - (a.elapsed || 0);
    return `

      LIVE
      ${_esc(a.profile)}
      ${_dur(a.elapsed || 0)}

`;
  }).join('');

  const histRows = recent.map(r => {
    const cls = r.status === 'success' ? 'on' : 'err';
    const bytes = r.bytes > 0 ? _fmtBytes(r.bytes) : '';
    return `

      ${r.status === 'success' ? 'ok' : 'fail'}
      ${_esc(r.profile)}
      ${r.date} ${r.time.slice(0,5)}
      ${_dur(r.duration)}
      ${bytes}

`;
  }).join('');

  return `

Sync Activity

    ${activeRows}
    ${activeRows && histRows ? '
' : ''}

      ${histRows || '
No sync history
'}

`;
}

// ── Settings section ──────────────────────────────────────────────────────────

+// ── Sync log card ─────────────────────────────────────────────────────────────

+  const active  = data.active      || [];

+  const history = data.bw_history  || [];

+  const recent  = [...history].reverse().slice(0, 14);

+  const now     = Math.floor(Date.now() / 1000);

+    const startedTs = now - (a.elapsed || 0);

+    return `<div class="vv-ry-slog-row" style="background:#0a1a0a;border-radius:3px;margin-bottom:4px;padding:4px 6px;border:none;">

+      <span class="vv-ry-elapsed" data-started="${startedTs}"

+            style="font-size:11px;color:#4a9eff;font-family:monospace;grid-column:3/span 3;text-align:right;">${_dur(a.elapsed || 0)}</span>

+    const cls = r.status === 'success' ? 'on' : 'err';

+    const bytes = r.bytes > 0 ? _fmtBytes(r.bytes) : '';

+    return `<div class="vv-ry-slog-row">

+      ${r.status === 'success' ? 'ok' : 'fail'}

+  return `<div class="vv-ry-card" style="grid-column:span 5;display:flex;flex-direction:column;min-height:0;">

+    ${activeRows && histRows ? '<hr class="vv-ry-sep">' : ''}

+      ${histRows || '
No sync history
'}

function _toggle(name, currentVal, label) {


Now wire it into `_render` and add the live ticker + fast poll logic:


  let html = '';
  html += _statusCard(data);
  html += _lastSyncCard(data);
  html += _windowsRow(data);
  html += _bwSection(data);
  html += _settingsSection(data);
  document.getElementById('vv-ry-grid').innerHTML = html;

  let html = '';
  html += _statusCard(data);
  html += _lastSyncCard(data);
  html += _windowsRow(data);
  html += _bwSection(data);
  html += _syncLogCard(data);
  html += _settingsSection(data);
  document.getElementById('vv-ry-grid').innerHTML = html;

  // Live elapsed ticker — updates every second without re-fetching
  if (_liveTicker) { clearInterval(_liveTicker); _liveTicker = null; }
  if ((data.active || []).length) {
    _liveTicker = setInterval(() => {
      document.querySelectorAll('.vv-ry-elapsed').forEach(el => {
        const s = parseInt(el.dataset.started, 10);
        if (s) el.textContent = _dur(Math.floor(Date.now() / 1000) - s);
      });
    }, 1000);
  }

  // Fast-poll (5s) while syncs are active; return to 30s when idle
  const hasActive = (data.active || []).length > 0;
  if (hasActive && !_fastPollTimer) {
    _fastPollTimer = setInterval(vvRyLoad, 5000);
  } else if (!hasActive && _fastPollTimer) {
    clearInterval(_fastPollTimer);
    _fastPollTimer = null;
  }


  let html = '';
  html += _statusCard(data);
  html += _lastSyncCard(data);
  html += _windowsRow(data);
  html += _bwSection(data);
  html += _settingsSection(data);
  document.getElementById('vv-ry-grid').innerHTML = html;

  let html = '';
  html += _statusCard(data);
  html += _lastSyncCard(data);
  html += _windowsRow(data);
  html += _bwSection(data);
  html += _syncLogCard(data);
  html += _settingsSection(data);
  document.getElementById('vv-ry-grid').innerHTML = html;

  // Live elapsed ticker — updates every second without re-fetching
  if (_liveTicker) { clearInterval(_liveTicker); _liveTicker = null; }
  if ((data.active || []).length) {
    _liveTicker = setInterval(() => {
      document.querySelectorAll('.vv-ry-elapsed').forEach(el => {
        const s = parseInt(el.dataset.started, 10);
        if (s) el.textContent = _dur(Math.floor(Date.now() / 1000) - s);
      });
    }, 1000);
  }

  // Fast-poll (5s) while syncs are active; return to 30s when idle
  const hasActive = (data.active || []).length > 0;
  if (hasActive && !_fastPollTimer) {
    _fastPollTimer = setInterval(vvRyLoad, 5000);
  } else if (!hasActive && _fastPollTimer) {
    clearInterval(_fastPollTimer);
    _fastPollTimer = null;
  }

+  // Live elapsed ticker — updates every second without re-fetching

+  if (_liveTicker) { clearInterval(_liveTicker); _liveTicker = null; }

+      document.querySelectorAll('.vv-ry-elapsed').forEach(el => {

+        const s = parseInt(el.dataset.started, 10);

+        if (s) el.textContent = _dur(Math.floor(Date.now() / 1000) - s);

+  // Fast-poll (5s) while syncs are active; return to 30s when idle

+  const hasActive = (data.active || []).length > 0;

+    _fastPollTimer = setInterval(vvRyLoad, 5000);

+  } else if (!hasActive && _fastPollTimer) {

// Restore any window panels that were open before the re-render

const card = document.querySelector(`.vv-ry-win[data-win-key="${key}"]`);


Let me verify the grid columns are consistent — settings at `6/-1`, log card `span 5`:


Bash

grep -n "grid-column" /boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php | grep -v "css\|style>\|/\*\|bw\|wins\|set-grid\|run-row\|slog-row\|info-cols\|1/-1\|1 / -1"

Check all explicit grid-column placements in rendered cards


573:  return `

610:  return `

749:            style="font-size:11px;color:#4a9eff;font-family:monospace;grid-column:3/span 3;text-align:right;">${_dur(a.elapsed || 0)}
765:  return `

792:  return `


Grid is clean:
- Row 1: `span 4` + `span 4` = 8 ✓
- Row 2: windows `1/-1` ✓
- Row 3: bw `1/-1` ✓
- Row 4: sync log `span 5` + settings `6/-1` = columns 1–5 + 6–8 ✓


git add Plugin/unraid/pages/rsync.php && git commit -m "Rsync page: add live sync activity log card left of settings"

Commit the sync log card


[main 4bb6bbf] Rsync page: add live sync activity log card left of settings
 1 file changed, 73 insertions(+)


Push to remote


remote: . Processing 1 references
remote: Processed 1 references in total
To ssh://192.168.50.2:221/FailedProxy/Varaverk.git
   bac0f7c..4bb6bbf  main -> main


Pushed. The sync log card sits at columns 1–5 in row 4, settings at 6–8.

Behavior:
- **Idle**: shows last 14 sync entries (profile, date/time, duration, bytes transferred)
- **Active sync**: `LIVE` badge row at the top with an elapsed timer ticking every second, history below the divider
- **Poll rate**: drops to 5s automatically while a sync is running, returns to 30s when done


all my cards are missing now


node -e "
const fs = require('fs');
const src = fs.readFileSync('/boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php', 'utf8');
const js = src.match(/<script>([\s\S]*?)<\/script>/)?.[1] || '';
// Check for syntax errors by looking at function boundaries
try {
  // Wrap in function to test syntax
  new Function(js);
  console.log('Syntax OK');
} catch(e) {
  console.log('Syntax ERROR:', e.message);
}
" 2>&1

Check JavaScript syntax in rsync page


Syntax OK


node -e "
const fs = require('fs');
const src = fs.readFileSync('/boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php', 'utf8');
const js = src.match(/<script>([\s\S]*?)<\/script>/)?.[1] || '';
const defs = new Set([
  ...[...js.matchAll(/function (\w+)\s*\(/g)].map(m=>m[1]),
  ...[...js.matchAll(/(?:let|const|var) (\w+)/g)].map(m=>m[1]),
  'Math','Date','document','fetch','setInterval','clearInterval','parseInt','Boolean','Array','Object','console','FormData','parseInt','isNaN','encodeURIComponent'
]);
const calls = [...js.matchAll(/\b([a-z_]\w+)\s*\(/g)].map(m=>m[1]);
const missing = [...new Set(calls)].filter(c => !defs.has(c) && c.length > 2);
console.log('Missing functions:', missing.length ? missing.join(', ') : 'none');
" 2>&1

Check for undefined function calls in rsync page


Missing functions: for, split, slice, join, replace, trim, pop, stopPropagation, function, floor, map, _relTime, entries, push, max, values, round, _fmtBytes, runs, reverse, _esc, toFixed, getElementById, querySelectorAll, forEach, poll, querySelector, toLocaleString, then, json, catch, delete, add, append, stringify, filter, has, splice, find, startsWith, contains, toggle, setTimeout, from, includes, keys, sort, focus, confirm, finally, substring, lastIndexOf, alert


`_fmtBytes`, `_relTime`, and `_esc` are missing from the rsync page script. Let me confirm:


grep -n "function _fmtBytes\|function _relTime\|function _esc\|function _dur" /boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php

Check which helpers are defined in rsync page


546:function _dur(s) {


Those were already called in pre-existing code too — let me check if they come from an included JS file:


grep -n "_fmtBytes\|_relTime\|_esc" /boot/config/plugins/varaverk/Plugin/unraid/js/varaverk.js | head -20
echo "---"
grep -n "varaverk.js\|script src" /boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php | head -10

Check if helpers come from shared JS


8:function _relTime(ts) {
19:function _fmtBytes(b) {
---


grep -n "_esc\|function _esc" /boot/config/plugins/varaverk/Plugin/unraid/js/varaverk.js | head -5
echo "---"
# Find where varaverk.js is loaded and how pages are served
grep -rn "varaverk.js\|script src" /boot/config/plugins/varaverk/Plugin/unraid/ | grep -v ".php~" | head -15

Find where varaverk.js and _esc are loaded


---
/boot/config/plugins/varaverk/Plugin/unraid/Varaverk.page:65:<script src="/plugins/<?=$plugin?>/js/varaverk.js"></script>


grep -n "_esc\|function _esc" /boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php | head -5
echo "---"
grep -rn "function _esc" /boot/config/plugins/varaverk/Plugin/unraid/ | grep -v ".php~" | head -10

Find _esc definition


747:      ${_esc(a.profile)}
758:      ${_esc(r.profile)}
---
/boot/config/plugins/varaverk/Plugin/unraid/pages/docker.php:112:function _esc(s) {
/boot/config/plugins/varaverk/Plugin/unraid/pages/auth.php:245:function _esc(s) {


`_esc` isn't defined in the rsync page — I added calls to it in `_syncLogCard`, which throws a ReferenceError inside `_render` that `.catch(() => {})` silently swallows. That kills all card rendering.


grep -A4 "function _esc" /boot/config/plugins/varaverk/Plugin/unraid/pages/docker.php | head -6

Get _esc implementation


function _esc(s) {
  return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}

function _shortImage(img) {


Edit

let _liveTicker   = null;
let _fastPollTimer = null;

function _esc(s) {
  return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}

let _liveTicker   = null;
let _fastPollTimer = null;


function _esc(s) {
  return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}

let _liveTicker   = null;
let _fastPollTimer = null;

// ── Sync log card ─────────────────────────────────────────────────────────────


Bash

git add Plugin/unraid/pages/rsync.php && git commit -m "Fix rsync page: add missing _esc helper that broke all card rendering" && git push

Commit and push the fix


[main 04cd15a] Fix rsync page: add missing _esc helper that broke all card rendering
 1 file changed, 4 insertions(+)
remote: . Processing 1 references
remote: Processed 1 references in total
To ssh://192.168.50.2:221/FailedProxy/Varaverk.git
   4bb6bbf..04cd15a  main -> main