<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Rsync tab. Sync profiles, share lists, transfer progress, per-run history, and manual
//   sync triggers.
//
// DESIGN PRINCIPLES
//   Pure view — profile and status data from api/rsync.php, profile edits through
//   api/rsync_profiles.php, manual runs through api/manual_sync.php.
//
//   Progress polls fast (2s) while a transfer is live and falls back to the slow interval
//   otherwise, so an idle page is not hammering the endpoint.
//
//   Profiles are edited as structured data, not as raw rsync flags typed by hand.
//
// OPERATIONAL SAFEGUARDS
//   Manual sync is confirmed before it fires. A sync moves real data between two machines and
//   is not something to trigger with a stray click.
//
//   The page shows the global RSYNC_ENABLED gate alongside the per-tier toggles, so a run
//   that will be skipped by the gate is visibly skipped rather than silently doing nothing.
//
//   Heavy escaping throughout — share paths, profile names and rsync output all render as
//   text, never as markup.
//
//   The page does not compose rsync flags. Deletion semantics in particular are owned by
//   Rsync/rsync.sh, where the pass-1-must-complete interlock lives.
//
// RENDERS
//   Profile editor, share lists, live transfer progress, run history, manual sync controls
//
// DEPENDS ON
//   api/rsync.php             status, polled 2s live / 30s idle
//   api/rsync_profiles.php    profile read and write
//   api/manual_sync.php       manual run trigger
//   api/rsync_win_arrays.php  share array editing
//   api/flag_toggle.php       enable/disable toggles
//   api/confform.php          inline conf edits
?>
<style>
.vv-ry-card    { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:10px 12px;min-width:0; }
.vv-ry-sec     { font-size:10px;font-weight:bold;color:#444;letter-spacing:.07em;text-transform:uppercase;margin-bottom:6px; }
.vv-ry-row     { display:flex;justify-content:space-between;align-items:baseline;gap:6px;margin:2px 0; }
.vv-ry-lbl     { font-size:11px;color:#444;white-space:nowrap; }
.vv-ry-val     { font-size:12px;color:#bbb;text-align:right; }
.vv-ry-sep     { border:none;border-top:1px solid #1e1e1e;margin:7px 0; }
.vv-ry-pill    { display:inline-block;font-size:10px;padding:1px 7px;border-radius:3px;font-weight:bold;letter-spacing:.04em; }
.vv-ry-pill.on   { background:#0d1f0d;color:#4caf50;border:1px solid #1a3a1a; }
.vv-ry-pill.off  { background:#1e1e1e;color:#555;border:1px solid #2a2a2a; }
.vv-ry-pill.warn { background:#1f1500;color:#ffb74d;border:1px solid #3a2800; }
.vv-ry-pill.err  { background:#200d0d;color:#ef5350;border:1px solid #3a1a1a; }
.vv-ry-pill.run  { background:#0a1a2a;color:#4a9eff;border:1px solid #1a3a5a; }
.vv-ry-dot     { display:inline-block;width:7px;height:7px;border-radius:50%;flex-shrink:0;margin-right:5px;margin-bottom:-1px; }

/* Window cards row */
.vv-ry-wins    { grid-column:1/-1;display:grid;grid-template-columns:repeat(5,1fr);gap:10px; }
@media (max-width:900px) { .vv-ry-wins { grid-template-columns:repeat(2,1fr); } }

.vv-ry-win     { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:10px 12px; }
.vv-ry-win.enabled  { border-color:#1a3a1a; }
.vv-ry-win.disabled { opacity:.55; }
.vv-ry-win-lbl { font-size:10px;font-weight:bold;letter-spacing:.08em;text-transform:uppercase;color:#555;margin-bottom:6px; }
.vv-ry-win-name{ font-size:15px;font-weight:bold;color:#888;margin-bottom:6px; }
.vv-ry-win-stat{ font-size:11px;color:#3a3a3a;margin-top:4px; }
.vv-ry-win-dur { font-size:10px;color:#3a3a3a; }

/* Bandwidth section */
.vv-ry-bw      { grid-column:1/-1;display:grid;grid-template-columns:5fr 3fr;gap:10px; }
@media (max-width:900px) { .vv-ry-bw { grid-template-columns:1fr; } }
.vv-ry-prof-row{ display:flex;align-items:center;gap:8px;margin:3px 0; }
.vv-ry-prof-nm { font-size:11px;color:#888;width:120px;flex-shrink:0; }
.vv-ry-prof-bar{ flex:1;height:5px;background:#1e1e1e;border-radius:2px;overflow:hidden; }
.vv-ry-prof-fill{ height:100%;border-radius:2px;transition:width .3s; }
.vv-ry-prof-cnt{ font-size:10px;color:#555;width:60px;text-align:right;flex-shrink:0; }
.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; }
.vv-ry-run-row:last-child { border-bottom:none; }
.vv-ry-runs-scroll { max-height:200px;overflow-y:auto;scrollbar-width:none;-ms-overflow-style:none; }
.vv-ry-runs-scroll::-webkit-scrollbar { display:none; }
.vv-ry-run-dt  { font-size:10px;color:#3a3a3a; }
.vv-ry-run-tm  { font-size:10px;color:#333; }
.vv-ry-run-pr  { font-size:11px;color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; }
.vv-ry-run-dur { font-size:10px;color:#555;text-align:right; }
.vv-ry-run-st  { text-align:right; }

/* 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;flex:1;min-height:0; }

/* 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; }
.vv-ry-info-hdr  { padding:7px 12px;background:#111;cursor:pointer;font-size:11px;font-weight:600;
                   color:#666;display:flex;align-items:center;gap:6px;user-select:none; }
.vv-ry-info-hdr:hover { color:#999;background:#141414; }
.vv-ry-info-chev { font-size:10px;color:#333;flex-shrink:0; }
.vv-ry-info-body { padding:10px 14px;background:#0d0d0d; }
.vv-ry-info-cols { display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:8px 20px; }
.vv-ry-info-cols > div { font-size:11px;color:#555;line-height:1.5; }
.vv-ry-info-cols strong { color:#777; }
.vv-ry-info-cols code { font-size:10px;color:#4a7a4a;background:#080808;padding:1px 4px;border-radius:2px;font-family:monospace; }

/* ── Profile editor flag checkboxes ─────────────────────────────── */
.vv-rp-flag       { display:inline-flex;align-items:center;gap:4px;cursor:pointer;
                    padding:3px 8px;border:1px solid #1e1e1e;border-radius:3px;background:#0d0d0d;
                    font-size:10px;color:#555;user-select:none;transition:border-color .1s; }
.vv-rp-flag:hover { border-color:#2a3a2a;color:#888; }
.vv-rp-flag input[type=checkbox] { accent-color:#4caf50;width:11px;height:11px;cursor:pointer; }
.vv-rp-flag input:checked ~ span { color:#4caf50; }
.vv-rp-flag:has(input:checked) { border-color:#1a3a1a;background:#080f08; }
.vv-rp-flag em { font-style:normal;color:#2e4e2e; }

/* ── Manual Sync card ──────────────────────────────────────────────── */
.vv-ms-row     { display:flex;gap:10px;margin-bottom:8px;align-items:flex-start;flex-wrap:wrap; }
.vv-ms-col     { display:flex;flex-direction:column;gap:4px;flex:1 1 200px;min-width:0; }
.vv-ms-lbl     { font-size:9px;font-weight:700;color:#444;text-transform:uppercase;letter-spacing:.07em; }
.vv-ms-inp     { background:#0d0d0d;border:1px solid #252525;border-radius:3px;color:#ccc;
                 font-size:12px;padding:5px 8px;width:100%;box-sizing:border-box;font-family:monospace;outline:none; }
.vv-ms-inp:focus { border-color:#3a5a3a; }
.vv-ms-sel     { background:#0d0d0d;border:1px solid #252525;border-radius:3px;color:#ccc;
                 font-size:12px;padding:5px 8px;width:100%;box-sizing:border-box;outline:none;cursor:pointer; }
.vv-ms-sel:focus { border-color:#3a5a3a; }
.vv-ms-dirs    { background:#080808;border:1px solid #1e1e1e;border-radius:3px;
                 max-height:160px;overflow-y:auto;margin-top:2px; }
.vv-ms-dir     { padding:4px 10px;font-size:11px;color:#555;font-family:monospace;cursor:pointer;
                 white-space:nowrap;overflow:hidden;text-overflow:ellipsis;border-bottom:1px solid #111; }
.vv-ms-dir:hover { background:#0f1f0f;color:#4caf50; }
.vv-ms-dir.parent { color:#444;font-style:italic; }
.vv-ms-dir.parent:hover { color:#888; }
.vv-ms-optrow  { display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin-bottom:8px; }
.vv-ms-chk     { display:flex;align-items:center;gap:5px;cursor:pointer; }
.vv-ms-chk input { accent-color:#4caf50;width:13px;height:13px; }
.vv-ms-chk span { font-size:11px;color:#555; }
.vv-ms-run     { background:#1a2a1a;border:1px solid #2d4a2d;color:#4caf50;font-size:12px;
                 font-weight:600;padding:6px 18px;border-radius:3px;cursor:pointer;letter-spacing:.03em; }
.vv-ms-run:hover { background:#223a22; }
.vv-ms-run:disabled { opacity:.4;cursor:default; }
.vv-ms-out     { background:#080808;border:1px solid #1a1a1a;border-radius:3px;padding:8px 10px;
                 font-family:monospace;font-size:10px;color:#555;white-space:pre-wrap;word-break:break-all;
                 max-height:220px;overflow-y:auto;margin-top:8px;display:none; }
.vv-ms-badge   { font-size:9px;padding:1px 7px;border-radius:3px;font-weight:700; }
@media (max-width:700px) { #vv-ms-body > div:first-child { grid-template-columns:1fr !important; } }
</style>

<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding:0 2px;">
  <span style="font-size:13px;font-weight:bold;color:#888;text-transform:uppercase;letter-spacing:.06em;">Rsync</span>
  <span style="font-size:11px;color:#3a3a3a;" id="vv-ry-ts"></span>
</div>

<!-- Manual Sync card — static, not re-rendered by the poll loop -->
<div class="vv-ry-card" id="vv-ms-card" style="margin-bottom:12px;">
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
    <div style="display:flex;align-items:center;gap:8px;">
      <span style="font-size:12px;font-weight:700;color:#888;text-transform:uppercase;letter-spacing:.06em;">Manual Rsync</span>
      <span id="vv-ms-badge" class="vv-ms-badge" style="display:none;"></span>
    </div>
    <button onclick="vvMsToggle()" style="font-size:10px;color:#4a6a4a;background:#0d1a0d;border:1px solid #1a3a1a;
                                          border-radius:3px;padding:2px 10px;cursor:pointer;" id="vv-ms-tog">▾ Show</button>
  </div>
  <div id="vv-ms-body" style="display:none;">

    <!-- Two-column browse layout -->
    <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:10px;">

      <!-- ── Left: local source ── -->
      <div>
        <div class="vv-ms-lbl" style="margin-bottom:5px;">Local source</div>
        <div style="display:flex;gap:5px;align-items:center;margin-bottom:4px;">
          <input class="vv-ms-inp" id="vv-ms-local" type="text" placeholder="/mnt/user/…"
                 style="flex:1;min-width:0;" onkeydown="if(event.key==='Enter')vvMsBrowseLocal()">
          <button onclick="vvMsLocalNavUp()" title="Parent"
                  style="background:#111;border:1px solid #222;color:#555;border-radius:3px;
                         padding:4px 8px;cursor:pointer;font-size:12px;flex-shrink:0;">↑</button>
          <button onclick="vvMsBrowseLocal()" id="vv-ms-lbrowse-btn"
                  style="background:#111;border:1px solid #222;color:#4a9eff;border-radius:3px;
                         padding:4px 9px;cursor:pointer;font-size:11px;white-space:nowrap;flex-shrink:0;">⟳ Browse</button>
        </div>
        <div id="vv-ms-local-dirs" class="vv-ms-dirs" style="display:none;"></div>
        <span id="vv-ms-lbrowse-err" style="font-size:9px;color:#ef5350;display:none;margin-top:2px;display:block;"></span>
        <span style="font-size:9px;color:#2a2a2a;margin-top:3px;display:block;">trailing / = sync contents · without / = sync the folder</span>
      </div>

      <!-- ── Right: remote destination ── -->
      <div>
        <div style="display:flex;gap:8px;margin-bottom:6px;flex-wrap:wrap;">
          <div style="flex:1 1 140px;min-width:0;">
            <div class="vv-ms-lbl" style="margin-bottom:3px;">Remote server</div>
            <select class="vv-ms-sel" id="vv-ms-host" onchange="vvMsHostChanged()">
              <option value="">Loading hosts…</option>
            </select>
            <span id="vv-ms-host-status" style="font-size:9px;color:#333;margin-top:2px;display:block;"></span>
          </div>
          <div style="flex:0 1 80px;">
            <div class="vv-ms-lbl" style="margin-bottom:3px;">User</div>
            <select class="vv-ms-sel" id="vv-ms-user">
              <option value="root">root</option>
            </select>
          </div>
        </div>
        <div class="vv-ms-lbl" style="margin-bottom:5px;">Remote path</div>
        <div style="display:flex;gap:5px;align-items:center;margin-bottom:4px;">
          <input class="vv-ms-inp" id="vv-ms-rpath" type="text" placeholder="/mnt/user/…"
                 style="flex:1;min-width:0;" onkeydown="if(event.key==='Enter')vvMsBrowse()">
          <button onclick="vvMsNavUp()" title="Parent"
                  style="background:#111;border:1px solid #222;color:#555;border-radius:3px;
                         padding:4px 8px;cursor:pointer;font-size:12px;flex-shrink:0;">↑</button>
          <button onclick="vvMsBrowse()" id="vv-ms-browse-btn"
                  style="background:#111;border:1px solid #222;color:#4a9eff;border-radius:3px;
                         padding:4px 9px;cursor:pointer;font-size:11px;white-space:nowrap;flex-shrink:0;">⟳ Browse</button>
        </div>
        <div id="vv-ms-dirs" class="vv-ms-dirs" style="display:none;"></div>
        <span id="vv-ms-browse-err" style="font-size:9px;color:#ef5350;display:none;margin-top:2px;display:block;"></span>
      </div>
    </div>

    <!-- Rsync flags -->
    <div style="border-top:1px solid #1a1a1a;padding-top:8px;margin-bottom:8px;">
      <div class="vv-ms-lbl" style="margin-bottom:6px;">Rsync flags
        <span style="font-size:9px;font-weight:normal;color:#2a2a2a;margin-left:4px;">does not inherit defaults — check every desired flag</span>
      </div>
      <div style="display:flex;gap:5px;flex-wrap:wrap;">
        <label class="vv-rp-flag"><input type="checkbox" data-flag="-av" checked onchange="vvMsUpdatePreview()"><span>-av</span></label>
        <label class="vv-rp-flag"><input type="checkbox" data-flag="--human-readable" checked onchange="vvMsUpdatePreview()"><span>--human-readable</span></label>
        <label class="vv-rp-flag"><input type="checkbox" data-flag="--stats" checked onchange="vvMsUpdatePreview()"><span>--stats</span></label>
        <span style="border-left:1px solid #1e1e1e;margin:0 3px;"></span>
        <label class="vv-rp-flag"><input type="checkbox" data-flag="--delete" onchange="vvMsUpdatePreview()"><span>--delete</span></label>
        <label class="vv-rp-flag"><input type="checkbox" data-flag="--inplace" onchange="vvMsUpdatePreview()"><span>--inplace</span></label>
        <label class="vv-rp-flag"><input type="checkbox" data-flag="--no-whole-file" onchange="vvMsUpdatePreview()"><span>--no-whole-file</span></label>
        <label class="vv-rp-flag"><input type="checkbox" data-flag="--checksum" onchange="vvMsUpdatePreview()"><span>--checksum</span></label>
        <label class="vv-rp-flag"><input type="checkbox" data-flag="--compress" onchange="vvMsUpdatePreview()"><span>--compress</span></label>
        <span style="border-left:1px solid #1e1e1e;margin:0 3px;"></span>
        <label class="vv-rp-flag"><input type="checkbox" id="vv-ms-dryrun" data-flag="--dry-run" onchange="vvMsUpdatePreview()"><span>--dry-run</span></label>
      </div>
      <div style="margin-top:6px;background:#080808;border:1px solid #181818;border-radius:3px;
                  padding:4px 8px;font-family:monospace;font-size:10px;color:#3a5a3a;display:flex;gap:8px;align-items:baseline;">
        <span style="font-size:9px;color:#2a2a2a;flex-shrink:0;">rsync</span>
        <span id="vv-ms-flags-preview"></span>
        <span id="vv-ms-bwlimit-preview" style="color:#2e4e2e;"></span>
        <span style="color:#2a2a2a;">…src host:dst</span>
      </div>
    </div>

    <!-- BW limit + SSH key -->
    <div class="vv-ms-optrow" style="margin-bottom:8px;">
      <div style="display:flex;align-items:center;gap:6px;">
        <span style="font-size:11px;color:#444;">BW limit</span>
        <input class="vv-ms-inp" id="vv-ms-bw" type="number" min="0" value="0"
               style="width:72px;padding:3px 6px;" oninput="vvMsUpdatePreview()">
        <span style="font-size:10px;color:#333;">KB/s (0 = unlimited)</span>
      </div>
      <label class="vv-ms-chk">
        <input type="checkbox" id="vv-ms-usekey" checked>
        <span>SSH key: <code id="vv-ms-key-info"
              style="font-size:9px;color:#2a3a2a;margin-left:2px;font-family:monospace;">loading…</code></span>
      </label>
    </div>

    <!-- Run + Show + Stop + status -->
    <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:4px;">
      <button class="vv-ms-run" id="vv-ms-run-btn" onclick="vvMsRun()">▶ Run Rsync</button>
      <button id="vv-ms-show-btn" onclick="vvMsToggleOutput()"
              style="background:#111;border:1px solid #252525;color:#555;font-size:11px;
                     padding:5px 14px;border-radius:3px;cursor:pointer;">▾ Show Rsync</button>
      <button id="vv-ms-stop-btn" onclick="vvMsStop()"
              style="display:none;background:#2a1010;border:1px solid #5a2020;color:#ef5350;
                     font-size:11px;padding:5px 14px;border-radius:3px;cursor:pointer;font-weight:600;">■ Stop</button>
      <span id="vv-ms-run-status" style="font-size:10px;color:#444;"></span>
    </div>

    <!-- Output (auto-opens on run) -->
    <pre class="vv-ms-out" id="vv-ms-out" style="display:none;margin-top:8px;"></pre>
  </div>
</div>

<div id="vv-ry-grid" style="display:grid;grid-template-columns:repeat(8,1fr);gap:12px;margin-bottom:12px;"></div>

<!-- ── Info / Profile-editor layer ───────────────────────────────────────── -->
<div id="vv-ry-layer">

  <!-- Info view (default) -->
  <div id="vv-ry-info-view">
    <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;flex-wrap:wrap;gap:8px;">
      <span style="font-size:11px;font-weight:700;color:#666;text-transform:uppercase;letter-spacing:.07em;">Rsync Reference</span>
      <button onclick="vvRpOpen()"
              style="background:#0a1a2a;border:1px solid #1a3a5a;color:#4a9eff;font-size:11px;
                     padding:4px 14px;border-radius:3px;cursor:pointer;font-weight:600;">⚙ Profile Editor</button>
    </div>

    <!-- How to use this page -->
    <div class="vv-ry-info-block">
      <div class="vv-ry-info-hdr" onclick="vvRyInfoToggle(this)">
        <span class="vv-ry-info-chev">▾</span> How to use this page
      </div>
      <div class="vv-ry-info-body">
        <div class="vv-ry-info-cols">
          <div><strong>Status card</strong> — shows whether the global rsync gate is open. Red = all syncs blocked regardless of tier toggles.</div>
          <div><strong>Sync windows</strong> — per-tier enable/disable. A tier can be enabled here but still blocked by the global gate.</div>
          <div><strong>Settings → Global gate</strong> — RSYNC_ENABLED in master.conf. Master switch; turn off during rebuilds or maintenance.</div>
          <div><strong>Settings → Tier toggles</strong> — CRITICAL/DAILY/etc._RSYNC_ENABLED. Disable one tier without affecting others.</div>
          <div><strong>Bandwidth history</strong> — last 30 days of per-profile rsync runs. Bar = relative time spent. Click recent runs for detail.</div>
          <div><strong>Manual Rsync</strong> — one-off transfer to any partner host. Browse remote dirs via SSH. Runs in background, output streams below.</div>
          <div><strong>Profile Editor</strong> — create and edit named profiles used by appdata syncs. Each profile controls flags, BW limit, container stop lists, and excludes.</div>
        </div>
      </div>
    </div>

    <!-- Sync windows explained -->
    <div class="vv-ry-info-block">
      <div class="vv-ry-info-hdr" onclick="vvRyInfoToggle(this)">
        <span class="vv-ry-info-chev">▸</span> Sync windows
      </div>
      <div class="vv-ry-info-body" style="display:none;">
        <div class="vv-ry-info-cols">
          <div><strong>Critical (30 min)</strong> — appdata (arrs, auth, databases), partnership health check, play state sync. Designed to be fast — uses dirty-sync profiles where possible.</div>
          <div><strong>Intermediate (4 hr)</strong> — arr sync between the nightly runs. Catches new indexer hits without waiting until midnight.</div>
          <div><strong>Daily (nightly)</strong> — media shares, arr maintenance, media cleaner, docker updates, daily container restarts.</div>
          <div><strong>Weekly</strong> — bulk media shares, update checks, weekly restarts, SMART health digest.</div>
          <div><strong>Fallback</strong> — fires when a server comes back online after being the active fallback. Syncs any changes that happened during downtime.</div>
          <div><strong>Two-tier toggle</strong> — global gate (RSYNC_ENABLED) blocks everything. Per-tier flags (e.g. DAILY_RSYNC_ENABLED) disable just that window. Both must be ON for syncs to run.</div>
        </div>
      </div>
    </div>

    <!-- Profile system -->
    <div class="vv-ry-info-block">
      <div class="vv-ry-info-hdr" onclick="vvRyInfoToggle(this)">
        <span class="vv-ry-info-chev">▸</span> Profile system
      </div>
      <div class="vv-ry-info-body" style="display:none;">
        <div class="vv-ry-info-cols">
          <div><strong>What is a profile?</strong> — A named set of rsync options applied to a specific appdata directory. Appdata shares match a profile by their directory basename (lowercased).</div>
          <div><strong>PROFILE_RSYNC_OPTS</strong> — Full rsync flag string for this profile. Does NOT inherit DEFAULT_RSYNC_OPTS — list every desired flag explicitly. Use <code>$BW_LIMIT</code> to reference the profile's bandwidth limit.</div>
          <div><strong>PROFILE_BW_LIMIT</strong> — Bandwidth cap in KB/s. Overrides the global BW_LIMIT for this profile.</div>
          <div><strong>PROFILE_RETRY_COUNT / SLEEP</strong> — How many times to retry on failure, and how long to wait between attempts.</div>
          <div><strong>PROFILE_CRITICAL_CONTAINER_NAMES</strong> — Containers stopped on BOTH hosts before rsync. Local stops first (flush DBs), then remote. Only running containers get restarted — stopped ones stay stopped.</div>
          <div><strong>PROFILE_DELAYED_CONTAINERS</strong> — Containers that need a delay after the others start (e.g. NextCloud waits for Postgres).</div>
          <div><strong>PROFILE_CONTAINER_DELAY</strong> — Seconds to wait before starting delayed containers.</div>
          <div><strong>PROFILE_EXCLUDE_DIRS</strong> — Space-separated patterns excluded from the rsync. Supports globs (e.g. <code>logs *.tmp *.db-wal</code>).</div>
          <div><strong>PROFILE_REMOTE_RESTART_CONTAINERS</strong> — Used by dirty-sync profiles. Restarts these containers on the remote after sync so it picks up the new data.</div>
          <div><strong>Clean vs dirty sync</strong> — Clean: stop containers, <code>--delete</code>, full mirror. Dirty: containers keep running, WAL files excluded, <code>--inplace --no-whole-file</code> for safe partial writes. Use dirty for frequent critical syncs.</div>
          <div><strong>App layer</strong> — Scripts add their appdata directory to HOST*_CRITICAL_SYNC_SHARES or HOST*_DAILY_SYNC_SHARES using <code>/path/to/dir|profile-name</code> syntax. Creating the profile here makes it available for that reference.</div>
        </div>
      </div>
    </div>

    <!-- Rsync flags reference -->
    <div class="vv-ry-info-block">
      <div class="vv-ry-info-hdr" onclick="vvRyInfoToggle(this)">
        <span class="vv-ry-info-chev">▸</span> Common flag combinations
      </div>
      <div class="vv-ry-info-body" style="display:none;">
        <div class="vv-ry-info-cols">
          <div><strong>Clean sync (weekly appdata)</strong><br><code>-av --human-readable --bwlimit=$BW_LIMIT --delete</code><br>Full mirror — removes files on destination that no longer exist at source. Safe when containers are stopped.</div>
          <div><strong>Dirty sync (every 30 min)</strong><br><code>-av --human-readable --bwlimit=$BW_LIMIT --delete --inplace --no-whole-file</code><br>Containers stay running. <code>--inplace</code> writes directly to destination file (avoids temp-file rename issues). <code>--no-whole-file</code> uses delta transfer. Exclude WAL files to avoid corrupt state.</div>
          <div><strong>Database sync (arrs)</strong><br><code>-av --info=progress2 --human-readable --bwlimit=$BW_LIMIT --delete --inplace</code><br>Stop containers first. <code>--info=progress2</code> gives cleaner progress output in logs. Lower BW limit to not starve other syncs running concurrently.</div>
          <div><strong>Large media (weekly)</strong><br><code>-av --human-readable --bwlimit=$BW_LIMIT --delete --inplace --no-whole-file</code><br>High BW limit, inplace for large files, delete to keep mirror clean. No container stops — media files are read-only.</div>
          <div><code>--delete</code> — remove files at destination that no longer exist at source. Essential for a true mirror.</div>
          <div><code>--inplace</code> — write file data directly into destination file instead of creating a temp copy first. Faster for large files; required for some databases.</div>
          <div><code>--no-whole-file</code> — force delta transfer even over fast LAN. Reduces data sent for files that change incrementally.</div>
          <div><code>--checksum</code> — compare files by checksum rather than mtime+size. Slower but catches any corruption. Use for archive syncs, not frequent syncs.</div>
        </div>
      </div>
    </div>
  </div><!-- /info view -->

  <!-- Profile editor view (hidden until button click) -->
  <div id="vv-rp-view" style="display:none;">
    <div style="display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap;">
      <button onclick="vvRpClose()"
              style="background:#111;border:1px solid #222;color:#555;font-size:11px;
                     padding:4px 12px;border-radius:3px;cursor:pointer;">← Rsync Info</button>
      <span style="font-size:11px;font-weight:700;color:#666;text-transform:uppercase;letter-spacing:.07em;">Profile Editor</span>
      <div style="margin-left:auto;display:flex;align-items:center;gap:6px;">
        <select id="vv-rp-selector"
                style="background:#111;border:1px solid #2a2a2a;color:#bbb;font-size:11px;
                       padding:3px 8px;border-radius:3px;outline:none;cursor:pointer;max-width:200px;"
                onchange="vvRpOnSelect(this.value)">
          <option value="">Loading…</option>
        </select>
        <button onclick="vvRpNew()"
                style="background:#0a1a0a;border:1px solid #1a3a1a;color:#4caf50;
                       font-size:11px;padding:3px 12px;border-radius:3px;cursor:pointer;font-weight:600;">＋ New</button>
      </div>
    </div>

    <!-- Empty state -->
    <div id="vv-rp-empty" class="vv-ry-card" style="color:#333;font-size:11px;padding:10px 14px;">
      Select a profile above to edit, or click ＋ New to create one.
    </div>

    <!-- Profile form -->
    <div id="vv-rp-form" style="display:none;">
      <div class="vv-ry-card">

        <!-- Name row -->
        <div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap;">
          <span style="font-size:10px;font-weight:700;color:#555;text-transform:uppercase;letter-spacing:.06em;width:110px;flex-shrink:0;">Profile name</span>
          <input id="vv-rp-name" type="text"
                 style="background:#0d0d0d;border:1px solid #2a2a2a;border-radius:3px;color:#ccc;
                        font-size:12px;padding:4px 8px;font-family:monospace;outline:none;width:200px;"
                 placeholder="e.g. my-app">
          <span id="vv-rp-name-hint" style="font-size:10px;color:#333;"></span>
        </div>
        <hr class="vv-ry-sep">

        <!-- Rsync opts — checkboxes + preview -->
        <div style="margin-bottom:10px;">
          <div style="font-size:10px;font-weight:700;color:#555;text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px;">
            Rsync opts
            <span style="font-size:9px;font-weight:normal;color:#2a2a2a;margin-left:6px;">does NOT inherit defaults — list every desired flag</span>
          </div>
          <div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px;" id="vv-rp-flag-group">
            <label class="vv-rp-flag"><input type="checkbox" data-flag="-av" onchange="vvRpBuildOpts()"><span>-av</span></label>
            <label class="vv-rp-flag"><input type="checkbox" data-flag="--human-readable" onchange="vvRpBuildOpts()"><span>--human-readable</span></label>
            <label class="vv-rp-flag"><input type="checkbox" data-flag="--info=progress2" onchange="vvRpBuildOpts()"><span>--info=progress2</span></label>
            <span style="border-left:1px solid #1e1e1e;margin:0 4px;"></span>
            <label class="vv-rp-flag"><input type="checkbox" data-flag="--delete" onchange="vvRpBuildOpts()"><span>--delete</span></label>
            <label class="vv-rp-flag"><input type="checkbox" data-flag="--inplace" onchange="vvRpBuildOpts()"><span>--inplace</span></label>
            <label class="vv-rp-flag"><input type="checkbox" data-flag="--no-whole-file" onchange="vvRpBuildOpts()"><span>--no-whole-file</span></label>
            <label class="vv-rp-flag"><input type="checkbox" data-flag="--checksum" onchange="vvRpBuildOpts()"><span>--checksum</span></label>
            <span style="border-left:1px solid #1e1e1e;margin:0 4px;"></span>
            <label class="vv-rp-flag"><input type="checkbox" data-flag="--bwlimit=$BW_LIMIT" onchange="vvRpBuildOpts()"><span>--bwlimit=<em>$BW_LIMIT</em></span></label>
            <label class="vv-rp-flag"><input type="checkbox" data-flag="--compress" onchange="vvRpBuildOpts()"><span>--compress</span></label>
          </div>
          <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
            <span style="font-size:10px;color:#444;flex-shrink:0;">Extra flags</span>
            <input id="vv-rp-opts-extra" type="text"
                   style="background:#0d0d0d;border:1px solid #1e1e1e;border-radius:3px;color:#888;
                          font-size:11px;padding:3px 7px;font-family:monospace;outline:none;flex:1;"
                   placeholder="any additional flags"
                   oninput="vvRpBuildOpts()">
          </div>
          <div style="background:#080808;border:1px solid #181818;border-radius:3px;padding:5px 9px;
                      font-family:monospace;font-size:11px;color:#4a6a4a;display:flex;align-items:baseline;gap:8px;">
            <span style="font-size:9px;color:#2a2a2a;flex-shrink:0;">rsync_opts =</span>
            <span id="vv-rp-opts-preview" style="word-break:break-all;"></span>
          </div>
          <input type="hidden" id="vv-rp-rsync_opts">
        </div>
        <hr class="vv-ry-sep">

        <!-- Numeric fields -->
        <div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:10px;">
          <div>
            <div style="font-size:9px;color:#444;margin-bottom:3px;">BW limit <span style="color:#2a2a2a;">KB/s</span></div>
            <input class="vv-ry-set-inp" id="vv-rp-bw_limit" type="number" min="0" style="width:80px;">
          </div>
          <div>
            <div style="font-size:9px;color:#444;margin-bottom:3px;">Retry count</div>
            <input class="vv-ry-set-inp" id="vv-rp-retry_count" type="number" min="0" max="20" style="width:60px;">
          </div>
          <div>
            <div style="font-size:9px;color:#444;margin-bottom:3px;">Retry sleep <span style="color:#2a2a2a;">s</span></div>
            <input class="vv-ry-set-inp" id="vv-rp-sleep" type="number" min="0" style="width:70px;">
          </div>
          <div>
            <div style="font-size:9px;color:#444;margin-bottom:3px;">Container delay <span style="color:#2a2a2a;">s</span></div>
            <input class="vv-ry-set-inp" id="vv-rp-container_delay" type="number" min="0" style="width:70px;">
          </div>
        </div>
        <hr class="vv-ry-sep">

        <!-- Container / path fields -->
        <div style="display:flex;flex-direction:column;gap:8px;">
          <div>
            <div style="font-size:9px;color:#444;margin-bottom:3px;">Stop containers <span style="color:#2a2a2a;">space-separated — stopped on both hosts before rsync, restarted after</span></div>
            <input class="vv-ry-set-inp" id="vv-rp-critical_containers" type="text" style="width:100%;box-sizing:border-box;font-family:monospace;">
          </div>
          <div>
            <div style="font-size:9px;color:#444;margin-bottom:3px;">Delayed containers <span style="color:#2a2a2a;">start after delay (e.g. app waits for its database)</span></div>
            <input class="vv-ry-set-inp" id="vv-rp-delayed_containers" type="text" style="width:100%;box-sizing:border-box;font-family:monospace;">
          </div>
          <div>
            <div style="font-size:9px;color:#444;margin-bottom:3px;">Exclude dirs <span style="color:#2a2a2a;">space-separated globs — e.g. <code>logs *.tmp *.db-wal *.db-shm</code></span></div>
            <input class="vv-ry-set-inp" id="vv-rp-exclude_dirs" type="text" style="width:100%;box-sizing:border-box;font-family:monospace;">
          </div>
          <div>
            <div style="font-size:9px;color:#444;margin-bottom:3px;">Remote restart <span style="color:#2a2a2a;">dirty-sync only — restarted on remote after sync so it picks up changes</span></div>
            <input class="vv-ry-set-inp" id="vv-rp-remote_restart" type="text" style="width:100%;box-sizing:border-box;font-family:monospace;">
          </div>
        </div>

        <div style="margin-top:14px;display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
          <button class="vv-ry-save-btn" onclick="vvRpSave()">Save Profile</button>
          <button id="vv-rp-del-btn" onclick="vvRpDelete()"
                  style="background:#1a0a0a;border:1px solid #3a1a1a;color:#ef5350;font-size:11px;
                         padding:4px 12px;border-radius:3px;cursor:pointer;display:none;">Delete</button>
          <span id="vv-rp-fb" class="vv-ry-feedback"></span>
        </div>
      </div>
    </div>
  </div><!-- /profile view -->

</div><!-- /layer -->

<script>
// Shared state — globals so IIFE internals and onclick handlers can both reach them
var _vvRyWinOpen    = new Set();   // which window panels are expanded
var _vvRyLastData   = null;        // last successful poll response
var _vvRyEditMode   = false;       // true = skip poll re-renders
var _vvRyWinEditing = null;        // key of window currently in edit mode
var _vvRyWinEState  = {};          // {key: {scripts:[{id,enabled}], shares:[{path,profile}]}}
var _vvRyScriptLib  = null;        // cached {Folder:[{id,label}]} — null = not yet loaded
var _vvRyProfiles   = null;        // cached profile name list

// View-mode panel inner HTML — shared by _windowsRow (inside IIFE) and vvRyWinCancel (outside)
function _vvRyViewPanelInner(key, cfg) {
  const scripts = cfg.scripts || [];
  const shares  = cfg.shares  || [];
  let inner = '';

  if (scripts.length) {
    inner += `<div style="font-size:9px;font-weight:700;color:#2a2a2a;text-transform:uppercase;letter-spacing:.07em;margin-bottom:4px;">Scripts</div>`;
    for (const s of scripts) {
      const parts  = s.split('/');
      const folder = parts.length > 1 ? parts.slice(0, -1).join('/') : '';
      const name   = parts[parts.length - 1].replace(/\.sh$/, '');
      inner += `<div style="font-size:10px;color:#555;padding:1px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
        ${folder ? `<span style="color:#2a2a2a;">${folder}/</span>` : ''}<span>${name}</span>
      </div>`;
    }
  }

  if (shares.length) {
    if (scripts.length) inner += `<div style="border-top:1px solid #161616;margin:6px 0;"></div>`;
    inner += `<div style="font-size:9px;font-weight:700;color:#2a2a2a;text-transform:uppercase;letter-spacing:.07em;margin-bottom:4px;">Sync shares</div>`;
    for (const sh of shares) {
      const [rawPath, profile] = sh.split('|');
      const basename = rawPath.trim().split('/').pop();
      inner += `<div style="font-size:10px;color:#555;padding:1px 0;display:flex;align-items:center;gap:5px;min-width:0;">
        <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;" title="${rawPath.trim()}">${basename}</span>
        ${profile ? `<span style="font-size:9px;color:#2a3a2a;flex-shrink:0;font-family:monospace;">${profile.trim()}</span>` : ''}
      </div>`;
    }
  }

  if (scripts.length || shares.length) {
    inner += `<div style="margin-top:7px;padding-top:6px;border-top:1px solid #161616;">
      <button onclick="event.stopPropagation();vvRyWinEdit('${key}')"
              style="background:#0a1a2a;border:1px solid #1a3a5a;color:#4a9eff;font-size:10px;
                     padding:2px 10px;border-radius:3px;cursor:pointer;">⚙ Edit</button>
    </div>`;
  }

  return inner;
}

(function() {

const WIN_META = {
  critical:     { label: 'Critical',     cadence: '30 min'      },
  intermediate: { label: 'Intermediate', cadence: '4 hr'        },
  daily:        { label: 'Daily',        cadence: 'nightly'     },
  weekly:       { label: 'Weekly',       cadence: 'weekly'      },
  monthly:      { label: 'Monthly',      cadence: '30-day gate' },
  fallback:     { label: 'Fallback',     cadence: 'on handback' },
};

const PROF_COLORS = [
  '#4caf50','#4a9eff','#ffb74d','#ce93d8','#ef5350',
  '#4dd0e1','#a5d6a7','#ff8a65','#90caf9','#f48fb1',
];

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

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

  let activeHtml;
  if (active.length === 0) {
    activeHtml = '<div style="color:#333;font-size:11px;margin-top:4px;">No transfers running</div>';
  } else {
    activeHtml = active.map(a =>
      `<div style="display:flex;align-items:center;gap:7px;margin:3px 0;">
        <span class="vv-ry-pill run">RUNNING</span>
        <span style="font-size:12px;color:#bbb;">${a.profile}</span>
        <span style="font-size:11px;color:#555;margin-left:auto;">${_dur(a.elapsed)}</span>
      </div>`
    ).join('');
  }

  return `<div class="vv-ry-card" style="grid-column:span 4;">
    <div style="display:flex;align-items:center;gap:8px;margin-bottom:10px;">
      <span class="vv-ry-dot" style="background:${dotC}"></span>
      <span style="font-size:13px;font-weight:bold;color:${en ? '#4caf50' : '#ef5350'};">
        ${en ? 'RSYNC ENABLED' : 'RSYNC DISABLED'}
      </span>
      <span class="vv-ry-pill ${en ? 'on' : 'err'}" style="margin-left:auto;">
        ${en ? 'global gate open' : 'all syncs blocked'}
      </span>
    </div>
    <hr class="vv-ry-sep">
    <div class="vv-ry-sec">Active transfers</div>
    ${activeHtml}
  </div>`;
}

// ── 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 `<div class="vv-ry-row">
      <span class="vv-ry-lbl">${WIN_META[k]?.label || k}</span>
      <span class="vv-ry-val" style="color:#333;">never</span>
    </div>`;
    const cls = e.status === 'success' ? 'color:#4caf50' : 'color:#ef5350';
    return `<div class="vv-ry-row">
      <span class="vv-ry-lbl">${WIN_META[k]?.label || k}</span>
      <span class="vv-ry-val">
        <span style="${cls}">${e.status}</span>
        <span style="color:#444;margin-left:6px;">${_relTime(e.ts)}</span>
        ${e.duration ? `<span style="color:#3a3a3a;margin-left:4px;">${_dur(e.duration)}</span>` : ''}
      </span>
    </div>`;
  });

  return `<div class="vv-ry-card" style="grid-column:span 4;">
    <div class="vv-ry-sec">Last completed run</div>
    ${rows.join('')}
  </div>`;
}

// ── 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];
    const cls     = enabled ? 'enabled' : 'disabled';
    const cfg     = winArr[key] || {};
    const scripts = cfg.scripts || [];
    const shares  = cfg.shares  || [];

    let statusHtml = '<div class="vv-ry-win-stat">No run history</div>';
    if (ls && ls.ts) {
      const okCol = ls.status === 'success' ? '#4caf50' : '#ef5350';
      statusHtml  = `<div class="vv-ry-win-stat" style="color:${okCol};">${ls.status}</div>
                     <div class="vv-ry-win-dur">${_relTime(ls.ts)}${ls.duration ? ' · ' + _dur(ls.duration) : ''}</div>`;
    }

    const expandable = !!(scripts.length || shares.length);
    const panelHtml  = expandable
      ? `<div class="vv-ry-win-panel" style="display:none;margin-top:9px;padding-top:8px;border-top:1px solid #1e1e1e;">${_vvRyViewPanelInner(key, cfg)}</div>`
      : '';

    return `<div class="vv-ry-win ${cls}" data-win-key="${key}"
        ${expandable ? 'onclick="vvRyWinExpand(this)" style="cursor:pointer;"' : ''}>
      <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:4px;">
        <div style="min-width:0;">
          <div class="vv-ry-win-lbl">${meta.cadence}</div>
          <div class="vv-ry-win-name">${meta.label}</div>
          <span class="vv-ry-pill ${enabled ? 'on' : 'off'}">${enabled ? 'enabled' : 'disabled'}</span>
          ${statusHtml}
        </div>
        ${expandable ? `<span class="vv-ry-win-chev" style="font-size:9px;color:#2a2a2a;flex-shrink:0;padding-top:2px;">▸</span>` : ''}
      </div>
      ${panelHtml}
    </div>`;
  });

  return `<div class="vv-ry-wins">${cards.join('')}</div>`;
}

// ── Bandwidth section ─────────────────────────────────────────────────────────
function _bwSection(data) {
  const history = data.bw_history || [];
  if (!history.length) {
    return `<div class="vv-ry-card" style="grid-column:1/-1;">
      <div class="vv-ry-sec">Bandwidth history</div>
      <div style="color:#333;font-size:11px;padding:8px 0;">No data in last 30 days</div>
    </div>`;
  }

  // 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 `<div class="vv-ry-prof-row">
      <span class="vv-ry-prof-nm">${name}</span>
      <div class="vv-ry-prof-bar"><div class="vv-ry-prof-fill" style="width:${pct}%;background:${col}"></div></div>
      <span class="vv-ry-prof-cnt">${p.runs} runs · ${byteStr}</span>
    </div>`;
  });

  // 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 `<div class="vv-ry-run-row">
      <span class="vv-ry-run-dt">${r.date}</span>
      <span class="vv-ry-run-tm">${r.time}</span>
      <span class="vv-ry-run-pr">${r.profile}</span>
      <span class="vv-ry-run-dur">${durStr}</span>
      <span class="vv-ry-run-st"><span class="vv-ry-pill ${stCls}" style="font-size:9px;">${r.status}</span></span>
    </div>`;
  });

  return `<div class="vv-ry-bw" style="grid-column:1/-1;">
    <div class="vv-ry-card">
      <div class="vv-ry-sec">Profile activity — last 30 days</div>
      ${profRows.join('')}
    </div>
    <div class="vv-ry-card">
      <div class="vv-ry-sec">Recent runs</div>
      <div style="font-size:10px;color:#3a3a3a;margin-bottom:4px;display:grid;grid-template-columns:80px 90px 1fr 55px 48px;gap:4px;">
        <span>Date</span><span>Time</span><span>Profile</span><span style="text-align:right;">Duration</span><span></span>
      </div>
      <div class="vv-ry-runs-scroll">${runRows.join('')}</div>
    </div>
  </div>`;
}

// ── Sync log card ─────────────────────────────────────────────────────────────
function _esc(s) {
  return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}

let _logPollTimer = null;
let _logElapsed   = 0;
let _logTicker    = null;

function _syncLogCard() {
  return `<div class="vv-ry-card" style="grid-column:span 5;display:flex;flex-direction:column;min-height:0;">
    <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
      <div class="vv-ry-sec" style="margin-bottom:0;">Sync Activity</div>
      <span id="vv-ry-log-hdr" style="font-size:10px;color:#3a3a3a;"></span>
    </div>
    <div id="vv-ry-log-live"></div>
    <div class="vv-ry-slog-scrl" id="vv-ry-log-lines"
         style="font-family:monospace;font-size:10px;color:#888;white-space:pre-wrap;word-break:break-all;">
      <span style="color:#333;">Loading…</span>
    </div>
  </div>`;
}

function _fetchLog() {
  fetch('/plugins/varaverk/api/rsync.php?action=rsync_log')
    .then(r => r.json())
    .then(d => {
      const linesEl = document.getElementById('vv-ry-log-lines');
      const liveEl  = document.getElementById('vv-ry-log-live');
      const hdrEl   = document.getElementById('vv-ry-log-hdr');
      if (!linesEl) return;

      if (d.live && d.profile) {
        _logElapsed = d.elapsed || 0;
        if (!_logTicker) {
          _logTicker = setInterval(() => {
            _logElapsed++;
            const el = document.getElementById('vv-ry-log-elapsed');
            if (el) el.textContent = _dur(_logElapsed);
          }, 1000);
        }
        if (liveEl) liveEl.innerHTML =
          `<div style="display:flex;align-items:center;gap:8px;background:#0a1a0a;border-radius:3px;padding:4px 8px;margin-bottom:4px;">
            <span class="vv-ry-pill run">LIVE</span>
            <span style="font-size:11px;color:#ccc;">${_esc(d.profile)}</span>
            <span id="vv-ry-log-elapsed" style="margin-left:auto;font-size:11px;color:#4a9eff;font-family:monospace;">${_dur(d.elapsed || 0)}</span>
          </div>`;
        if (hdrEl) hdrEl.textContent = '';
      } else {
        if (_logTicker) { clearInterval(_logTicker); _logTicker = null; }
        if (liveEl) liveEl.innerHTML = '';
        if (hdrEl)  hdrEl.textContent = d.profile ? 'last: ' + d.profile : '';
      }

      const atBottom = linesEl.scrollHeight - linesEl.scrollTop <= linesEl.clientHeight + 10;
      linesEl.textContent = (d.lines || []).join('\n') || (d.profile ? '(no output captured)' : 'No sync history');
      if (atBottom) linesEl.scrollTop = linesEl.scrollHeight;
    })
    .catch(() => {});
}

function _startLogPoll() {
  _fetchLog();
  if (!_logPollTimer) _logPollTimer = setInterval(_fetchLog, 2000);
}

// ── Settings section ──────────────────────────────────────────────────────────
function _toggle(name, currentVal, label) {
  const id = 'vv-ry-tog-' + name;
  return `<label class="vv-ry-toggle" title="${name}">
    <div class="vv-ry-toggle-track ${currentVal ? 'on' : ''}" id="${id}"
         onclick="vvRyToggle(this,'${name}')"></div>
    <span class="vv-ry-toggle-lbl">${label}</span>
  </label>`;
}

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 `<div class="vv-ry-card" style="grid-column:6/-1;">
    <div class="vv-ry-sec" style="margin-bottom:10px;">Settings</div>

    <div style="margin-bottom:12px;">
      <div style="font-size:10px;color:#444;letter-spacing:.05em;text-transform:uppercase;margin-bottom:6px;">Global gate</div>
      ${_toggle('RSYNC_ENABLED', en, 'RSYNC_ENABLED — blocks all syncs when off')}
    </div>

    <hr class="vv-ry-sep">

    <div style="margin-bottom:12px;">
      <div style="font-size:10px;color:#444;letter-spacing:.05em;text-transform:uppercase;margin-bottom:6px;">Sync windows</div>
      <div class="vv-ry-set-grid">
        ${_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('MONTHLY_RSYNC_ENABLED',      w.monthly,      'Monthly')}
        ${_toggle('FALLBACK_RSYNC_ENABLED',     w.fallback,     'Fallback')}
      </div>
    </div>

    <hr class="vv-ry-sep">

    <div>
      <div style="font-size:10px;color:#444;letter-spacing:.05em;text-transform:uppercase;margin-bottom:8px;">Defaults</div>
      <div class="vv-ry-set-row">
        <span class="vv-ry-set-lbl">Bandwidth limit</span>
        <input class="vv-ry-set-inp" id="vv-ry-bwlimit" type="number" min="0" value="${s.bw_limit || 0}">
        <span class="vv-ry-set-hint">KB/s${mbit}</span>
      </div>
      <div class="vv-ry-set-row">
        <span class="vv-ry-set-lbl">Retry count</span>
        <input class="vv-ry-set-inp" id="vv-ry-retry" type="number" min="0" max="20" value="${s.retry_count ?? 3}">
        <span class="vv-ry-set-hint">attempts</span>
      </div>
      <div class="vv-ry-set-row">
        <span class="vv-ry-set-lbl">Retry delay</span>
        <input class="vv-ry-set-inp" id="vv-ry-sleep" type="number" min="0" value="${s.sleep ?? 300}">
        <span class="vv-ry-set-hint">seconds between attempts</span>
      </div>
      <div class="vv-ry-set-row">
        <span class="vv-ry-set-lbl">BW warn threshold</span>
        <input class="vv-ry-set-inp" id="vv-ry-bwwarn" type="number" min="0" value="${s.bw_warn_gb ?? 50}">
        <span class="vv-ry-set-hint">GB — flag large transfers in weekly report</span>
      </div>
      <div style="margin-top:10px;display:flex;align-items:center;">
        <button class="vv-ry-save-btn" id="vv-ry-save" onclick="vvRySaveDefaults()">Save</button>
        <span class="vv-ry-feedback" id="vv-ry-save-fb"></span>
      </div>
    </div>
  </div>`;
}

// ── Main render ───────────────────────────────────────────────────────────────
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 += _syncLogCard();
  html += _settingsSection(data);
  document.getElementById('vv-ry-grid').innerHTML = html;

  _startLogPoll();

  // 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})),
    shares:  (cfg.shares  || []).map(s => {
      const [p, pr] = s.split('|');
      return {path: p.trim(), profile: (pr || '').trim()};
    }),
  };
  _vvRyEditMode   = true;
  _vvRyWinEditing = key;

  const go = () => _vvRyBuildEditPanel(key);
  if (!_vvRyScriptLib) {
    fetch('/plugins/varaverk/api/rsync_win_arrays.php?action=list_scripts&_=' + Date.now())
      .then(r => r.json()).then(d => { if (d.ok) _vvRyScriptLib = d.groups; go(); }).catch(go);
  } else { go(); }
}

function vvRyWinCancel(key) {
  _vvRyEditMode   = false;
  _vvRyWinEditing = null;
  const cfg   = (_vvRyLastData?.win_arrays || {})[key] || {};
  const panel = document.querySelector(`.vv-ry-win[data-win-key="${key}"] .vv-ry-win-panel`);
  if (panel) { panel.innerHTML = _vvRyViewPanelInner(key, cfg); panel.style.display = ''; }
  const chev = document.querySelector(`.vv-ry-win[data-win-key="${key}"] .vv-ry-win-chev`);
  if (chev) chev.textContent = '▾';
}

function vvRyWinSave(key) {
  const state = _vvRyWinEState[key];
  if (!state) return;
  const btn = document.getElementById('vv-ry-esave-' + key);
  const fb  = document.getElementById('vv-ry-efb-'   + key);
  if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; }
  if (fb)  fb.textContent = '';

  const fd = new FormData();
  fd.append('action',  'save');
  fd.append('win_key', key);
  fd.append('scripts', JSON.stringify(state.scripts));
  fd.append('shares',  JSON.stringify(state.shares));

  fetch('/plugins/varaverk/api/rsync_win_arrays.php', {method:'POST', body:fd})
    .then(r => r.json())
    .then(d => {
      if (d.ok) {
        // Update cached data so cancel/re-render shows the new values
        if (_vvRyLastData) {
          _vvRyLastData.win_arrays ??= {};
          _vvRyLastData.win_arrays[key] = {
            scripts: state.scripts.map(s => s.id),
            shares:  state.shares.map(s => s.profile ? `${s.path}|${s.profile}` : s.path),
          };
        }
        vvRyWinCancel(key);
      } else {
        if (btn) { btn.disabled = false; btn.textContent = 'Save changes'; }
        if (fb)  { fb.style.color = '#ef5350'; fb.textContent = (d.errors || ['Save failed']).join(', '); }
      }
    })
    .catch(e => {
      if (btn) { btn.disabled = false; btn.textContent = 'Save changes'; }
      if (fb)  { fb.style.color = '#ef5350'; fb.textContent = 'Request failed'; }
    });
}

// ── Edit panel builder ────────────────────────────────────────────────────────
function _vvRyBuildEditPanel(key) {
  const panel = document.querySelector(`.vv-ry-win[data-win-key="${key}"] .vv-ry-win-panel`);
  if (!panel) return;
  panel.style.display = '';
  const state   = _vvRyWinEState[key] || {scripts:[], shares:[]};
  const scripts = state.scripts;
  const shares  = state.shares;
  let h = '';

  // ── Scripts list ─────────────────────────────────────────────────────────────
  h += `<div style="font-size:9px;font-weight:700;color:#2a2a2a;text-transform:uppercase;letter-spacing:.07em;margin-bottom:5px;">Scripts</div>`;
  for (let i = 0; i < scripts.length; i++) {
    const s     = scripts[i];
    const parts = s.id.split('/');
    const dir   = parts.length > 1 ? parts.slice(0,-1).join('/') : '';
    const name  = parts[parts.length-1].replace(/\.sh$/,'');
    h += `<div style="display:flex;align-items:center;gap:3px;padding:2px 0;border-bottom:1px solid #111;">
      <button onclick="event.stopPropagation();vvRyERemScript('${key}',${i})"
              style="background:none;border:none;color:#4a1a1a;cursor:pointer;padding:0 3px;font-size:13px;line-height:1;flex-shrink:0;">×</button>
      <div style="display:flex;flex-direction:column;flex-shrink:0;">
        <button onclick="event.stopPropagation();vvRyEMvScript('${key}',${i},-1)" ${i===0?'disabled':''}
                style="background:none;border:none;color:${i===0?'#1e1e1e':'#3a5a3a'};cursor:${i===0?'default':'pointer'};padding:0;font-size:8px;line-height:1.2;">▲</button>
        <button onclick="event.stopPropagation();vvRyEMvScript('${key}',${i},1)" ${i===scripts.length-1?'disabled':''}
                style="background:none;border:none;color:${i===scripts.length-1?'#1e1e1e':'#3a5a3a'};cursor:${i===scripts.length-1?'default':'pointer'};padding:0;font-size:8px;line-height:1.2;">▼</button>
      </div>
      <span style="font-size:10px;color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;">
        ${dir?`<span style="color:#2a2a2a;">${dir}/</span>`:''}${name}
      </span>
    </div>`;
  }
  // Script add row
  const existIds = new Set(scripts.map(s => s.id));
  h += `<div style="display:flex;gap:4px;margin-top:5px;align-items:center;" onclick="event.stopPropagation()">
    <select id="vv-ry-esel-${key}"
            style="background:#0d0d0d;border:1px solid #1e1e1e;border-radius:3px;color:#666;
                   font-size:10px;padding:3px 5px;flex:1;outline:none;min-width:0;">
      <option value="">— add script —</option>`;
  if (_vvRyScriptLib) {
    for (const [folder, items] of Object.entries(_vvRyScriptLib)) {
      const avail = items.filter(it => !existIds.has(it.id));
      if (!avail.length) continue;
      h += `<optgroup label="${folder}">`;
      for (const it of avail) h += `<option value="${it.id}">${it.label}</option>`;
      h += `</optgroup>`;
    }
  }
  h += `</select>
    <button onclick="event.stopPropagation();vvRyEAddScript('${key}')"
            style="background:#0a1a0a;border:1px solid #1a3a1a;color:#4caf50;font-size:10px;
                   padding:3px 9px;border-radius:3px;cursor:pointer;flex-shrink:0;">+ Add</button>
  </div>`;

  // ── Shares list ───────────────────────────────────────────────────────────────
  h += `<div style="border-top:1px solid #161616;margin:8px 0;"></div>
  <div style="font-size:9px;font-weight:700;color:#2a2a2a;text-transform:uppercase;letter-spacing:.07em;margin-bottom:5px;">Sync shares</div>`;
  for (let i = 0; i < shares.length; i++) {
    const sh = shares[i];
    const bn = sh.path.split('/').pop();
    h += `<div style="display:flex;align-items:center;gap:3px;padding:2px 0;border-bottom:1px solid #111;">
      <button onclick="event.stopPropagation();vvRyERemShare('${key}',${i})"
              style="background:none;border:none;color:#4a1a1a;cursor:pointer;padding:0 3px;font-size:13px;line-height:1;flex-shrink:0;">×</button>
      <div style="display:flex;flex-direction:column;flex-shrink:0;">
        <button onclick="event.stopPropagation();vvRyEMvShare('${key}',${i},-1)" ${i===0?'disabled':''}
                style="background:none;border:none;color:${i===0?'#1e1e1e':'#3a5a3a'};cursor:${i===0?'default':'pointer'};padding:0;font-size:8px;line-height:1.2;">▲</button>
        <button onclick="event.stopPropagation();vvRyEMvShare('${key}',${i},1)" ${i===shares.length-1?'disabled':''}
                style="background:none;border:none;color:${i===shares.length-1?'#1e1e1e':'#3a5a3a'};cursor:${i===shares.length-1?'default':'pointer'};padding:0;font-size:8px;line-height:1.2;">▼</button>
      </div>
      <span style="font-size:10px;color:#666;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${sh.path}">${bn}</span>
      ${sh.profile?`<span style="font-size:9px;color:#2a3a2a;flex-shrink:0;font-family:monospace;">${sh.profile}</span>`:''}
    </div>`;
  }
  // Share add form
  h += `<div style="margin-top:6px;" onclick="event.stopPropagation()">
    <div style="display:flex;gap:4px;margin-bottom:3px;">
      <input id="vv-ry-epath-${key}" type="text" placeholder="/mnt/user/…" autocomplete="off"
             style="background:#0d0d0d;border:1px solid #1e1e1e;border-radius:3px;color:#888;
                    font-size:10px;padding:3px 6px;flex:1;min-width:0;font-family:monospace;outline:none;"
             onkeydown="if(event.key==='Enter')vvRyEBrowse('${key}')">
      <button onclick="vvRyEBrowse('${key}')"
              style="background:#0a1a2a;border:1px solid #1a3a5a;color:#4a9eff;font-size:10px;
                     padding:3px 8px;border-radius:3px;cursor:pointer;flex-shrink:0;white-space:nowrap;">⟳ Browse</button>
    </div>
    <div id="vv-ry-ebrowse-${key}" class="vv-ms-dirs" style="display:none;max-height:120px;" data-last-path=""></div>
    <div style="display:flex;gap:4px;margin-top:4px;align-items:center;">
      <select id="vv-ry-eprof-${key}"
              style="background:#0d0d0d;border:1px solid #1e1e1e;border-radius:3px;color:#666;
                     font-size:10px;padding:3px 5px;flex:1;min-width:0;outline:none;">
        <option value="">no profile</option>
      </select>
      <button onclick="vvRyEAddShare('${key}')"
              style="background:#0a1a0a;border:1px solid #1a3a1a;color:#4caf50;font-size:10px;
                     padding:3px 9px;border-radius:3px;cursor:pointer;flex-shrink:0;white-space:nowrap;">+ Add</button>
    </div>
  </div>`;

  // ── Save / Cancel ─────────────────────────────────────────────────────────────
  h += `<div style="display:flex;gap:6px;align-items:center;margin-top:10px;padding-top:8px;border-top:1px solid #1e1e1e;">
    <button id="vv-ry-esave-${key}" onclick="event.stopPropagation();vvRyWinSave('${key}')"
            style="background:#1a2a1a;border:1px solid #2d4a2d;color:#4caf50;font-size:11px;
                   padding:4px 14px;border-radius:3px;cursor:pointer;">Save changes</button>
    <button onclick="event.stopPropagation();vvRyWinCancel('${key}')"
            style="background:#111;border:1px solid #222;color:#555;font-size:11px;
                   padding:4px 12px;border-radius:3px;cursor:pointer;">Cancel</button>
    <span id="vv-ry-efb-${key}" style="font-size:10px;color:#ef5350;"></span>
  </div>`;

  panel.innerHTML = h;
  _vvRyLoadProfiles(key);
}

// ── Script mutations ──────────────────────────────────────────────────────────
function vvRyEMvScript(key, idx, dir) {
  const s = _vvRyWinEState[key]?.scripts; if (!s) return;
  const n = idx + dir; if (n < 0 || n >= s.length) return;
  [s[idx], s[n]] = [s[n], s[idx]]; _vvRyBuildEditPanel(key);
}
function vvRyERemScript(key, idx) {
  const s = _vvRyWinEState[key]?.scripts; if (!s) return;
  s.splice(idx, 1); _vvRyBuildEditPanel(key);
}
function vvRyEAddScript(key) {
  const sel = document.getElementById('vv-ry-esel-' + key);
  if (!sel?.value) return;
  const s = _vvRyWinEState[key]?.scripts; if (!s) return;
  if (s.find(x => x.id === sel.value)) return;
  s.push({id: sel.value, enabled: true}); _vvRyBuildEditPanel(key);
}

// ── Share mutations ───────────────────────────────────────────────────────────
function vvRyEMvShare(key, idx, dir) {
  const s = _vvRyWinEState[key]?.shares; if (!s) return;
  const n = idx + dir; if (n < 0 || n >= s.length) return;
  [s[idx], s[n]] = [s[n], s[idx]]; _vvRyBuildEditPanel(key);
}
function vvRyERemShare(key, idx) {
  const s = _vvRyWinEState[key]?.shares; if (!s) return;
  s.splice(idx, 1); _vvRyBuildEditPanel(key);
}
function vvRyEAddShare(key) {
  const path = document.getElementById('vv-ry-epath-' + key)?.value.trim();
  const prof = document.getElementById('vv-ry-eprof-' + key)?.value.trim() || '';
  if (!path || !path.startsWith('/')) return;
  const s = _vvRyWinEState[key]?.shares; if (!s) return;
  s.push({path, profile: prof});
  document.getElementById('vv-ry-epath-' + key).value = '';
  const dirsEl = document.getElementById('vv-ry-ebrowse-' + key);
  if (dirsEl) dirsEl.style.display = 'none';
  _vvRyBuildEditPanel(key);
}

// ── Share path browser ────────────────────────────────────────────────────────
function vvRyEBrowse(key) {
  const pathEl = document.getElementById('vv-ry-epath-' + key);
  const dirsEl = document.getElementById('vv-ry-ebrowse-' + key);
  if (!pathEl || !dirsEl) return;
  const path = pathEl.value.trim() || '/mnt/user';

  if (dirsEl.dataset.lastPath === path && dirsEl.style.display !== 'none') {
    dirsEl.style.display = 'none'; dirsEl.dataset.lastPath = ''; return;
  }
  dirsEl.innerHTML = '<div style="color:#444;font-size:10px;padding:4px;">Loading…</div>';
  dirsEl.style.display = '';

  fetch(`/plugins/varaverk/api/manual_sync.php?action=browse_local&path=${encodeURIComponent(path)}&_=${Date.now()}`)
    .then(r => r.json())
    .then(d => {
      if (!d.ok) { dirsEl.innerHTML = `<div class="vv-ms-dir" style="color:#ef5350;cursor:default;">${d.error}</div>`; return; }
      if (pathEl) pathEl.value = d.path;
      dirsEl.dataset.lastPath = d.path;
      let h = '';
      if (d.parent) {
        const pn = d.parent === '/' ? '/' : (d.parent.replace(/^.*\//,'') || d.parent) + '/';
        h += `<div class="vv-ms-dir parent" onclick="vvRyENavTo('${key}',${JSON.stringify(d.parent)})">↑ ${pn}</div>`;
      }
      if (!d.dirs.length) {
        h += '<div class="vv-ms-dir" style="color:#2a2a2a;cursor:default;">— empty —</div>';
      } else {
        for (const dir of d.dirs) {
          const name = dir.replace(/^.*\//,'') || dir;
          h += `<div class="vv-ms-dir" onclick="vvRyENavTo('${key}',${JSON.stringify(dir)})" title="${dir}">▶ ${name}</div>`;
        }
      }
      dirsEl.innerHTML = h;
    })
    .catch(() => { dirsEl.innerHTML = '<div class="vv-ms-dir" style="color:#ef5350;cursor:default;">Browse failed</div>'; });
}

function vvRyENavTo(key, path) {
  const pathEl = document.getElementById('vv-ry-epath-' + key);
  if (pathEl) pathEl.value = path;
  vvRyEBrowse(key);
}

// ── Profile dropdown loader ───────────────────────────────────────────────────
function _vvRyLoadProfiles(key) {
  const sel = document.getElementById('vv-ry-eprof-' + key);
  if (!sel) return;
  const populate = () => {
    sel.innerHTML = '<option value="">no profile</option>' +
      (_vvRyProfiles || []).map(p => `<option value="${p}">${p}</option>`).join('');
  };
  if (_vvRyProfiles !== null) { populate(); return; }
  fetch('/plugins/varaverk/api/rsync_win_arrays.php?action=list_profiles&_=' + Date.now())
    .then(r => r.json())
    .then(d => { _vvRyProfiles = d.ok ? (d.profiles || []) : []; populate(); })
    .catch(() => { _vvRyProfiles = []; populate(); });
}

// ── Toggle a boolean flag — called by inline onclick ──────────────────────────
function vvRyToggle(trackEl, name) {
  const on = !trackEl.classList.contains('on');
  trackEl.classList.toggle('on', on);
  const fd = new FormData();
  fd.append('name', name);
  fd.append('enabled', on ? '1' : '0');
  fetch('/plugins/varaverk/api/flag_toggle.php', { method: 'POST', body: fd })
    .then(r => r.json())
    .then(d => { if (!d.ok) { trackEl.classList.toggle('on', !on); } })
    .catch(()  => { trackEl.classList.toggle('on', !on); });
}

// ── Save scalar defaults ──────────────────────────────────────────────────────
function vvRySaveDefaults() {
  const btn = document.getElementById('vv-ry-save');
  const fb  = document.getElementById('vv-ry-save-fb');
  const changes = [
    { file: 'master.conf', key: 'BW_LIMIT',          value: document.getElementById('vv-ry-bwlimit').value, type: 'scalar' },
    { file: 'master.conf', key: 'RETRY_COUNT',        value: document.getElementById('vv-ry-retry').value,   type: 'scalar' },
    { file: 'master.conf', key: 'SLEEP',              value: document.getElementById('vv-ry-sleep').value,   type: 'scalar' },
    { file: 'master.conf', key: 'BANDWIDTH_WARN_GB',  value: document.getElementById('vv-ry-bwwarn').value,  type: 'scalar' },
  ];
  btn.disabled = true;
  btn.textContent = 'Saving…';
  fb.textContent = '';
  const fd = new FormData();
  fd.append('id', 'rsync');
  fd.append('changes', JSON.stringify(changes));
  fetch('/plugins/varaverk/api/confform.php', { method: 'POST', body: fd })
    .then(r => r.json())
    .then(d => {
      btn.disabled = false;
      btn.textContent = 'Save';
      fb.style.color = d.ok ? '#4caf50' : '#ef5350';
      fb.textContent  = d.ok ? 'Saved ✓' : ('Error: ' + (d.error || 'unknown'));
      if (d.ok) setTimeout(() => { fb.textContent = ''; }, 3000);
    })
    .catch(() => {
      btn.disabled = false;
      btn.textContent = 'Save';
      fb.style.color = '#ef5350';
      fb.textContent = 'Request failed';
    });
}

// ── Profile Editor ────────────────────────────────────────────────────────────

let _vvRpProfiles = {};
let _vvRpSelected = null;
let _vvRpIsNew    = false;
let _vvRpLoaded   = false;

const RP_FIELDS = ['bw_limit','retry_count','sleep','container_delay',
                   'critical_containers','delayed_containers','exclude_dirs','remote_restart'];

// Known checkbox flags in order
const RP_FLAG_ORDER = ['-av','--human-readable','--info=progress2',
                       '--delete','--inplace','--no-whole-file','--checksum',
                       '--bwlimit=$BW_LIMIT','--compress'];

function vvRpOpen() {
  document.getElementById('vv-ry-info-view').style.display = 'none';
  document.getElementById('vv-rp-view').style.display = '';
  if (!_vvRpLoaded) vvRpLoad();
}
function vvRpClose() {
  document.getElementById('vv-rp-view').style.display = 'none';
  document.getElementById('vv-ry-info-view').style.display = '';
}

function vvRyInfoToggle(hdr) {
  const body = hdr.nextElementSibling;
  const chev = hdr.querySelector('.vv-ry-info-chev');
  const open = body.style.display !== 'none';
  body.style.display = open ? 'none' : '';
  if (chev) chev.textContent = open ? '▸' : '▾';
}

// Build opts string from checkboxes + extra, update preview + hidden input
function vvRpBuildOpts() {
  const checked = Array.from(document.querySelectorAll('#vv-rp-flag-group input[type=checkbox]:checked'))
                       .map(c => c.dataset.flag);
  const extra   = (document.getElementById('vv-rp-opts-extra')?.value || '').trim();
  const parts   = [...checked, ...(extra ? [extra] : [])];
  const val     = parts.join(' ');
  const preview = document.getElementById('vv-rp-opts-preview');
  const hidden  = document.getElementById('vv-rp-rsync_opts');
  if (preview) preview.textContent = val || '(none)';
  if (hidden)  hidden.value = val;
}

// Parse opts string → set checkboxes + extra input
function _vvRpApplyOpts(opts) {
  const tokens = (opts || '').trim().split(/\s+/).filter(Boolean);
  const knownSet = new Set(RP_FLAG_ORDER);
  const extra = [];

  document.querySelectorAll('#vv-rp-flag-group input[type=checkbox]').forEach(cb => {
    cb.checked = tokens.includes(cb.dataset.flag);
  });

  for (const t of tokens) {
    if (!knownSet.has(t)) extra.push(t);
  }
  const extraEl = document.getElementById('vv-rp-opts-extra');
  if (extraEl) extraEl.value = extra.join(' ');
  vvRpBuildOpts();
}

function vvRpLoad() {
  fetch('/plugins/varaverk/api/rsync_profiles.php?action=list&_=' + Date.now())
    .then(r => r.json())
    .then(d => {
      if (!d.ok) return;
      _vvRpProfiles = d.profiles || {};
      _vvRpLoaded   = true;
      _vvRpPopulateSelector();
      if (_vvRpSelected && _vvRpProfiles[_vvRpSelected]) {
        _vvRpFillForm(_vvRpSelected);
      } else if (_vvRpIsNew) {
        _vvRpClearForm();
      } else if (Object.keys(_vvRpProfiles).length) {
        _vvRpSelected = Object.keys(_vvRpProfiles).sort()[0];
        _vvRpFillForm(_vvRpSelected);
      }
    })
    .catch(() => {});
}

function _vvRpPopulateSelector() {
  const sel = document.getElementById('vv-rp-selector');
  if (!sel) return;
  const names = Object.keys(_vvRpProfiles).sort();
  sel.innerHTML = (names.length ? '' : '<option value="">No profiles</option>')
    + names.map(n => `<option value="${n}"${n === _vvRpSelected ? ' selected' : ''}>${n}</option>`).join('');
}

function _vvRpFillForm(name) {
  const p = _vvRpProfiles[name] || {};
  document.getElementById('vv-rp-name').value    = name;
  document.getElementById('vv-rp-name').readOnly = true;
  document.getElementById('vv-rp-name-hint').textContent = 'rename by creating a new profile with a different name';
  _vvRpApplyOpts(p.rsync_opts || '');
  RP_FIELDS.forEach(f => {
    const el = document.getElementById('vv-rp-' + f);
    if (el) el.value = p[f] ?? '';
  });
  document.getElementById('vv-rp-form').style.display  = '';
  document.getElementById('vv-rp-empty').style.display = 'none';
  const del = document.getElementById('vv-rp-del-btn');
  if (del) del.style.display = '';
  const sel = document.getElementById('vv-rp-selector');
  if (sel) sel.value = name;
}

function _vvRpClearForm() {
  document.getElementById('vv-rp-name').value    = '';
  document.getElementById('vv-rp-name').readOnly = false;
  document.getElementById('vv-rp-name-hint').textContent = 'new profile name';
  // Default opts for new profile
  _vvRpApplyOpts('-av --human-readable --bwlimit=$BW_LIMIT');
  document.getElementById('vv-rp-bw_limit').value        = '5000';
  document.getElementById('vv-rp-retry_count').value     = '3';
  document.getElementById('vv-rp-sleep').value           = '300';
  document.getElementById('vv-rp-container_delay').value = '5';
  RP_FIELDS.filter(f => !['bw_limit','retry_count','sleep','container_delay'].includes(f))
    .forEach(f => { const el = document.getElementById('vv-rp-' + f); if (el) el.value = ''; });
  document.getElementById('vv-rp-form').style.display  = '';
  document.getElementById('vv-rp-empty').style.display = 'none';
  const del = document.getElementById('vv-rp-del-btn');
  if (del) del.style.display = 'none';
  const sel = document.getElementById('vv-rp-selector');
  if (sel) sel.value = '';
}

function vvRpOnSelect(name) {
  if (!name) return;
  _vvRpSelected = name; _vvRpIsNew = false;
  _vvRpFillForm(name);
  document.getElementById('vv-rp-fb').textContent = '';
}

function vvRpNew() {
  _vvRpSelected = null; _vvRpIsNew = true;
  _vvRpClearForm();
  document.getElementById('vv-rp-fb').textContent = '';
  document.getElementById('vv-rp-name').focus();
}

function vvRpSave() {
  const name = document.getElementById('vv-rp-name').value.trim();
  if (!name) { document.getElementById('vv-rp-fb').style.color='#ef5350'; document.getElementById('vv-rp-fb').textContent='Enter a profile name'; return; }

  const fb  = document.getElementById('vv-rp-fb');
  const btn = document.querySelector('#vv-rp-card .vv-ry-save-btn');
  btn.disabled = true; btn.textContent = 'Saving…'; fb.textContent = '';

  const fd = new FormData();
  fd.append('action',     'save');
  fd.append('name',       name);
  fd.append('rsync_opts', document.getElementById('vv-rp-rsync_opts')?.value?.trim() || '');
  RP_FIELDS.forEach(f => {
    const el = document.getElementById('vv-rp-' + f);
    fd.append(f, el ? el.value.trim() : '');
  });

  fetch('/plugins/varaverk/api/rsync_profiles.php', { method: 'POST', body: fd })
    .then(r => r.json())
    .then(d => {
      btn.disabled = false; btn.textContent = 'Save Profile';
      if (d.ok) {
        fb.style.color = '#4caf50';
        fb.textContent = 'Saved ✓';
        _vvRpSelected = name;
        _vvRpIsNew    = false;
        vvRpLoad(); // refresh profile list
        setTimeout(() => { fb.textContent = ''; }, 3000);
      } else {
        fb.style.color = '#ef5350';
        fb.textContent = d.error || 'Save failed';
      }
    })
    .catch(() => { btn.disabled = false; btn.textContent = 'Save Profile'; fb.style.color='#ef5350'; fb.textContent='Request failed'; });
}

function vvRpDelete() {
  const name = document.getElementById('vv-rp-name').value.trim();
  if (!name) return;
  if (!confirm(`Delete profile "${name}"?\n\nThis removes it from all PROFILE_* arrays in master.conf.`)) return;

  const fb  = document.getElementById('vv-rp-fb');
  const del = document.getElementById('vv-rp-del-btn');
  del.disabled = true; fb.textContent = '';

  const fd = new FormData();
  fd.append('action', 'delete');
  fd.append('name', name);

  fetch('/plugins/varaverk/api/rsync_profiles.php', { method: 'POST', body: fd })
    .then(r => r.json())
    .then(d => {
      del.disabled = false;
      if (d.ok) {
        _vvRpSelected = null;
        _vvRpIsNew    = false;
        document.getElementById('vv-rp-form').style.display  = 'none';
        document.getElementById('vv-rp-empty').style.display = '';
        fb.style.color = '#4caf50'; fb.textContent = 'Deleted ✓';
        vvRpLoad();
        setTimeout(() => { fb.textContent = ''; }, 3000);
      } else {
        fb.style.color = '#ef5350'; fb.textContent = d.error || 'Delete failed';
      }
    })
    .catch(() => { del.disabled = false; fb.style.color='#ef5350'; fb.textContent='Request failed'; });
}

// ── Manual Sync ───────────────────────────────────────────────────────────────

let _vvMsHosts    = [];
let _vvMsParent   = null;
let _vvMsPollId   = null;
let _vvMsSshKey   = '';   // key path from host*.conf

function vvMsToggleOutput() {
  const out = document.getElementById('vv-ms-out');
  const btn = document.getElementById('vv-ms-show-btn');
  const open = out.style.display !== 'none';
  out.style.display = open ? 'none' : '';
  btn.textContent   = open ? '▾ Show Rsync' : '▴ Hide Rsync';
}

function vvMsStop() {
  const token = document.getElementById('vv-ms-stop-btn').dataset.token;
  if (!token) return;
  const btn = document.getElementById('vv-ms-stop-btn');
  btn.textContent = '⟳ Stopping…'; btn.disabled = true;
  const fd = new FormData();
  fd.append('action', 'stop');
  fd.append('token',  token);
  fetch('/plugins/varaverk/api/manual_sync.php', { method: 'POST', body: fd })
    .then(r => r.json())
    .then(() => {})
    .catch(() => {})
    .finally(() => { btn.textContent = '■ Stop'; btn.disabled = false; });
}

function vvMsToggle() {
  const body = document.getElementById('vv-ms-body');
  const btn  = document.getElementById('vv-ms-tog');
  const open = body.style.display !== 'none';
  body.style.display = open ? 'none' : '';
  btn.textContent    = open ? '▾ Show' : '▴ Hide';
  if (!open) {
    if (!_vvMsHosts.length) _vvMsLoadHosts();
    else vvMsUpdatePreview();
  }
}

function _vvMsLoadHosts() {
  const sel = document.getElementById('vv-ms-host');
  sel.innerHTML = '<option value="">Loading…</option>';
  fetch('/plugins/varaverk/api/manual_sync.php?action=hosts&_=' + Date.now())
    .then(r => r.json())
    .then(d => {
      if (!d.ok) { sel.innerHTML = '<option value="">Error loading hosts</option>'; return; }
      _vvMsHosts  = d.hosts;
      _vvMsSshKey = d.ssh_key || '';
      const keyEl = document.getElementById('vv-ms-key-info');
      if (keyEl) keyEl.textContent = _vvMsSshKey ? _vvMsSshKey.replace(/^.*\//, '') : '(none configured)';
      vvMsUpdatePreview();
      if (!d.hosts.length) {
        sel.innerHTML = '<option value="">No remote hosts configured</option>';
        return;
      }
      sel.innerHTML = d.hosts.map(h => {
        const dot = h.online === null ? '◌' : h.online ? '●' : '○';
        const col = h.online === null ? '#444' : h.online ? '#4caf50' : '#ef5350';
        return `<option value="${h.slot}" data-col="${col}">${dot} ${h.id} — ${h.hostname}</option>`;
      }).join('');
      // Auto-select first and auto-browse
      vvMsHostChanged();
    })
    .catch(() => { sel.innerHTML = '<option value="">Failed to load</option>'; });
}

function vvMsHostChanged() {
  const sel  = document.getElementById('vv-ms-host');
  const slot = sel.value;
  const host = _vvMsHosts.find(h => h.slot === slot);
  const stat = document.getElementById('vv-ms-host-status');

  if (!host) { stat.textContent = ''; return; }

  const onlineTxt = host.online === null ? 'status unknown'
                  : host.online ? '● online' + (host.ip ? ' · ' + host.ip : '')
                  : '○ offline';
  const onlineCol = host.online === null ? '#333' : host.online ? '#4caf50' : '#ef5350';
  stat.style.color   = onlineCol;
  stat.textContent   = onlineTxt;

  // Auto-browse /mnt/user on host change
  const rpEl = document.getElementById('vv-ms-rpath');
  if (!rpEl.value) rpEl.value = '/mnt/user';
  vvMsBrowse();
}

function vvMsBrowse() {
  const slot      = document.getElementById('vv-ms-host').value;
  const path      = document.getElementById('vv-ms-rpath').value.trim() || '/mnt/user';
  const browseBtn = document.getElementById('vv-ms-browse-btn');
  const dirsEl    = document.getElementById('vv-ms-dirs');
  const errEl     = document.getElementById('vv-ms-browse-err');
  if (!slot) return;

  // Toggle: already showing this path → collapse
  if (dirsEl.style.display !== 'none' && browseBtn.dataset.lastPath === path) {
    dirsEl.style.display = 'none';
    errEl.style.display  = 'none';
    browseBtn.textContent = '⟳ Browse';
    browseBtn.dataset.lastPath = '';
    return;
  }

  browseBtn.textContent = '⟳';
  browseBtn.disabled    = true;
  dirsEl.style.display  = 'none';
  errEl.style.display   = 'none';

  fetch(`/plugins/varaverk/api/manual_sync.php?action=browse&host=${encodeURIComponent(slot)}&path=${encodeURIComponent(path)}&_=${Date.now()}`)
    .then(r => r.json())
    .then(d => {
      browseBtn.textContent = '⟳ Browse';
      browseBtn.disabled    = false;
      if (!d.ok) {
        errEl.textContent   = d.error || 'Browse failed';
        errEl.style.display = '';
        return;
      }
      _vvMsParent = d.parent;
      document.getElementById('vv-ms-rpath').value = d.path;
      browseBtn.dataset.lastPath = d.path;
      _vvMsRenderDirs('vv-ms-dirs', d.dirs, d.parent, 'vvMsNavTo');
    })
    .catch(e => {
      browseBtn.textContent = '⟳ Browse';
      browseBtn.disabled    = false;
      errEl.textContent     = 'Request failed: ' + e;
      errEl.style.display   = '';
    });
}

function vvMsUpdatePreview() {
  const flags = Array.from(document.querySelectorAll('#vv-ms-card input[data-flag]:checked'))
                     .map(el => el.dataset.flag).join(' ');
  const bw    = parseInt(document.getElementById('vv-ms-bw')?.value) || 0;
  const prev  = document.getElementById('vv-ms-flags-preview');
  const bwPrev= document.getElementById('vv-ms-bwlimit-preview');
  if (prev)  prev.textContent  = flags || '(none)';
  if (bwPrev) bwPrev.textContent = bw > 0 ? '--bwlimit=' + bw : '';
}

function _vvMsRenderDirs(dirsElId, dirs, parent, navFn) {
  const dirsEl = document.getElementById(dirsElId);
  if (!dirsEl) return;
  let html = '';

  if (parent !== null) {
    const pname = parent === '/' ? '/' : (parent.replace(/^.*\//, '') || parent) + '/';
    html += `<div class="vv-ms-dir parent" onclick="${navFn}(${JSON.stringify(parent)})">↑ ${pname}</div>`;
  }

  if (!dirs.length) {
    html += '<div class="vv-ms-dir" style="color:#2a2a2a;cursor:default;">— no subdirectories —</div>';
  } else {
    for (const d of dirs) {
      const name = d.replace(/^.*\//, '') || d;
      html += `<div class="vv-ms-dir" onclick="${navFn}(${JSON.stringify(d)})" title="${d}">▶ ${name}</div>`;
    }
  }

  dirsEl.innerHTML     = html;
  dirsEl.style.display = '';
}

// ── Remote browse ──────────────────────────────────────────────────────────────

function vvMsNavTo(path) {
  document.getElementById('vv-ms-rpath').value = path;
  vvMsBrowse();
}

function vvMsNavUp() {
  const cur = (document.getElementById('vv-ms-rpath').value || '/mnt/user').replace(/\/$/, '') || '/';
  const up  = cur === '/' ? '/' : (cur.substring(0, cur.lastIndexOf('/')) || '/');
  document.getElementById('vv-ms-rpath').value = up;
  vvMsBrowse();
}

// ── Local browse ───────────────────────────────────────────────────────────────

function vvMsLocalNavTo(path) {
  document.getElementById('vv-ms-local').value = path;
  vvMsBrowseLocal();
}

function vvMsLocalNavUp() {
  const cur = (document.getElementById('vv-ms-local').value || '/mnt/user').replace(/\/$/, '') || '/';
  const up  = cur === '/' ? '/' : (cur.substring(0, cur.lastIndexOf('/')) || '/');
  document.getElementById('vv-ms-local').value = up;
  vvMsBrowseLocal();
}

function vvMsBrowseLocal() {
  const path   = document.getElementById('vv-ms-local').value.trim() || '/mnt/user';
  const btn    = document.getElementById('vv-ms-lbrowse-btn');
  const dirsEl = document.getElementById('vv-ms-local-dirs');
  const errEl  = document.getElementById('vv-ms-lbrowse-err');

  // Toggle: already showing this path → collapse
  if (dirsEl.style.display !== 'none' && btn.dataset.lastPath === path) {
    dirsEl.style.display = 'none';
    errEl.style.display  = 'none';
    btn.textContent = '⟳ Browse';
    btn.dataset.lastPath = '';
    return;
  }

  btn.textContent = '⟳'; btn.disabled = true;
  dirsEl.style.display = 'none';
  errEl.style.display  = 'none';

  fetch(`/plugins/varaverk/api/manual_sync.php?action=browse_local&path=${encodeURIComponent(path)}&_=${Date.now()}`)
    .then(r => r.json())
    .then(d => {
      btn.textContent = '⟳ Browse'; btn.disabled = false;
      if (!d.ok) { errEl.textContent = d.error || 'Browse failed'; errEl.style.display = ''; return; }
      document.getElementById('vv-ms-local').value = d.path;
      btn.dataset.lastPath = d.path;
      _vvMsRenderDirs('vv-ms-local-dirs', d.dirs, d.parent, 'vvMsLocalNavTo');
    })
    .catch(e => {
      btn.textContent = '⟳ Browse'; btn.disabled = false;
      errEl.textContent = 'Request failed: ' + e; errEl.style.display = '';
    });
}

function vvMsRun() {
  const local  = document.getElementById('vv-ms-local').value.trim();
  const slot   = document.getElementById('vv-ms-host').value;
  const rpath  = document.getElementById('vv-ms-rpath').value.trim();
  const user   = document.getElementById('vv-ms-user').value;
  const bw     = parseInt(document.getElementById('vv-ms-bw').value) || 0;
  const useKey = document.getElementById('vv-ms-usekey').checked;
  const isDry  = document.getElementById('vv-ms-dryrun').checked;
  // Build flags from all checked checkboxes in the card
  const flags  = Array.from(document.querySelectorAll('#vv-ms-card input[data-flag]:checked'))
                      .map(el => el.dataset.flag).join(' ');

  if (!local)  { alert('Enter a local source path.'); return; }
  if (!slot)   { alert('Select a remote server.'); return; }
  if (!rpath)  { alert('Enter a remote destination path.'); return; }

  const btn    = document.getElementById('vv-ms-run-btn');
  const stat   = document.getElementById('vv-ms-run-status');
  const out    = document.getElementById('vv-ms-out');
  const badge  = document.getElementById('vv-ms-badge');

  const stopBtn = document.getElementById('vv-ms-stop-btn');
  const showBtn = document.getElementById('vv-ms-show-btn');

  btn.disabled      = true;
  out.style.display = '';         // auto-open output
  showBtn.textContent = '▴ Hide Rsync';
  out.textContent   = isDry ? '⟳ Dry run starting…' : '⟳ Rsync starting in background…';
  stat.style.color  = '#4a9eff';
  stat.textContent  = 'Starting…';

  badge.style.display    = '';
  badge.textContent      = isDry ? 'DRY RUN' : 'RUNNING';
  badge.style.background = '#0a1a2a';
  badge.style.color      = '#4a9eff';
  badge.style.border     = '1px solid #1a3a5a';

  const fd = new FormData();
  fd.append('action',      'run');
  fd.append('local',       local);
  fd.append('host',        slot);
  fd.append('remote_path', rpath);
  fd.append('user',        user);
  fd.append('flags',       flags);
  fd.append('bw_limit',    bw);
  fd.append('use_key',     useKey ? '1' : '0');

  fetch('/plugins/varaverk/api/manual_sync.php', { method: 'POST', body: fd })
    .then(r => r.json())
    .then(d => {
      if (!d.ok) {
        btn.disabled     = false;
        out.textContent  = '✗ ' + (d.error || 'Failed to start');
        stat.style.color = '#ef5350';
        stat.textContent = 'Error';
        badge.style.display = 'none';
        return;
      }
      stat.textContent = 'Running…';
      stopBtn.dataset.token = d.token;
      stopBtn.style.display = '';
      _vvMsStartPoll(d.token, btn, stat, out, badge, stopBtn);
    })
    .catch(e => {
      btn.disabled     = false;
      out.textContent  = '✗ Request failed: ' + e;
      stat.style.color = '#ef5350';
      stat.textContent = 'Error';
      badge.style.display = 'none';
    });
}

function _vvMsStartPoll(token, btn, stat, out, badge, stopBtn) {
  if (_vvMsPollId) clearInterval(_vvMsPollId);
  let ticks = 0;

  _vvMsPollId = setInterval(() => {
    ticks++;
    fetch(`/plugins/varaverk/api/manual_sync.php?action=poll&token=${token}&_=${Date.now()}`)
      .then(r => r.json())
      .then(d => {
        if (!d.ok) return;
        if (!d.started && ticks < 6) return;
        if (d.output) {
          out.textContent = d.output;
          out.scrollTop   = out.scrollHeight;
        }
        if (d.done || (!d.started && ticks >= 6)) {
          clearInterval(_vvMsPollId);
          _vvMsPollId  = null;
          btn.disabled = false;
          if (stopBtn) { stopBtn.style.display = 'none'; stopBtn.dataset.token = ''; }
          const cancelled = d.output && d.output.includes('Cancelled by user');
          const ok        = !cancelled && d.output && !d.output.includes('rsync error');
          stat.style.color = cancelled ? '#ff9800' : ok ? '#4caf50' : '#ef5350';
          stat.textContent = cancelled ? 'Cancelled' : ok ? 'Done ✓' : 'Finished with errors';
          badge.textContent      = cancelled ? 'CANCELLED' : ok ? 'DONE' : 'ERROR';
          badge.style.background = cancelled ? '#1a1200' : ok ? '#0a1a0a' : '#200d0d';
          badge.style.color      = cancelled ? '#ff9800'  : ok ? '#4caf50' : '#ef5350';
          badge.style.border     = cancelled ? '1px solid #3a2800' : ok ? '1px solid #1a3a1a' : '1px solid #3a1a1a';
          setTimeout(() => { badge.style.display = 'none'; stat.textContent = ''; }, 6000);
        }
      })
      .catch(() => {});
  }, 1500);
}
</script>
