<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Scheduler tab. The largest page in the plugin and the main operational surface: the full
//   script library, per-job schedules, live run status and logs, dry-run output, conf editing
//   per script, and the custom-script editor.
//
// OPERATIONAL MODEL
//   Writes state that changes what the machine does on a timer. Enabling, disabling or
//   rescheduling a job here takes effect on the next cron rebuild — this page decides what
//   runs unattended at 1am.
//
// DESIGN PRINCIPLES
//   Script descriptions come from the scripts themselves.
//       Requires include/scheduler.php, which parses the PURPOSE block out of each bash
//       header. The library listing and the script's own documentation are the same text and
//       cannot drift. This page is the reason the repo-wide header convention exists.
//
//   The job tree is derived from conf, not maintained here.
//       Orchestrator job lists come from master.conf arrays; custom scripts are whatever
//       *.sh sits in CUSTOM_SCRIPTS_DIR. Nothing has to be registered in two places.
//
//   Long operations never block the page.
//       Runs, dry-runs and imports go through api/run.php and api/dryrun.php with status
//       polled separately, so a script that takes ten minutes does not hold a request open.
//
// OPERATIONAL SAFEGUARDS
//   Running a script from the UI is confirmed first. Several of these delete files.
//
//   Dry-run is offered alongside run for anything destructive, and its output is shown in
//   full rather than summarised — reading the list is the point.
//
//   Stale locks are cleared explicitly through api/clearlock.php, never automatically. A lock
//   that looks stale may belong to a job still running.
//
//   Conf edits are surgical, preserving the comment blocks that document every threshold.
//
//   Custom scripts live outside the git repo, so a user's own scripts are never touched by a
//   pull and never committed by accident.
//
//   Script content and log output render escaped throughout — this page displays arbitrary
//   file contents.
//
// RENDERS
//   Job tree by orchestrator tier, per-job schedule and toggle controls, live status and
//   logs, dry-run output, per-script conf forms, custom script editor, run history
//
// DEPENDS ON
//   include/scheduler.php  required directly — schedule, cron rebuild, script library
//   api/scheduler.php      schedule read/write        api/run.php        execute
//   api/dryrun.php         dry-run execute           api/stop.php       terminate
//   api/status.php         live run status           api/log.php        log tail
//   api/script.php         script read/write         api/readscript.php source
//   api/scriptinfo.php     header metadata           api/import_script.php
//   api/conf_toggle.php    per-script enable         api/flag_toggle.php
//   api/confform.php       conf forms                api/rawconf.php    raw conf edit
//   api/board.php          overview board            api/recent.php     recent runs
//   api/snapshot.php       state snapshot            api/clearlock.php  stale lock clear
//   api/savefolders.php    folder grouping           api/reorderarray.php
//   api/rsync_standalone.php
require_once dirname(__DIR__) . '/include/scheduler.php';

// Setup mode — auto-open a conf file and force the editing sequence
$vv_setup_conf = preg_match('/^[\w.]+\.conf$/', $_GET['vv_setup'] ?? '')
    ? $_GET['vv_setup'] : '';

// Deep-link from Monitor → open a specific script on load (e.g. ?tab=scheduler&vv_script=Media/x.sh)
$vv_open_script = preg_match('#^[\w./-]+\.sh$#', $_GET['vv_script'] ?? '')
    ? $_GET['vv_script'] : '';
$_vv_my_host       = vv_detect_host();
$_vv_local_host_conf = ($_vv_my_host !== 'unknown') ? $_vv_my_host . '.conf' : 'host1.conf';

$tree    = vv_job_tree();
$customs = vv_custom_scripts();
$tools   = vv_tools_scripts();
$_library = vv_script_library();
$_folders = vv_folders_load();

// Full README and Manual for collapsed accordion panels
$readmePath = SCRIPTS_DIR . '/README.md';
$manualPath = SCRIPTS_DIR . '/Manual.md';
$readmeText = file_exists($readmePath) ? file_get_contents($readmePath) : '';
$manualText = file_exists($manualPath) ? file_get_contents($manualPath) : '';

// Repository tree: all .sh + .md files in git folder order, minus Plugin/
$_repoFiles  = [];
$_repoBase   = rtrim(SCRIPTS_DIR, '/') . '/';
$_repoSkip   = ['Plugin', '.git'];
try {
    $_ri = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator(SCRIPTS_DIR, RecursiveDirectoryIterator::SKIP_DOTS)
    );
    foreach ($_ri as $_rf) {
        if (!$_rf->isFile()) continue;
        if (!preg_match('/\.(sh|md)$/i', $_rf->getFilename())) continue;
        $_rel   = ltrim(str_replace($_repoBase, '', $_rf->getPathname()), '/');
        $_parts = explode('/', $_rel);
        if (in_array($_parts[0], $_repoSkip)) continue;
        $_repoFiles[] = $_rel;
    }
} catch (Exception $_re) {}
sort($_repoFiles);

// Docs tree: README and Manual with their per-module children
$_mainReadme   = in_array('README.md', $_repoFiles) ? 'README.md' : null;
$_childReadmes = array_values(array_filter($_repoFiles, function($f) {
    return preg_match('/^README/i', basename($f)) && $f !== 'README.md';
}));
sort($_childReadmes);
$_mainManual   = in_array('Manual.md', $_repoFiles) ? 'Manual.md' : null;
$_childManuals = array_values(array_filter($_repoFiles, function($f) {
    return preg_match('/^Manual/i', basename($f)) && $f !== 'Manual.md';
}));
sort($_childManuals);

// Job stats for notification board
$totalJobs = 0; $scheduledJobs = 0;
foreach ($tree as $orch) {
    $totalJobs++;
    if (!empty($orch['cron']) && $orch['enabled']) $scheduledJobs++;
    foreach (($orch['children'] ?? []) as $child) {
        $totalJobs++;
        if (!empty($child['cron']) && $child['enabled']) $scheduledJobs++;
    }
}
$runningScripts = [];
exec('pgrep -af bash 2>/dev/null', $psLines);
foreach ($psLines as $line) {
    if (preg_match('#' . preg_quote(SCRIPTS_DIR, '#') . '/([^\s]+\.sh)#', $line, $rm)) {
        $runningScripts[] = basename($rm[1], '.sh');
    }
}
$runningScripts = array_unique($runningScripts);
?>

<div id="vv-scheduler">
  <div id="vv-sched-layout">

    <!-- ── Left: script cards ── -->
    <div id="vv-sched-left">
      <div id="vv-sched-cards">

      <?php foreach ($tree as $orch): $oid = htmlspecialchars($orch['id']); ?>
        <div class="vv-card vv-wide vv-sched-card" data-id="<?= $oid ?>" data-conf-arrays="<?= htmlspecialchars(implode(',', $orch['conf_arrays'] ?? [])) ?>">

          <div class="vv-job-row vv-orch-row">
            <label class="vv-toggle" title="Enable/disable">
              <input type="checkbox" class="vv-enabled"
                <?= $orch['enabled'] ? 'checked' : '' ?>
                onchange="vvSaveOrch(this)">
              <span class="vv-slider"></span>
            </label>
            <span class="vv-cog-btn" onclick="vvClickCog(this)" title="Settings">⚙</span><span class="vv-job-label" onclick="vvClickLabel(this)" style="cursor:pointer"><?= htmlspecialchars($orch['label']) ?></span>
            <?php if (str_starts_with($orch['cron'], 'array_')): ?>
              <span class="vv-event-badge"><?= $orch['cron'] === 'array_start' ? '⚡ Array Start' : '⚡ Array Stop' ?></span>
            <?php endif; ?>
            <input type="text" class="vv-cron" value="<?= htmlspecialchars($orch['cron']) ?>"
              placeholder="cron  or array_start / array_stop"
              onblur="vvSaveCronBlur(this)">
            <span class="vv-save-check"></span>
          </div>
          <?php if (!empty($orch['desc'])): ?>
            <div class="vv-job-desc" title="<?= htmlspecialchars($orch['desc']) ?>"><?= htmlspecialchars($orch['desc']) ?></div>
          <?php endif; ?>
          <div class="vv-job-actions">
            <button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">&#9654; Run</button>
            <button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">&#9654; Dry Run</button>
            <?php $orchLog = vv_job_log_path($orch['id']); ?>
            <button class="vv-btn-sm vv-log-btn<?= (file_exists($orchLog) && filesize($orchLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>
            <label class="vv-log-label" title="Enable verbose --log output">
              <input type="checkbox" class="vv-log-enabled"
                <?= $orch['log_enabled'] ? 'checked' : '' ?>
                onchange="vvSaveJob(this)">
              <span>Verbose</span>
            </label>
            <span class="vv-job-dot"></span>
            <?php if (!empty($orch['children'])): ?>
              <button class="vv-advanced-toggle" style="margin-left:auto;width:110px" onclick="vvToggleAdvanced(this)">Advanced ▸</button>
            <?php endif; ?>
          </div>

          <?php if (!empty($orch['children'])): ?>
            <div class="vv-children" data-conf-arrays="<?= htmlspecialchars(implode(',', $orch['conf_arrays'] ?? [])) ?>" style="display:none;">
              <?php foreach ($orch['children'] as $child): $cid = htmlspecialchars($child['id']); ?>
                <?php if (($child['type'] ?? 'script') === 'conf_flag'):
                  $_rsync    = vv_rsync_standalone($child['flag_name']);
                  $_rsyncFn  = htmlspecialchars($child['flag_name']);
                  $_rsyncOid = htmlspecialchars($orch['id']);
                ?>
                <div class="vv-script" data-id="<?= $cid ?>"
                     data-type="conf_flag"
                     data-flag-name="<?= htmlspecialchars($child['flag_name']) ?>"
                     data-flag-value="<?= !empty($child['flag_value']) ? '1' : '0' ?>">
                  <div class="vv-job-row">
                    <label class="vv-toggle" title="Toggle <?= htmlspecialchars($child['flag_name']) ?> in master.conf">
                      <input type="checkbox" class="vv-enabled"
                        <?= !empty($child['flag_value']) ? 'checked' : '' ?>
                        onchange="vvSaveChild(this)">
                      <span class="vv-slider"></span>
                    </label>
                    <span class="vv-job-label"><?= htmlspecialchars($child['label']) ?></span>
                    <span class="vv-flag-badge"<?= empty($child['flag_value']) ? ' style="display:none"' : '' ?>><?= htmlspecialchars($child['flag_name']) ?></span>
                    <input type="text" class="vv-cron vv-rsync-standalone"
                           value="<?= htmlspecialchars($_rsync['cron']) ?>"
                           placeholder="standalone cron">
                    <span class="vv-save-check"></span>
                  </div>
                  <?php if (!empty($child['desc'])): ?>
                    <div class="vv-job-desc" title="<?= htmlspecialchars($child['desc']) ?>"><?= htmlspecialchars($child['desc']) ?></div>
                  <?php endif; ?>
                  <div class="vv-job-actions">
                    <button class="vv-btn-sm vv-run-btn vv-rsync-standalone" onclick="vvRunJob(this)">&#9654; Run</button>
                    <button class="vv-btn-sm vv-dry-btn vv-rsync-standalone" onclick="vvDryRun(this)">&#9654; Dry Run</button>
                    <?php $childLog = vv_job_log_path($child['id']); ?>
                    <button class="vv-btn-sm vv-log-btn<?= (file_exists($childLog) && filesize($childLog) > 0) ? ' vv-has-log' : '' ?> vv-rsync-standalone" onclick="vvSelectLog(this)">Log</button>
                    <input type="text" class="vv-rsync-location vv-rsync-standalone"
                           value="<?= htmlspecialchars($_rsync['location']) ?>"
                           placeholder="/mnt/user/path"
                           data-flag="<?= $_rsyncFn ?>"
                           data-orch-id="<?= $_rsyncOid ?>">
                    <button class="vv-btn-sm vv-rsync-save-btn vv-rsync-standalone"
                            onclick="vvSaveRsyncStandalone(this)">Save</button>
                    <span class="vv-job-dot"></span>
                  </div>
                </div>
                <?php else: ?>
                <div class="vv-script" data-id="<?= $cid ?>"
                     data-conf-managed="<?= $child['conf_managed'] ? '1' : '0' ?>"
                     data-conf-enabled="<?= $child['conf_enabled'] === true ? '1' : ($child['conf_enabled'] === false ? '0' : '') ?>"
                     data-conf-array="<?= htmlspecialchars($child['conf_array'] ?? '') ?>">
                  <div class="vv-job-row">
                    <label class="vv-toggle" title="Enable/disable">
                      <input type="checkbox" class="vv-enabled"
                        <?= $child['enabled'] ? 'checked' : '' ?>
                        onchange="vvSaveChild(this)">
                      <span class="vv-slider"></span>
                    </label>
                    <span class="vv-cog-btn" onclick="vvClickCog(this)" title="Settings">⚙</span><span class="vv-job-label" onclick="vvClickLabel(this)" style="cursor:pointer"><?= htmlspecialchars($child['label']) ?></span>
                    <input type="text" class="vv-cron" value="<?= htmlspecialchars($child['cron']) ?>"
                      placeholder="cron  (orch off only)"
                      onblur="vvSaveCronBlur(this)">
                    <span class="vv-save-check"></span>
                  </div>
                  <?php if (!empty($child['desc'])): ?>
                    <div class="vv-job-desc" title="<?= htmlspecialchars($child['desc']) ?>"><?= htmlspecialchars($child['desc']) ?></div>
                  <?php endif; ?>
                  <div class="vv-job-actions">
                    <button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">&#9654; Run</button>
                    <button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">&#9654; Dry Run</button>
                    <?php $childLog = vv_job_log_path($child['id']); ?>
                    <button class="vv-btn-sm vv-log-btn<?= (file_exists($childLog) && filesize($childLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>
                    <label class="vv-log-label" title="Enable verbose --log output">
                      <input type="checkbox" class="vv-log-enabled"
                        <?= $child['log_enabled'] ? 'checked' : '' ?>
                        onchange="vvSaveJob(this)">
                      <span>Verbose</span>
                    </label>
                    <input type="text" class="vv-script-args" placeholder="extra args"
                      style="display:<?= $orch['enabled'] ? 'none' : '' ?>">
                    <span class="vv-job-dot"></span>
                  </div>
                </div>
                <?php endif; ?>
              <?php endforeach; ?>
            </div>
          <?php endif; ?>

        </div>
      <?php endforeach; ?>

      <!-- Tools container -->
      <div class="vv-card vv-wide vv-sched-card vv-tools-card">
        <div class="vv-job-row">
          <span class="vv-job-label" style="font-weight:bold;">Tools</span>
          <span class="vv-custom-count"><?= count($tools) ?> tool<?= count($tools) !== 1 ? 's' : '' ?></span>
          <button class="vv-advanced-toggle" style="margin-left:auto;width:100px" onclick="vvToggleTools(this)">Tools ▸</button>
        </div>
        <div class="vv-children" id="vv-tools-children" style="display:none;">
          <?php if (empty($tools)): ?>
            <p class="vv-custom-empty">No scripts found in Tools/ or Plugin/*/tools/.</p>
          <?php else: ?>
            <?php foreach ($tools as $ts): $tsid = htmlspecialchars($ts['id']); ?>
              <div class="vv-script" data-id="<?= $tsid ?>">
                <div class="vv-job-row">
                  <label class="vv-toggle" title="Enable/disable cron">
                    <input type="checkbox" class="vv-enabled"
                      <?= $ts['enabled'] ? 'checked' : '' ?>
                      onchange="vvSaveJob(this)">
                    <span class="vv-slider"></span>
                  </label>
                  <span class="vv-job-label"><?= htmlspecialchars($ts['label']) ?></span>
                  <input type="text" class="vv-cron" value="<?= htmlspecialchars($ts['cron']) ?>"
                    placeholder="cron expression"
                    onblur="vvSaveCronBlur(this)">
                  <span class="vv-save-check"></span>
                </div>
                <?php if (!empty($ts['desc'])): ?>
                  <div class="vv-job-desc" title="<?= htmlspecialchars($ts['desc']) ?>"><?= htmlspecialchars($ts['desc']) ?></div>
                <?php endif; ?>
                <div class="vv-job-actions">
                  <button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">&#9654; Run</button>
                  <button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">&#9654; Dry Run</button>
                  <?php $tsLog = vv_job_log_path($ts['id']); ?>
                  <button class="vv-btn-sm vv-log-btn<?= (file_exists($tsLog) && filesize($tsLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>
                  <label class="vv-log-label" title="Enable verbose --log output">
                    <input type="checkbox" class="vv-log-enabled"
                      <?= $ts['log_enabled'] ? 'checked' : '' ?>
                      onchange="vvSaveJob(this)">
                    <span>Verbose</span>
                  </label>
                  <input type="text" class="vv-script-args" placeholder="extra args">
                  <span class="vv-job-dot"></span>
                </div>
              </div>
            <?php endforeach; ?>
          <?php endif; ?>
        </div>
      </div>

      <!-- Custom Scripts container -->
      <?php
      // Build set of IDs that belong to a folder
      $_inFolder = [];
      foreach ($_folders as $_fn => $_fs) foreach ($_fs as $_fid) $_inFolder[] = $_fid;
      ?>
      <div class="vv-card vv-wide vv-sched-card vv-custom-card">
        <div class="vv-job-row">
          <span class="vv-job-label" style="font-weight:bold;">Custom Scripts</span>
          <span class="vv-custom-count"><?= count($customs) ?> script<?= count($customs) !== 1 ? 's' : '' ?></span>
          <button class="vv-advanced-toggle" style="margin-left:auto;width:100px" onclick="vvToggleCustom(this)">Scripts ▸</button>
        </div>
        <div class="vv-children" id="vv-custom-children" style="display:none;">
          <?php if (empty($customs)): ?>
            <p class="vv-custom-empty">No custom scripts yet — click <strong>+ Create Script</strong> to create one.</p>
          <?php else: ?>

            <?php // ── Folder groups ──────────────────────────────────────────────
            foreach ($_folders as $_fname => $_fscripts):
              $_folderScripts = array_filter($customs, fn($c) => in_array($c['id'], $_fscripts));
            ?>
            <div class="vv-folder-group" data-folder="<?= htmlspecialchars($_fname) ?>">
              <div class="vv-folder-row" onclick="vvToggleFolder(this)">
                <span class="vv-folder-chevron">▸</span>
                <span class="vv-folder-name"><?= htmlspecialchars($_fname) ?></span>
                <span class="vv-folder-count"><?= count($_folderScripts) ?></span>
              </div>
              <div class="vv-folder-children" style="display:none">
                <?php foreach ($_folderScripts as $cs): $csid = htmlspecialchars($cs['id']); ?>
                  <div class="vv-script" data-id="<?= $csid ?>">
                    <div class="vv-job-row">
                      <label class="vv-toggle" title="Enable/disable">
                        <input type="checkbox" class="vv-enabled" <?= $cs['enabled'] ? 'checked' : '' ?> onchange="vvSaveJob(this)">
                        <span class="vv-slider"></span>
                      </label>
                      <span class="vv-cog-btn" onclick="vvClickCog(this)" title="Settings">⚙</span><span class="vv-job-label" onclick="vvClickLabel(this)" style="cursor:pointer"><?= htmlspecialchars($cs['label']) ?></span>
                      <input type="text" class="vv-cron" value="<?= htmlspecialchars($cs['cron']) ?>" placeholder="cron expression" onblur="vvSaveCronBlur(this)">
                      <span class="vv-save-check"></span>
                    </div>
                    <?php if (!empty($cs['desc'])): ?>
                      <div class="vv-job-desc" title="<?= htmlspecialchars($cs['desc']) ?>"><?= htmlspecialchars($cs['desc']) ?></div>
                    <?php endif; ?>
                    <div class="vv-job-actions">
                      <button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">&#9654; Run</button>
                      <button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">&#9654; Dry Run</button>
                      <button class="vv-btn-sm vv-edit-btn" onclick="vvEditScript('<?= $csid ?>')">Edit</button>
                      <?php $csLog = vv_job_log_path($cs['id']); ?>
                      <button class="vv-btn-sm vv-log-btn<?= (file_exists($csLog) && filesize($csLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>
                      <label class="vv-log-label"><input type="checkbox" class="vv-log-enabled" <?= $cs['log_enabled'] ? 'checked' : '' ?> onchange="vvSaveJob(this)"><span>Verbose</span></label>
                      <input type="text" class="vv-script-args" placeholder="extra args">
                      <span class="vv-job-dot"></span>
                    </div>
                  </div>
                <?php endforeach; ?>
              </div>
            </div>
            <?php endforeach; ?>

            <?php // ── Unfoldered scripts ──────────────────────────────────────
            foreach ($customs as $cs):
              if (in_array($cs['id'], $_inFolder)) continue;
              $csid = htmlspecialchars($cs['id']);
            ?>
              <div class="vv-script" data-id="<?= $csid ?>">
                <div class="vv-job-row">
                  <label class="vv-toggle" title="Enable/disable">
                    <input type="checkbox" class="vv-enabled"
                      <?= $cs['enabled'] ? 'checked' : '' ?>
                      onchange="vvSaveJob(this)">
                    <span class="vv-slider"></span>
                  </label>
                  <span class="vv-cog-btn" onclick="vvClickCog(this)" title="Settings">⚙</span><span class="vv-job-label" onclick="vvClickLabel(this)" style="cursor:pointer"><?= htmlspecialchars($cs['label']) ?></span>
                  <input type="text" class="vv-cron" value="<?= htmlspecialchars($cs['cron']) ?>"
                    placeholder="cron expression"
                    onblur="vvSaveCronBlur(this)">
                  <span class="vv-save-check"></span>
                </div>
                <?php if (!empty($cs['desc'])): ?>
                  <div class="vv-job-desc" title="<?= htmlspecialchars($cs['desc']) ?>"><?= htmlspecialchars($cs['desc']) ?></div>
                <?php endif; ?>
                <div class="vv-job-actions">
                  <button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">&#9654; Run</button>
                  <button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">&#9654; Dry Run</button>
                  <button class="vv-btn-sm vv-edit-btn" onclick="vvEditScript('<?= $csid ?>')">Edit</button>
                  <?php $csLog = vv_job_log_path($cs['id']); ?>
                  <button class="vv-btn-sm vv-log-btn<?= (file_exists($csLog) && filesize($csLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>
                  <label class="vv-log-label" title="Enable verbose --log output">
                    <input type="checkbox" class="vv-log-enabled"
                      <?= $cs['log_enabled'] ? 'checked' : '' ?>
                      onchange="vvSaveJob(this)">
                    <span>Verbose</span>
                  </label>
                  <input type="text" class="vv-script-args" placeholder="extra args">
                  <span class="vv-job-dot"></span>
                </div>
              </div>
            <?php endforeach; ?>
          <?php endif; ?>
        </div>
      </div>

      </div><!-- /#vv-sched-cards -->

      <div class="vv-sched-footer">
        <button class="vv-save-btn vv-add-script-btn" onclick="vvAddScript()">+ Create Script</button>
        <button class="vv-save-btn vv-import-script-btn" onclick="vvImportScriptOpen()" title="Move an existing script from anywhere on the server into Custom Scripts">+ Import Script</button>
        <button class="vv-save-btn vv-new-folder-btn" onclick="vvNewFolder()" title="Create a folder in Custom Scripts">+ Folder</button>
        <button id="vv-arrange-btn" class="vv-save-btn" onclick="vvToggleArrange()" title="Drag scripts between orchestrators">Arrange</button>
        <button id="vv-arrange-save-btn" class="vv-save-btn vv-arrange-save-btn" onclick="vvSaveArrange()" style="display:none">Save Arrangement</button>
        <button id="vv-arrange-cancel-btn" class="vv-save-btn vv-delete-btn" onclick="vvCancelArrange()" style="display:none">Cancel</button>
        <button id="vv-delete-script-btn" class="vv-save-btn vv-delete-btn" onclick="vvDeleteScript()" style="display:none">&#128465; Delete</button>
      </div>

    </div><!-- /#vv-sched-left -->

    <!-- ── Right: suggestions + log panel ── -->
    <div id="vv-sched-right" class="vv-panel-visible">
      <div class="vv-card vv-log-card">
        <div class="vv-log-toolbar" style="margin-bottom:8px;">
          <div style="display:flex;align-items:center;gap:8px;">
            <button id="vv-back-btn" class="vv-btn-sm" onclick="vvBackToSuggestions()" style="display:none">&#9776; Scheduler Info</button>
            <button id="vv-restore-btn" class="vv-btn-sm" onclick="vvRestoreLastLog()" style="display:none">&#8592; <span id="vv-restore-label"></span></button>
            <span id="vv-log-title" style="font-size:13px;font-weight:bold;color:#ccc;">Scheduler Information</span>
            <span id="vv-log-dot" style="display:none;width:8px;height:8px;border-radius:50%;background:#4caf50;flex-shrink:0;"></span>
          </div>
          <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
            <label id="vv-auto-scroll-label" class="vv-log-label" title="Auto-scroll to newest line (pauses when you scroll up, resumes at bottom)" style="display:none">
              <input type="checkbox" id="vv-auto-scroll" checked> <span>Auto Scroll</span>
            </label>
            <label id="vv-invert-log-label" class="vv-log-label" title="Show newest lines at top" style="display:none">
              <input type="checkbox" id="vv-invert-log" onchange="vvToggleInvert(this)"> <span>Invert</span>
            </label>
            <button id="vv-cancel-edit-btn" class="vv-btn-sm vv-delete-btn" onclick="vvBackToSuggestions()" style="display:none">Cancel</button>
            <button id="vv-undo-btn" class="vv-btn-sm vv-undo-redo-btn" onclick="vvUndo()" style="display:none" disabled title="Undo (Ctrl+Z)">↩ Undo<span class="vv-undo-count" id="vv-undo-count"></span></button>
            <button id="vv-redo-btn" class="vv-btn-sm vv-undo-redo-btn" onclick="vvRedo()" style="display:none" disabled title="Redo (Ctrl+Y / Ctrl+Shift+Z)">↪ Redo</button>
            <button id="vv-save-script-btn" class="vv-btn-sm vv-save-script-btn-style" onclick="vvSaveScript()" style="display:none">Save Script</button>
            <button id="vv-save-conf-btn" class="vv-btn-sm vv-save-script-btn-style" onclick="vvSaveConf()" style="display:none">Save Config</button>
            <button id="vv-save-rawconf-btn" class="vv-btn-sm vv-save-script-btn-style" onclick="vvSaveRawConf()" style="display:none">Save Conf</button>
            <span id="vv-log-ts" class="vv-log-ts" style="display:inline-flex;flex-direction:column;line-height:1.3;white-space:nowrap"></span>
            <input type="text" id="vv-log-search" placeholder="Search…"
                   oninput="vvFilterLog(this.value)" style="display:none;">
            <button id="vv-stop-btn" class="vv-btn-sm" onclick="vvStopJob()" style="display:none" title="Stop running script and clear any stuck locks">■ Stop</button>
            <button id="vv-clear-btn" class="vv-btn-sm" onclick="vvClearRightLog()" style="display:none">Clear</button>
            <button id="vv-advanced-mode-btn" class="vv-btn-sm vv-adv-mode-btn" onclick="vvToggleAdvancedMode()" title="Toggle Advanced mode — shows full script source and raw conf editing">Advanced</button>
          </div>
        </div>

        <!-- Suggestions view (default) -->
        <div id="vv-suggestions">

          <!-- ── Notification Board ── -->
          <div class="vv-nb-board">
            <span class="vv-nb-stat"><?= $scheduledJobs ?> / <?= $totalJobs ?> scheduled</span>
            <span class="vv-nb-sep">·</span>
            <span class="vv-nb-stat <?= !empty($runningScripts) ? 'vv-nb-running' : '' ?>">
              <?= !empty($runningScripts) ? 'Running: ' . htmlspecialchars(implode(', ', $runningScripts)) : 'Idle' ?>
            </span>
            <span class="vv-nb-sep vv-adv-only" style="display:none">·</span>
            <?php $confFiles = vv_get_conf_files(); ?>
            <?php if (!empty($confFiles)): ?>
              <span class="vv-nb-conf-btns vv-adv-only" style="display:none">
                <?php foreach ($confFiles as $cf): ?>
                  <button class="vv-btn-sm vv-nb-conf-btn" onclick="vvEditRawConf('<?= htmlspecialchars($cf) ?>')"><?= htmlspecialchars($cf) ?></button>
                <?php endforeach; ?>
              </span>
            <?php endif; ?>
            <button class="vv-btn-sm vv-adv-only" id="vv-git-pull-btn"
                    onclick="vvGitPull(this)"
                    title="Pull latest scripts from repository and run conf upgrade"
                    style="display:none;margin-left:auto;">&#8635; Git Pull</button>
          </div>

          <!-- ── How do I use this (pinned) ── -->
          <div class="vv-sug-block vv-info-block" id="vv-how-to-use" data-save-key="how-to-use">
            <div class="vv-sug-header" onclick="vvToggleSug(this)">
              <span class="vv-sug-chevron">▾</span>
              <span class="vv-sug-title">How do I use this</span>
            </div>
            <div class="vv-sug-body vv-info-body">
              <ul class="vv-info-cols">
                <li><strong>Toggle</strong> — saves immediately to schedule.json</li>
                <li><strong>Cron field</strong> — auto-saved on focus-out; hint shows plain English; red border = invalid</li>
                <li><strong>⚙ Cog</strong> — opens settings panel: script header + matching README/Manual sections + config fields</li>
                <li><strong>Script name</strong> — click navigates to the matching entry in the Orch tree (right panel)</li>
                <li><strong>Run</strong> — fires script immediately; resets the cron timer so next fire is one full interval later</li>
                <li><strong>Dry Run</strong> — same as Run but passes <code>--dry-run</code>; no changes written to disk</li>
                <li><strong>Log</strong> — opens script log here; auto-scrolls; use Search box to filter/highlight lines</li>
                <li><strong>Stop</strong> — SIGTERM → 3 s → SIGKILL; clears stuck lock files</li>
                <li><strong>Verbose</strong> — appends <code>--log</code> flag for per-item detail output</li>
                <li><strong>Cancel</strong> — discards changes and returns here; file is never touched until Save</li>
                <li><strong>⚡ Array events</strong> — set any cron field to <code>array_start</code> or <code>array_stop</code>; badge appears; fires on Unraid array start/stop; works on any orch or custom script</li>
                <li><strong>Arrange</strong> — drag scripts between orchs; drop to right panel removes from orch; Save Arrangement commits to master.conf</li>
                <li><strong>+ Folder</strong> — collapsible subfolder in Custom Scripts; drag scripts in</li>
                <li><strong>Suggested cron</strong> — click the cron code badge in the Orch tree to apply and save instantly</li>
                <li><strong>Status dot (tree)</strong> — green/orange/red ● = last run ok/warn/error; dim = never run; hover for detail</li>
                <li><strong>Cron Calculator</strong> — type an expression or plain English; shows description + next 5 fires; Apply pushes to last-focused cron field</li>
                <li><strong>Recent Activity</strong> — last 24 runs; click a row to open that log; auto-expands on errors</li>
                <li><strong>Recent Errors</strong> — last error per script (7 days); click to open log; auto-expands on new errors</li>
                <li><strong>Advanced</strong> — top-right button, turns blue; ⚙ shows enriched view (header + docs + config); raw conf editing unlocked</li>
                <li><strong>Log search</strong> — filter box in toolbar when log is open; highlights matches, dims others</li>
                <li><strong>← Scheduler Info</strong> — returns here from any log, editor, conf, or script view</li>
                <li class="vv-info-sep">Orchestrator &amp; Child Behaviour</li>
                <li><strong>Orch ON</strong> — sole trigger; cron fires the orch; it calls children in sequence</li>
                <li><strong>Orch OFF</strong> — never runs automatically; children suppressed; each can get its own standalone cron</li>
                <li><strong>Child (orch ON)</strong> — toggle comments/uncomments the script in master.conf</li>
                <li><strong>Child (orch ON, no array)</strong> — orch hardcodes the call; toggle is display-only</li>
                <li><strong>Child (orch OFF)</strong> — enter a cron to run it standalone</li>
                <li><strong>Rsync badge</strong> — toggle writes TIER_RSYNC_ENABLED directly to master.conf</li>
                <li><strong>Rsync (orch OFF)</strong> — fill location + standalone cron + Save; both required for independent firing</li>
              </ul>
            </div>
          </div>

          <!-- ── Next Runs ── -->
          <div class="vv-sug-block" data-save-key="next-runs">
            <div class="vv-sug-header" onclick="vvToggleSug(this)">
              <span class="vv-sug-chevron">▾</span>
              <span class="vv-sug-title">Next Runs</span>
            </div>
            <div class="vv-sug-body" id="vv-nextruns-body">
              <div class="vv-board-placeholder">calculating…</div>
            </div>
          </div>

          <!-- ── Cron Calculator ── -->
          <div class="vv-sug-block" data-save-key="calc">
            <div class="vv-sug-header" onclick="vvToggleSug(this)">
              <span class="vv-sug-chevron">▸</span>
              <span class="vv-sug-title">Cron Calculator</span>
            </div>
            <div class="vv-sug-body" style="display:none">
              <div class="vv-calc-wrap">
                <input type="text" id="vv-calc-input" class="vv-calc-in"
                       placeholder="*/15 * * * *  or  every day at 3am"
                       oninput="vvCalcUpdate(this.value)"
                       onkeydown="if(event.key==='Enter')vvCalcApply()">
                <div id="vv-calc-result"></div>
                <button class="vv-btn-sm vv-calc-apply-btn" id="vv-calc-apply"
                        onclick="vvCalcApply()" style="display:none">
                  Apply to <span id="vv-calc-target-label">selected</span>
                </button>
              </div>
            </div>
          </div>

          <!-- ── Recent Activity ── -->
          <div class="vv-sug-block" data-save-key="activity">
            <div class="vv-sug-header" onclick="vvToggleSug(this)">
              <span class="vv-sug-chevron">▸</span>
              <span class="vv-sug-title">Recent Activity</span>
              <span id="vv-activity-badge" class="vv-hdr-badge vv-hdr-badge-red" style="display:none"></span>
            </div>
            <div class="vv-sug-body" style="display:none" id="vv-activity-body">
              <div class="vv-board-placeholder">loading…</div>
            </div>
          </div>

          <!-- ── Recent Errors ── -->
          <div class="vv-sug-block" data-save-key="errors">
            <div class="vv-sug-header" onclick="vvToggleSug(this)">
              <span class="vv-sug-chevron">▸</span>
              <span class="vv-sug-title">Recent Errors</span>
              <span id="vv-errors-badge" class="vv-hdr-badge vv-hdr-badge-red" style="display:none"></span>
            </div>
            <div class="vv-sug-body" style="display:none" id="vv-errors-body">
              <div class="vv-board-placeholder">loading…</div>
            </div>
          </div>

          <!-- ── Active Locks ── -->
          <div class="vv-sug-block" data-save-key="locks">
            <div class="vv-sug-header" onclick="vvToggleSug(this)">
              <span class="vv-sug-chevron">▸</span>
              <span class="vv-sug-title">Active Locks</span>
              <span id="vv-locks-badge" class="vv-hdr-badge vv-hdr-badge-orange" style="display:none"></span>
            </div>
            <div class="vv-sug-body" style="display:none" id="vv-locks-body">
              <div class="vv-board-placeholder">loading…</div>
            </div>
          </div>

          <!-- ── Partner ── -->
          <div class="vv-sug-block" data-save-key="partner">
            <div class="vv-sug-header" onclick="vvToggleSug(this)">
              <span class="vv-sug-chevron">▸</span>
              <span class="vv-sug-title">Partner</span>
              <span id="vv-partner-hdr" style="font-size:11px;color:#555;flex-shrink:0;">—</span>
            </div>
            <div class="vv-sug-body" style="display:none" id="vv-partner-body">
              <div class="vv-board-placeholder">loading…</div>
            </div>
          </div>

          <!-- ── Disabled on this host ── -->
          <?php
          $_confMap  = vv_conf_script_map();
          $_disabled = array_filter($_confMap, fn($v) => $v['enabled'] === false);
          ?>
          <div class="vv-sug-block" data-save-key="disabled">
            <div class="vv-sug-header" onclick="vvToggleSug(this)">
              <span class="vv-sug-chevron">▸</span>
              <span class="vv-sug-title">Disabled on this host</span>
              <?php if (!empty($_disabled)): ?>
                <span class="vv-hdr-badge vv-hdr-badge-gray"><?= count($_disabled) ?></span>
              <?php endif; ?>
            </div>
            <div class="vv-sug-body" style="display:none">
              <?php if (empty($_disabled)): ?>
                <div class="vv-board-placeholder">All scripts enabled.</div>
              <?php else: ?>
                <div class="vv-disabled-list">
                <?php foreach ($_disabled as $_drel => $_dv):
                  $_dname = str_replace('_', ' ', strtolower(preg_replace('/_SCRIPTS$/', '', $_dv['array'] ?? '')));
                ?>
                  <div class="vv-disabled-row">
                    <span class="vv-disabled-name"><?= htmlspecialchars(basename($_drel, '.sh')) ?></span>
                    <span class="vv-disabled-grp"><?= htmlspecialchars($_dname) ?></span>
                  </div>
                <?php endforeach; ?>
                </div>
              <?php endif; ?>
            </div>
          </div>

          <!-- ── Fallback ── -->
          <div class="vv-sug-block vv-info-block" data-save-key="fallback">
            <div class="vv-sug-header" onclick="vvToggleSug(this)">
              <span class="vv-sug-chevron">▾</span>
              <span class="vv-sug-title">Fallback</span>
            </div>
            <div class="vv-sug-body">
              <pre class="vv-readme-body">Not enterprise HA. No SLAs, no quorum nodes, no guaranteed uptime — don't put your billing system on this.

What it is: mutual automatic failover between two independent unRAID servers. When one goes down the other starts its containers, cuts over DNS, and keeps users online. When it comes back, everything hands back in the correct sequence — DDNS stops, containers stop, rsync writeback runs, containers start on the primary, primary DDNS starts last — so users hit the returning server only after it's actually ready.

Built from scratch. Refined through a year of production testing. The DDNS sequencing and handback order were the hardest parts to get right. Both directions are exercised regularly with fallback_test.sh.</pre>
            </div>
          </div>

          <!-- ── How it works ── -->
          <div class="vv-sug-block vv-info-block">
            <div class="vv-sug-header" onclick="vvToggleSug(this)">
              <span class="vv-sug-chevron">▸</span>
              <span class="vv-sug-title">How it works</span>
            </div>
            <div class="vv-sug-body" style="display:none">
              <pre class="vv-readme-body">Varaverk owns the cron layer. schedule.json stores every orchestrator's enabled state, cron expression, and log setting. When you save, the plugin rebuilds /etc/cron.d/varaverk — one entry per enabled orchestrator. That's all cron knows about.

Orchestrators are the only scripts with cron schedules. Each one reads its SCRIPTS list out of master.conf and runs each child in sequence. The children do the actual work. The orchestrator controls the order and the timing. A child failure stops the chain.

master.conf is the per-host control plane. Comment out a line to disable that script on this host without touching any code. Both servers run the same repository — different master.conf comments produce different behaviour per machine.

Children only get their own cron when their orchestrator is OFF. When the orch is on, it owns the timing completely.

common.sh provides the shared runtime every script sources: acquire_lock(), log_msg(), info(), success(), warn(), error(). Lock files live in /tmp/varaverk_locks/. acquire_lock() exits cleanly if a prior run is still in progress — nothing piles up.

Logs are written per script to the appdata path. The Log button shows the latest run. Verbose mode passes --log for per-step detail.

Array events bypass cron entirely. array_start fires when Unraid brings the array up; array_stop fires before shutdown. array_started handles all post-start setup in sequence; array_stopping handles the graceful teardown.

The watchdog tier runs every 15 minutes as a single-pass chain: resource_watchdog frees memory pressure first, docker_watchdog restarts down containers into that headroom, system_watchdog checks storage/WebGUI/network, stability_watchdog reboots as a last resort. The order matters — restarting containers before freeing memory just restarts them into the same pressure.</pre>
            </div>
          </div>

          <!-- ── This started as ── -->
          <div class="vv-sug-block vv-info-block">
            <div class="vv-sug-header" onclick="vvToggleSug(this)">
              <span class="vv-sug-chevron">▸</span>
              <span class="vv-sug-title">This started as...</span>
            </div>
            <div class="vv-sug-body" style="display:none">
              <pre class="vv-readme-body">One script. A few lines — checking whether a container was running and restarting it if not. Backgrounded through User Scripts, sleeping a minute between checks, essentially forgotten. It did its job.

Then a conversation with a buddy: wouldn't it be cool if it emailed when something crashed? Sure, added it. Wouldn't it be cool if it handled more containers? Easy — grow the list. Wouldn't it be cool if both servers stayed in sync automatically so neither of us had to remember to do it manually?

That one took a while.

Order started to matter. Freeing memory before attempting container restarts meant the containers actually stayed up. Sync had to happen before the media database refresh, not after. Watchdogs couldn't block each other. The big loop that tried to do everything started breaking into pieces — one piece per job, and a sequencer that knew which piece went first.

master.conf became the control plane: one file, two different sets of comments for two different hosts. Same codebase, different behaviour per machine. The orchestrator pattern came out of not wanting to duplicate logic.

By the time the User Scripts plugin couldn't clearly show what was actually scheduled, whether something was running, or why a sync had quietly stopped working two weeks ago — the interface became the next thing to build.

Varaverk started as a status table and a schedule editor. The scheduler became the main thing. The monitor followed.

Still the same two servers, two households, the same media stack running itself. The scripts just grew up around it.</pre>
            </div>
          </div>

          <!-- ── Docs tree ── -->
          <div class="vv-info-divider">Docs</div>
          <div id="vv-docs-tree">

          <?php if ($_mainReadme || !empty($_childReadmes)): ?>
          <div class="vv-sb-entry">
            <div class="vv-sb-row vv-sb-orch-row"
                 data-id="README.md"
                 data-hdr=""
                 data-desc="Project README"
                 onclick="vvSelectScript(this)">
              <?php if (!empty($_childReadmes)): ?>
                <span class="vv-sb-expand" onclick="vvToggleSbChildren(this,event)">▸</span>
              <?php else: ?>
                <span class="vv-sb-expand vv-sb-leaf"> </span>
              <?php endif; ?>
              <span class="vv-sb-name">README</span>
            </div>
            <?php if (!empty($_childReadmes)): ?>
            <div class="vv-sb-children" style="display:none">
              <?php foreach ($_childReadmes as $_cr):
                $_crLabel = preg_replace('/^README-?/i', '', basename($_cr));
                $_crLabel = preg_replace('/\.md$/i', '', $_crLabel);
                $_crLabel = str_replace('_', ' ', $_crLabel) ?: dirname($_cr);
              ?>
              <div class="vv-sb-row vv-sb-child-row"
                   data-id="<?= htmlspecialchars($_cr) ?>"
                   data-hdr=""
                   data-desc=""
                   onclick="vvSelectScript(this)">
                <span class="vv-sb-child-indent">└</span>
                <span class="vv-sb-name"><?= htmlspecialchars($_crLabel) ?></span>
              </div>
              <?php endforeach; ?>
            </div>
            <?php endif; ?>
          </div>
          <?php endif; ?>

          <?php if ($_mainManual || !empty($_childManuals)): ?>
          <div class="vv-sb-entry">
            <div class="vv-sb-row vv-sb-orch-row"
                 data-id="Manual.md"
                 data-hdr=""
                 data-desc="Project Manual"
                 onclick="vvSelectScript(this)">
              <?php if (!empty($_childManuals)): ?>
                <span class="vv-sb-expand" onclick="vvToggleSbChildren(this,event)">▸</span>
              <?php else: ?>
                <span class="vv-sb-expand vv-sb-leaf"> </span>
              <?php endif; ?>
              <span class="vv-sb-name">Manual</span>
            </div>
            <?php if (!empty($_childManuals)): ?>
            <div class="vv-sb-children" style="display:none">
              <?php foreach ($_childManuals as $_cm):
                $_cmLabel = preg_replace('/^Manual-?/i', '', basename($_cm));
                $_cmLabel = preg_replace('/\.md$/i', '', $_cmLabel);
                $_cmLabel = str_replace('_', ' ', $_cmLabel) ?: dirname($_cm);
              ?>
              <div class="vv-sb-row vv-sb-child-row"
                   data-id="<?= htmlspecialchars($_cm) ?>"
                   data-hdr=""
                   data-desc=""
                   onclick="vvSelectScript(this)">
                <span class="vv-sb-child-indent">└</span>
                <span class="vv-sb-name"><?= htmlspecialchars($_cmLabel) ?></span>
              </div>
              <?php endforeach; ?>
            </div>
            <?php endif; ?>
          </div>
          <?php endif; ?>

          </div><!-- /#vv-docs-tree -->

          <!-- ── Script browser tree ── -->
          <div class="vv-info-divider">Orch</div>
          <div id="vv-sb-tree">
          <?php foreach ($tree as $orch):
            $orchId   = $orch['id'];
            $orchHdr  = vv_script_header_clean(SCRIPTS_DIR . '/' . $orchId);
            $hasKids  = !empty($orch['children']);
          ?>
          <div class="vv-sb-entry">
            <div class="vv-sb-row vv-sb-orch-row"
                 data-id="<?= htmlspecialchars($orchId) ?>"
                 data-hdr="<?= htmlspecialchars($orchHdr) ?>"
                 data-desc="<?= htmlspecialchars($orch['desc']) ?>"
                 onclick="vvSelectScript(this)">
              <?php if ($hasKids): ?>
                <span class="vv-sb-expand" onclick="vvToggleSbChildren(this,event)">▸</span>
              <?php else: ?>
                <span class="vv-sb-expand vv-sb-leaf"> </span>
              <?php endif; ?>
              <span class="vv-sb-name"><?= htmlspecialchars($orch['label']) ?></span>
              <?php if (str_starts_with($orch['cron'], 'array_')): ?>
                <span class="vv-event-badge vv-sb-badge">⚡</span>
              <?php elseif ($orch['suggested_cron']): ?>
                <code class="vv-sug-cron vv-sb-cron" style="cursor:pointer" title="Click to apply cron" onclick="vvApplySugCron(this,event)"><?= htmlspecialchars($orch['suggested_cron']) ?></code>
              <?php endif; ?>
              <?php
                $_os = @json_decode(@file_get_contents(vv_job_stat_path($orchId)), true);
                $_or = $_os['status'] ?? '';
                $_oc = $_or === 'ok' ? 'vv-stat-ok' : ($_or === 'warn' ? 'vv-stat-warn' : ($_or === 'error' ? 'vv-stat-error' : ($_or === 'skipped' ? 'vv-stat-skip' : ($orch['enabled'] ? 'vv-sug-on' : 'vv-stat-none'))));
              ?>
              <span class="<?= $_oc ?> vv-sb-status" title="<?= $_or ? htmlspecialchars("last: $_or") : ($orch['enabled'] ? 'enabled' : 'never run') ?>">
                <?= ($_or || $orch['enabled']) ? '●' : '○' ?>
              </span>
            </div>
            <?php if ($hasKids): ?>
            <div class="vv-sb-children" style="display:none">
              <?php foreach ($orch['children'] as $child):
                $childHdr = vv_script_header_clean(SCRIPTS_DIR . '/' . $child['id']);
              ?>
              <div class="vv-sb-row vv-sb-child-row"
                   data-id="<?= htmlspecialchars($child['id']) ?>"
                   data-hdr="<?= htmlspecialchars($childHdr) ?>"
                   data-desc="<?= htmlspecialchars($child['desc']) ?>"
                   onclick="vvSelectScript(this)">
                <span class="vv-sb-child-indent">└</span>
                <span class="vv-sb-name"><?= htmlspecialchars($child['label']) ?></span>
                <?php if (!empty($child['suggested_cron'])): ?>
                  <code class="vv-sug-cron vv-sb-cron" style="cursor:pointer" title="Click to apply cron" onclick="vvApplySugCron(this,event)"><?= htmlspecialchars($child['suggested_cron']) ?></code>
                <?php endif; ?>
                <?php
                  $_cs = @json_decode(@file_get_contents(vv_job_stat_path($child['id'])), true);
                  $_cr = $_cs['status'] ?? '';
                  $_cc = $_cr === 'ok' ? 'vv-stat-ok' : ($_cr === 'warn' ? 'vv-stat-warn' : ($_cr === 'error' ? 'vv-stat-error' : ($_cr === 'skipped' ? 'vv-stat-skip' : (!empty($child['conf_enabled']) ? 'vv-sug-on' : 'vv-stat-none'))));
                ?>
                <span class="<?= $_cc ?> vv-sb-status" title="<?= $_cr ? htmlspecialchars("last: $_cr") : (!empty($child['conf_enabled']) ? 'enabled' : 'never run') ?>">
                  <?= ($_cr || !empty($child['conf_enabled'])) ? '●' : '○' ?>
                </span>
              </div>
              <?php endforeach; ?>
            </div>
            <?php endif; ?>
          </div>
          <?php endforeach; ?>
          </div><!-- /#vv-sb-tree -->

          <!-- ── Repository separator + tree (Advanced mode only) ── -->
          <div class="vv-info-divider vv-adv-only" style="display:none;margin-top:8px;">Repository</div>
          <?php if (!empty($_repoFiles)): ?>
          <div class="vv-sug-block vv-info-block vv-adv-only" style="display:none;margin-top:4px;">
            <div class="vv-sug-header" onclick="vvToggleSug(this)">
              <span class="vv-sug-chevron">▸</span>
              <span class="vv-sug-title">Varaverk</span>
            </div>
            <div class="vv-sug-body" style="display:none;padding:0;">
              <?php
              $_rootFiles = array_values(array_filter($_repoFiles, function($f) { return strpos($f, '/') === false; }));
              $_subFiles  = array_values(array_filter($_repoFiles, function($f) { return strpos($f, '/') !== false; }));
              // Root-level files under Varaverk/ divider
              if (!empty($_rootFiles)):
              ?>
              <div class="vv-info-divider" style="padding-left:8px;font-size:10px;">Varaverk/</div>
              <?php foreach ($_rootFiles as $_rel):
                $_isScript = (bool)preg_match('/\.sh$/i', $_rel);
                $_hdr      = $_isScript ? vv_script_header_clean(SCRIPTS_DIR . '/' . $_rel) : '';
              ?>
              <div class="vv-sb-row"
                   data-id="<?= htmlspecialchars($_rel) ?>"
                   data-hdr="<?= htmlspecialchars($_hdr) ?>"
                   data-desc=""
                   onclick="vvSelectScript(this)"
                   style="padding-left:18px">
                <span class="vv-sb-expand vv-sb-leaf"> </span>
                <span class="vv-sb-name" style="color:<?= $_isScript ? '#b0c4d0' : '#7a9eb5' ?>;font-size:12px;">
                  <?= htmlspecialchars($_rel) ?>
                </span>
              </div>
              <?php endforeach; endif; ?>
              <?php
              // Subdirectory files
              $_seenDirs = [];
              foreach ($_subFiles as $_rel):
                $_parts    = explode('/', $_rel);
                $_dirParts = array_slice($_parts, 0, -1);
                $_filename = end($_parts);
                $_isScript = (bool)preg_match('/\.sh$/i', $_filename);
                for ($_di = 0; $_di < count($_dirParts); $_di++):
                  $_dirPath = implode('/', array_slice($_dirParts, 0, $_di + 1));
                  if (!in_array($_dirPath, $_seenDirs)):
                    $_seenDirs[] = $_dirPath;
              ?>
              <div class="vv-info-divider" style="padding-left:<?= $_di * 14 + 8 ?>px;font-size:10px;">
                <?= htmlspecialchars($_dirParts[$_di]) ?>/
              </div>
              <?php
                  endif;
                endfor;
                $_depth = count($_dirParts);
                $_hdr   = $_isScript ? vv_script_header_clean(SCRIPTS_DIR . '/' . $_rel) : '';
              ?>
              <div class="vv-sb-row"
                   data-id="<?= htmlspecialchars($_rel) ?>"
                   data-hdr="<?= htmlspecialchars($_hdr) ?>"
                   data-desc=""
                   onclick="vvSelectScript(this)"
                   style="padding-left:<?= $_depth * 14 + 4 ?>px">
                <span class="vv-sb-expand vv-sb-leaf"> </span>
                <span class="vv-sb-name" style="color:<?= $_isScript ? '#b0c4d0' : '#7a9eb5' ?>;font-size:12px;">
                  <?= htmlspecialchars($_filename) ?>
                </span>
              </div>
              <?php endforeach; ?>
            </div>
          </div>
          <?php endif; ?>

        </div>

        <!-- Log view (shown when a script is selected) -->
        <pre id="vv-log-pre" class="vv-log-pre vv-log-right-pre" style="display:none"></pre>

        <!-- Config form view (script conf sections) -->
        <div id="vv-confform" style="display:none; overflow-y:auto;"></div>

        <!-- Script info view (header or full source for selected tree row) -->
        <div id="vv-si-view" style="display:none; overflow-y:auto;"></div>

        <!-- Editor view (custom scripts + raw conf) -->
        <div id="vv-editor" style="display:none">
          <div style="display:flex;gap:8px;align-items:center;margin-bottom:6px;">
            <span style="color:#888;font-size:12px;flex-shrink:0;">Name:</span>
            <input type="text" id="vv-editor-name" class="vv-cron" style="flex:1" placeholder="script_name  (no .sh)">
          </div>
          <div id="vv-editor-find">
            <div class="vv-find-row">
              <span class="vv-find-expand" id="vv-find-expand" onclick="vvFindToggleReplace()" title="Toggle Replace (Ctrl+H)">›</span>
              <input type="text" id="vv-find-input" placeholder="Find…"
                     oninput="vvFindSearch()" onkeydown="vvFindKeydown(event)">
              <span class="vv-find-count" id="vv-find-count"></span>
              <button class="vv-btn-sm vv-find-nav-btn" onclick="vvFindNav(-1)" title="Previous (Shift+Enter)">↑</button>
              <button class="vv-btn-sm vv-find-nav-btn" onclick="vvFindNav(1)"  title="Next (Enter)">↓</button>
              <span class="vv-find-x" onclick="vvFindClose()" title="Close (Esc)">×</span>
            </div>
            <div class="vv-replace-row" id="vv-replace-row">
              <span style="width:14px;flex-shrink:0"></span>
              <input type="text" id="vv-replace-input" placeholder="Replace with…"
                     onkeydown="vvReplaceKeydown(event)">
              <button class="vv-btn-sm vv-repl-btn" onclick="vvReplaceOne()" title="Replace (Enter)">Replace</button>
              <button class="vv-btn-sm vv-repl-btn" onclick="vvReplaceAll()" title="Replace All">All</button>
            </div>
          </div>
          <div id="vv-goto-bar">
            <span style="color:#666;font-size:12px;flex-shrink:0">Go to line</span>
            <input type="number" id="vv-goto-input" min="1" placeholder="#"
                   oninput="vvGotoApply()" onkeydown="vvGotoKeydown(event)">
            <span class="vv-goto-info" id="vv-goto-info"></span>
            <span class="vv-find-x" onclick="vvGotoClose()" title="Close (Esc)">×</span>
          </div>
          <!-- ── Keyboard Shortcuts — expanded by default, state remembered ── -->
          <div class="vv-sug-block" data-save-key="editor-shortcuts" style="margin-bottom:6px;border:1px solid #1e1e1e;border-radius:4px;">
            <div class="vv-sug-header" onclick="vvToggleSug(this)" style="padding:5px 8px;">
              <span class="vv-sug-chevron">▾</span>
              <span style="font-size:11px;font-weight:bold;color:#555;letter-spacing:.06em;text-transform:uppercase;">Keyboard Shortcuts</span>
            </div>
            <div class="vv-sug-body" style="padding:4px 10px 8px;">
              <ul class="vv-info-cols" style="font-size:11px;">
                <li><strong>Ctrl+S</strong> — save</li>
                <li><strong>Ctrl+Z / Ctrl+Y</strong> — undo / redo (200 steps)</li>
                <li><strong>Ctrl+/</strong> — toggle line comment</li>
                <li><strong>Tab / Shift+Tab</strong> — indent / dedent</li>
                <li><strong>Ctrl+F</strong> — find</li>
                <li><strong>Ctrl+H</strong> — find &amp; replace</li>
                <li><strong>Ctrl+G</strong> — go to line</li>
                <li><strong>Ctrl+D</strong> — select next occurrence</li>
                <li><strong>Ctrl+L</strong> — select line</li>
                <li><strong>Ctrl+Shift+K</strong> — delete line</li>
                <li><strong>Alt+↑/↓</strong> — move line</li>
                <li><strong>Alt+Shift+↓</strong> — duplicate line below</li>
                <li><strong>Home</strong> — smart (first non-space → col 0)</li>
                <li><strong>( { [ " `</strong> — auto-close pair; wraps selection; Backspace deletes both</li>
                <li><strong>A− / A+</strong> in status bar — font size (9–22px, persists)</li>
              </ul>
            </div>
          </div>

          <div id="vv-editor-wrap">
            <pre id="vv-ln-gutter" aria-hidden="true"></pre>
            <div id="vv-editor-inner">
              <div id="vv-cur-line"></div>
              <pre id="vv-hl-overlay" aria-hidden="true"></pre>
              <textarea id="vv-editor-body" class="vv-editor-body" spellcheck="false"
                oninput="vvSyncHlOverlay(); vvUndoCapture()" onscroll="vvSyncHlScroll()"
                onkeydown="vvEditorKeydown(event)"
                onkeyup="vvEditorCursorMoved()" onmouseup="vvEditorCursorMoved()"
                onclick="vvEditorCursorMoved()"></textarea>
            </div>
          </div>
          <div id="vv-editor-status">
            <span class="vv-es-pos" id="vv-es-pos">Ln 1, Col 1</span>
            <span class="vv-es-sel" id="vv-es-sel"></span>
            <button class="vv-es-btn" onclick="vvFontSize(-1)" title="Decrease font size">A−</button>
            <span id="vv-es-fontsize">12px</span>
            <button class="vv-es-btn" onclick="vvFontSize(1)"  title="Increase font size">A+</button>
            <button class="vv-es-btn" id="vv-es-wrap-btn" onclick="vvToggleWordWrap()" title="Word wrap OFF — click to enable">Wrap</button>
            <span class="vv-es-lang" id="vv-es-lang"></span>
          </div>
        </div>

        <!-- Arrange workspace (shown when arrange mode is active) -->
        <div id="vv-arrange-workspace" style="display:none; overflow-y:auto; padding:8px 12px;">
          <div class="vv-arrange-ws-hdr">
            <span>Unassigned Scripts</span>
            <span id="vv-pending-badge" style="display:none"></span>
          </div>
          <div id="vv-arrange-pending" style="display:none; margin-bottom:10px;">
            <div class="vv-arrange-pending-hdr">Pending Changes</div>
            <div id="vv-pending-list"></div>
          </div>
          <div id="vv-library-drop-zone" class="vv-library-zone"
               ondragover="event.preventDefault(); this.classList.add('vv-drop-target')"
               ondragleave="if(!this.contains(event.relatedTarget)) this.classList.remove('vv-drop-target')"
               ondrop="vvDropToLibrary(event, this)">
            <div class="vv-arrange-drop-hint">Drop here to remove from any orchestrator</div>
            <div id="vv-library-cards">
              <?php foreach ($_library as $_lib): ?>
                <div class="vv-lib-card"
                     data-script="<?= htmlspecialchars($_lib['id']) ?>"
                     draggable="true"
                     ondragstart="vvLibDragStart(event, this)"
                     ondragend="vvDragEnd(event, this)">
                  <span class="vv-lib-card-name"><?= htmlspecialchars($_lib['label']) ?></span>
                  <span class="vv-lib-card-path"><?= htmlspecialchars(dirname($_lib['id'])) ?></span>
                </div>
              <?php endforeach; ?>
              <?php if (empty($_library)): ?>
                <div class="vv-board-placeholder">All scripts are assigned.</div>
              <?php endif; ?>
            </div>
          </div>
        </div>
      </div>
      <div class="vv-sched-footer vv-sched-info vv-snap-footer" id="vv-sched-info-footer">
        <span class="vv-snap-item">
          <span class="vv-snap-label">CPU</span>
          <span class="vv-snap-bar"><span id="vv-snap-cpu-bar"></span></span>
          <span id="vv-snap-cpu-val" class="vv-snap-val">—</span>
        </span>
        <span class="vv-snap-item">
          <span class="vv-snap-label">RAM</span>
          <span class="vv-snap-bar"><span id="vv-snap-ram-bar"></span></span>
          <span id="vv-snap-ram-val" class="vv-snap-val">—</span>
        </span>
        <span class="vv-snap-div">·</span>
        <span id="vv-snap-fallback" class="vv-snap-state">—</span>
        <span class="vv-snap-div">·</span>
        <span id="vv-snap-partner" class="vv-snap-partner">—</span>
        <span class="vv-snap-div">·</span>
        <span id="vv-snap-streams" class="vv-snap-media">—</span>
        <span class="vv-snap-div">·</span>
        <span id="vv-snap-transcodes" class="vv-snap-media">—</span>
      </div>
    </div>

  </div><!-- /#vv-sched-layout -->
</div>

<script>
let vvActiveId      = null;
let vvRunningSet    = new Set();
let vvPollTimer     = null;
let vvStatusTimer   = null;
let vvEditorId      = null;
let vvConfId        = null;
let vvSelectedRow   = null;
let vvCurrentSiId   = null;
let vvCurrentSiHdr  = null;
// Editor state
let vvWordMatch    = null;  // word under cursor — highlighted everywhere in overlay
let vvFindTerm     = '';    // active find-bar search string
let vvFindMatches  = [];    // [{start,end}] in raw text
let vvFindIdx      = 0;     // current match index
// Undo / redo
let vvUndoStack   = [];     // [{v,ss,se}] snapshots — v=value, ss=selStart, se=selEnd
let vvRedoStack   = [];
let vvUndoTimer   = null;
const VV_UNDO_MAX = 200;    // max history depth
// Auto-close pairs
const VV_PAIRS = {'(':')', '[':']', '{':'}', '"':'"', "'":"'", '`':'`'};
const VV_CLOSE = new Set([')', ']', '}', '"', "'", '`']);
// Editor display prefs
let vvFontSizePx = parseInt(localStorage.getItem('vv-editor-fontsize') || '12') || 12;
let vvWordWrap   = localStorage.getItem('vv-editor-wordwrap') === '1';
// Line height helper — used everywhere instead of hardcoded 18
function vvLH() {
  const ta = document.getElementById('vv-editor-body');
  if (ta) { const lh = parseFloat(window.getComputedStyle(ta).lineHeight); if (lh > 0) return lh; }
  return vvFontSizePx * 1.5;
}

// Setup mode — injected by PHP when navigating from the first-run wizard
let vvSetupConf      = <?= json_encode($vv_setup_conf) ?>;
const vvLocalHostConf = <?= json_encode($_vv_local_host_conf) ?>;

// Where Custom Scripts (Create + Import) actually live — shown in the Import Script dialog
window.__vvCustomScriptsDir = <?= json_encode(CUSTOM_SCRIPTS_DIR) ?>;

if (vvSetupConf) {
  // Auto-open the setup conf file once the page is ready
  document.addEventListener('DOMContentLoaded', () => {
    setTimeout(() => vvLoadRawConf(vvSetupConf), 150);
  });
}

// Deep-link from Monitor: open a specific script's panel once the tree is rendered.
const vvOpenScriptId = <?= json_encode($vv_open_script) ?>;
if (vvOpenScriptId) {
  document.addEventListener('DOMContentLoaded', () => {
    setTimeout(() => {
      if (document.querySelector('[data-id="' + CSS.escape(vvOpenScriptId) + '"]')) {
        vvOpenRight(vvOpenScriptId);
        const row = document.querySelector('[data-id="' + CSS.escape(vvOpenScriptId) + '"]');
        if (row) row.scrollIntoView({behavior: 'smooth', block: 'center'});
      }
    }, 200);
  });
}

function vvEscHtml(s) {
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}

function vvPost(url, data) {
  const params = new URLSearchParams({csrf_token, ...data});
  return fetch(url, {
    method: 'POST',
    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    body: params
  }).then(r => r.json());
}

function vvFitRight() {
  const right = document.getElementById('vv-sched-right');
  if (!right.classList.contains('vv-panel-visible')) return;
  const left  = document.getElementById('vv-sched-left');

  const lf      = left.querySelector('.vv-sched-footer');
  const rf      = right.querySelector('.vv-sched-info');
  const toolbar = right.querySelector('.vv-log-toolbar');
  const pre     = document.getElementById('vv-log-pre');
  const sug     = document.getElementById('vv-suggestions');

  const leftRect  = left.getBoundingClientRect();
  const rightRect = right.getBoundingClientRect();

  // Stacked layout (narrow viewport) — right panel is below left; don't force height
  if (rightRect.top > leftRect.bottom + 4) {
    right.style.height = '';
    return;
  }

  rf.style.height    = lf.offsetHeight + 'px';
  right.style.height = left.offsetHeight + 'px';

  const contentH = Math.max(80, lf.getBoundingClientRect().top - 32 - toolbar.getBoundingClientRect().bottom);
  if (pre.style.display !== 'none') {
    pre.style.maxHeight = 'none';
    pre.style.height    = contentH + 'px';
  }
  if (sug.style.display !== 'none') {
    sug.style.height = contentH + 'px';
  }
  const ed = document.getElementById('vv-editor');
  if (ed.style.display !== 'none') {
    const _edH = Math.max(60, contentH - 38) + 'px';
    document.getElementById('vv-editor-body').style.height = _edH;
    document.getElementById('vv-ln-gutter').style.height   = _edH;
  }
  const cf = document.getElementById('vv-confform');
  if (cf.style.display !== 'none') {
    cf.style.height = contentH + 'px';
  }
  const si = document.getElementById('vv-si-view');
  if (si && si.style.display !== 'none') {
    si.style.height = contentH + 'px';
  }
}
window.addEventListener('resize', vvFitRight);

// Whenever the left panel changes height (expand/collapse), sync the right panel immediately.
new ResizeObserver(vvFitRight).observe(document.getElementById('vv-sched-left'));

function vvShowLogMode(id) {
  document.getElementById('vv-suggestions').style.display       = 'none';
  document.getElementById('vv-editor').style.display            = 'none';
  document.getElementById('vv-si-view').style.display           = 'none';
  document.getElementById('vv-arrange-workspace').style.display = 'none';
  document.getElementById('vv-log-pre').style.display           = '';
  document.getElementById('vv-back-btn').style.display          = '';
  document.getElementById('vv-log-search').style.display        = '';
  document.getElementById('vv-clear-btn').style.display         = '';
  document.getElementById('vv-cancel-edit-btn').style.display   = 'none';
  document.getElementById('vv-save-script-btn').style.display   = 'none';
  document.getElementById('vv-save-conf-btn').style.display     = 'none';
  document.getElementById('vv-save-rawconf-btn').style.display  = 'none';
  document.getElementById('vv-advanced-mode-btn').style.display = 'none';
  document.getElementById('vv-delete-script-btn').style.display = 'none';
  document.getElementById('vv-confform').style.display          = 'none';
  document.getElementById('vv-auto-scroll-label').style.display = '';
  document.getElementById('vv-invert-log-label').style.display  = '';
  document.getElementById('vv-stop-btn').style.display          = '';
  document.getElementById('vv-auto-scroll').checked = true;
  const _pre = document.getElementById('vv-log-pre');
  _pre.removeEventListener('scroll', vvOnLogScroll);
  _pre.addEventListener('scroll', vvOnLogScroll);
  const name = id.replace(/\.sh$/, '').split('/').pop();
  document.getElementById('vv-log-title').textContent = name;
  document.getElementById('vv-restore-btn').style.display = 'none';
}

function vvRestoreLastLog() {
  const last = localStorage.getItem('vv-last-job');
  if (last && document.querySelector('[data-id="' + CSS.escape(last) + '"]')) vvOpenRight(last);
}

function vvOnLogScroll() {
  const pre = document.getElementById('vv-log-pre');
  const atBottom = pre.scrollHeight - pre.scrollTop - pre.clientHeight < 20;
  const cb = document.getElementById('vv-auto-scroll');
  if (cb) cb.checked = atBottom;
}

function vvToggleInvert(cb) {
  localStorage.setItem('vv-invert-log', cb.checked ? '1' : '0');
  vvFetchRight();
}

function vvRestoreInvert() {
  const val = localStorage.getItem('vv-invert-log');
  if (val !== null) document.getElementById('vv-invert-log').checked = val === '1';
}

function vvBackToSuggestions() {
  vvEditorReset();
  document.getElementById('vv-editor-status').classList.remove('vv-ed-active');
  document.getElementById('vv-undo-btn').style.display = 'none';
  document.getElementById('vv-redo-btn').style.display = 'none';
  if (vvActiveId) {
    const old = document.querySelector('[data-id="' + CSS.escape(vvActiveId) + '"]');
    if (old) old.querySelector('.vv-job-row').classList.remove('vv-row-selected');
  }
  vvActiveId = null;
  if (vvPollTimer) { clearInterval(vvPollTimer); vvPollTimer = null; }

  document.getElementById('vv-log-pre').style.display           = 'none';
  document.getElementById('vv-editor').style.display            = 'none';
  document.getElementById('vv-si-view').style.display           = 'none';
  document.getElementById('vv-arrange-workspace').style.display = 'none';
  document.getElementById('vv-suggestions').style.display       = '';
  document.getElementById('vv-back-btn').style.display          = 'none';
  // Clear script info selection
  if (vvSelectedRow) { vvSelectedRow.classList.remove('vv-sb-selected'); vvSelectedRow = null; }
  vvCurrentSiId = null;
  document.getElementById('vv-log-search').style.display        = 'none';
  document.getElementById('vv-log-search').value                = '';
  document.getElementById('vv-clear-btn').style.display         = 'none';
  document.getElementById('vv-cancel-edit-btn').style.display   = 'none';
  document.getElementById('vv-save-script-btn').style.display   = 'none';
  document.getElementById('vv-save-conf-btn').style.display     = 'none';
  document.getElementById('vv-save-rawconf-btn').style.display  = 'none';
  const _advBtn = document.getElementById('vv-advanced-mode-btn');
  _advBtn.style.display = '';
  _advBtn.classList.toggle('vv-adv-mode-on', vvAdvancedMode);
  document.getElementById('vv-delete-script-btn').style.display = 'none';
  document.getElementById('vv-confform').style.display          = 'none';
  vvConfId = null;
  const lastJob = localStorage.getItem('vv-last-job');
  if (lastJob) {
    const lname = lastJob.replace(/\.sh$/, '').split('/').pop();
    document.getElementById('vv-restore-label').textContent = lname;
    document.getElementById('vv-restore-btn').style.display = '';
  }
  document.getElementById('vv-auto-scroll-label').style.display = 'none';
  document.getElementById('vv-invert-log-label').style.display  = 'none';
  document.getElementById('vv-stop-btn').style.display          = 'none';
  document.getElementById('vv-auto-scroll').checked  = true;
  const _preBack = document.getElementById('vv-log-pre');
  _preBack.removeEventListener('scroll', vvOnLogScroll);
  _preBack.style.overflowY = '';
  document.getElementById('vv-log-title').textContent = 'Scheduler Information';
  document.getElementById('vv-log-ts').textContent    = '';
  document.getElementById('vv-log-dot').style.display = 'none';
  requestAnimationFrame(vvFitRight);
}

function vvOpenRight(id) {
  if (vvActiveId) {
    const old = document.querySelector('[data-id="' + CSS.escape(vvActiveId) + '"]');
    if (old) old.querySelector('.vv-job-row').classList.remove('vv-row-selected');
  }
  vvActiveId = id;
  localStorage.setItem('vv-last-job', id);
  const job = document.querySelector('[data-id="' + CSS.escape(id) + '"]');
  if (job) job.querySelector('.vv-job-row').classList.add('vv-row-selected');

  vvShowLogMode(id);
  vvSetStopBtn(vvRunningSet.has(id));
  requestAnimationFrame(vvFitRight);

  vvFetchRight();
  if (!vvPollTimer) vvPollTimer = setInterval(vvFetchRight, 2000);
}

function vvFetchRight() {
  if (!vvActiveId) return;
  fetch('/plugins/varaverk/api/log.php?id=' + encodeURIComponent(vvActiveId))
    .then(r => r.json())
    .then(d => {
      const pre        = document.getElementById('vv-log-pre');
      const ts         = document.getElementById('vv-log-ts');
      const autoScroll  = document.getElementById('vv-auto-scroll').checked;
      const invert      = document.getElementById('vv-invert-log').checked;
      const savedScroll = pre.scrollTop;
      if (!d.ok) { pre.textContent = '✗ ' + (d.error ?? 'Error'); return; }
      const content = d.content || '';
      vvSetLogBtnState(vvActiveId, content.trim().length > 0);
      const display = content.trim() ? (invert ? content.split('\n').reverse().join('\n') : content) : '(no log yet)';
      pre._raw = display;
      const term = document.getElementById('vv-log-search')?.value.trim() ?? '';
      if (term) vvFilterLog(term); else pre.textContent = display;
      if (autoScroll) {
        pre.scrollTop = invert ? 0 : pre.scrollHeight;
      } else {
        requestAnimationFrame(() => { pre.scrollTop = savedScroll; });
      }
      const _d = d.ts ? new Date(d.ts * 1000) : null;
      ts.innerHTML = _d ? '<span>Last run:</span><span>' + _d.toLocaleDateString([],{month:'numeric',day:'numeric',year:'numeric'}) + '</span><span>' + _d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'}) + '</span>' : '';
    })
    .catch(() => {});
}

function vvSetStopBtn(running) {
  const btn = document.getElementById('vv-stop-btn');
  if (!btn || btn.style.display === 'none') return;
  btn.disabled         = !running;
  btn.style.background = running ? '#c62828' : '#444';
  btn.style.color      = running ? '#fff'    : '#888';
  btn.style.cursor     = running ? 'pointer' : 'default';
}

function vvSetDot(id) {
  const job = document.querySelector('[data-id="' + CSS.escape(id) + '"]');
  if (job) job.querySelector('.vv-job-dot').classList.add('vv-dot-running');
  if (id === vvActiveId) {
    document.getElementById('vv-log-dot').style.display = 'inline-block';
    vvSetStopBtn(true);
  }
}

function vvClearDot(id) {
  const job = document.querySelector('[data-id="' + CSS.escape(id) + '"]');
  if (job) job.querySelector('.vv-job-dot').classList.remove('vv-dot-running');
  if (id === vvActiveId) {
    document.getElementById('vv-log-dot').style.display = 'none';
    vvSetStopBtn(false);
  }
}

function vvStopJob() {
  if (!vvActiveId || !vvRunningSet.has(vvActiveId)) return;
  const btn = document.getElementById('vv-stop-btn');
  btn.disabled   = true;
  btn.textContent = '…';
  vvPost('/plugins/varaverk/api/stop.php', {id: vvActiveId})
    .then(d => {
      btn.textContent = '■ Stop';
      if (d.ok) {
        vvRunningSet.delete(vvActiveId);
        vvClearDot(vvActiveId);
        vvFetchRight();
      } else {
        btn.disabled = false;
        vvSetStopBtn(true);
      }
    })
    .catch(() => { btn.textContent = '■ Stop'; btn.disabled = false; vvSetStopBtn(true); });
}

function vvPollStatus() {
  fetch('/plugins/varaverk/api/status.php')
    .then(r => r.json())
    .then(statuses => {
      const nowRunning = new Set(
        Object.entries(statuses).filter(([, s]) => s === 'running').map(([id]) => id)
      );
      for (const id of vvRunningSet) {
        if (!nowRunning.has(id)) { vvClearDot(id); if (id === vvActiveId) vvFetchRight(); }
      }
      for (const id of nowRunning) {
        if (!vvRunningSet.has(id)) { vvSetDot(id); if (id === vvActiveId) vvFetchRight(); }
      }
      vvRunningSet = nowRunning;
    })
    .catch(() => {});
}

function vvStartStatusPoll() {
  if (vvStatusTimer) return;
  vvPollStatus();
  vvStatusTimer = setInterval(vvPollStatus, 3000);
}

function vvSelectLog(btn) {
  const job = btn.closest('[data-id]');
  vvOpenRight(job.dataset.id);
}

// Run a script by ID directly — no DOM card needed.
function vvRunById(id) {
  vvOpenRight(id);
  vvSetDot(id);
  vvRunningSet.add(id);
  vvPost('/plugins/varaverk/api/run.php', {id, manual: '1'})
    .then(d => {
      if (!d.ok) {
        document.getElementById('vv-log-pre').textContent = '✗ ' + (d.error ?? 'Failed to start');
        vvClearDot(id);
        vvRunningSet.delete(id);
      }
    });
}

function vvGitPull(btn) {
  const id = 'git_pull_execute.sh';
  btn.disabled = true;
  btn.textContent = '⟳ Pulling…';
  vvRunById(id);
  // Re-enable once the log poll confirms it's running or done
  setTimeout(() => { btn.disabled = false; btn.textContent = '↻ Git Pull'; }, 4000);
}

function vvRunJob(btn) {
  const job        = btn.closest('[data-id]');
  const id         = job.dataset.id;
  const location   = job.querySelector('.vv-rsync-location')?.value.trim() || '';
  const extra_args = job.querySelector('.vv-script-args')?.value.trim() || '';
  vvOpenRight(id);
  vvSetDot(id);
  vvRunningSet.add(id);
  const data = {id};
  if (location)   data.location   = location;
  if (extra_args) data.extra_args = extra_args;
  vvPost('/plugins/varaverk/api/run.php', data)
    .then(d => {
      if (!d.ok) {
        document.getElementById('vv-log-pre').textContent = '✗ ' + (d.error ?? 'Failed to start');
        vvClearDot(id);
        vvRunningSet.delete(id);
      }
    });
}

function vvDryRun(btn) {
  const job        = btn.closest('[data-id]');
  const id         = job.dataset.id;
  const location   = job.querySelector('.vv-rsync-location')?.value.trim() || '';
  const extra_args = job.querySelector('.vv-script-args')?.value.trim() || '';
  vvOpenRight(id);
  vvSetDot(id);
  vvRunningSet.add(id);
  const data = {id};
  if (location)   data.location   = location;
  if (extra_args) data.extra_args = extra_args;
  vvPost('/plugins/varaverk/api/dryrun.php', data)
    .then(d => {
      if (!d.ok) {
        document.getElementById('vv-log-pre').textContent = '✗ ' + (d.error ?? 'Failed to start');
        vvClearDot(id);
        vvRunningSet.delete(id);
      }
    });
}

function vvSaveRsyncStandalone(btn) {
  const job      = btn.closest('[data-id]');
  const locInput = job.querySelector('.vv-rsync-location');
  const cronEl   = job.querySelector('.vv-cron.vv-rsync-standalone');
  const flagName = locInput?.dataset.flag  || '';
  const orchId   = locInput?.dataset.orchId || '';
  if (!flagName) return;
  const location = locInput?.value.trim() || '';
  const cron     = cronEl?.value.trim()   || '';
  btn.disabled = true;
  btn.textContent = 'Saving…';
  vvPost('/plugins/varaverk/api/rsync_standalone.php', {flag_name: flagName, orch_id: orchId, location, cron})
    .then(d => {
      btn.disabled = false;
      if (d.ok) {
        btn.textContent = 'Save';
        vvFlashSaved(job);
      } else {
        btn.textContent = 'Error!';
        setTimeout(() => btn.textContent = 'Save', 2000);
      }
    })
    .catch(() => { btn.disabled = false; btn.textContent = 'Error!'; setTimeout(() => btn.textContent = 'Save', 2000); });
}

function vvFlashSaved(job) {
  const check = job.querySelector('.vv-save-check');
  if (!check) return;
  check.textContent = '✓';
  check.classList.remove('vv-check-show');
  void check.offsetWidth; // force reflow to restart animation
  check.classList.add('vv-check-show');
}

function vvFlashStatus(el, msg, ok) {
  el.textContent = msg;
  el.style.color = ok ? '#4caf50' : '#f44336';
  clearTimeout(el._t);
  el._t = setTimeout(() => { el.textContent = ''; }, 3000);
}

// Cron blur-to-save — fires when focus leaves any cron input.
// Skips rsync standalone (conf_flag) cron fields — those are saved together with location via their own Save button.
function vvSaveCronBlur(input) {
  const job = input.closest('[data-id]');
  if (!job || job.dataset.type === 'conf_flag') return;
  vvSaveJob(input);
}

// Generic save — used for log_enabled toggles and custom scripts.
function vvSaveJob(el) {
  const job         = el.closest('[data-id]');
  const id          = job.dataset.id;
  const enabled     = job.querySelector('.vv-enabled').checked ? '1' : '0';
  const cron        = job.querySelector('.vv-cron')?.value.trim() ?? '';
  const log_enabled = job.querySelector('.vv-log-enabled')?.checked ? '1' : '0';
  vvPost('/plugins/varaverk/api/scheduler.php', {id, enabled, cron, log_enabled})
    .then(d => { if (d.ok) vvFlashSaved(job); });
}

// Orch toggle: saves orch state and propagates to children.
function vvSaveOrch(el) {
  const card    = el.closest('[data-id]');
  const id      = card.dataset.id;
  const enabled = el.checked;
  const cron    = card.querySelector('.vv-orch-row .vv-cron')?.value.trim() ?? '';
  const log_e   = card.querySelector('.vv-log-enabled')?.checked ? '1' : '0';
  vvPost('/plugins/varaverk/api/scheduler.php', {id, enabled: enabled ? '1' : '0', cron, log_enabled: log_e})
    .then(d => { if (d.ok) vvFlashSaved(card); });
  vvApplyOrchState(card, enabled);
}

// Apply visual + interactive state to children based on whether orch is on or off.
function vvApplyOrchState(orchCard, orchEnabled) {
  orchCard.querySelectorAll('.vv-script').forEach(child => {
    const toggle    = child.querySelector('.vv-enabled');
    const cronInput = child.querySelector('input.vv-cron');

    // conf_flag children (e.g. Rsync): toggle reflects real flag value; standalone controls visible only when orch is off
    if (child.dataset.type === 'conf_flag') {
      toggle.checked  = child.dataset.flagValue === '1';
      toggle.disabled = !orchEnabled;
      // Show standalone controls (Run/Dry Run/Log/location/cron/save) only when orch is off
      child.querySelectorAll('.vv-rsync-standalone').forEach(el => {
        el.style.display = orchEnabled ? 'none' : '';
      });
      return;
    }

    const confManaged = child.dataset.confManaged === '1';
    const confEnabled = child.dataset.confEnabled === '1';

    const argsInput = child.querySelector('.vv-script-args');
    if (orchEnabled) {
      // Orch is god: set toggle from master.conf state; dim independent cron.
      // If not conf-managed (no *_SCRIPTS array, e.g. transcode), orch hardcodes the call —
      // show as enabled so the user sees the script is active.
      toggle.checked  = confManaged ? confEnabled : true;
      toggle.disabled = false;
      if (cronInput)  { cronInput.disabled = true; cronInput.style.opacity = '0.35'; }
      if (argsInput)  { argsInput.style.display = 'none'; }
    } else {
      // Orch off: all children switch off; cron and args become active
      toggle.checked  = false;
      toggle.disabled = false;
      if (cronInput)  { cronInput.disabled = false; cronInput.style.opacity = ''; }
      if (argsInput)  { argsInput.style.display = ''; }
      // Persist the off state to schedule.json so cron rebuild reflects it
      const cid   = child.dataset.id;
      const ccron = cronInput?.value.trim() ?? '';
      const clog  = child.querySelector('.vv-log-enabled')?.checked ? '1' : '0';
      vvPost('/plugins/varaverk/api/scheduler.php', {id: cid, enabled: '0', cron: ccron, log_enabled: clog});
    }
  });
}

// Child toggle: conf_toggle when orch is on; scheduler save when orch is off.
// conf_flag children always write directly to master.conf regardless of orch state.
function vvSaveChild(el) {
  const child    = el.closest('[data-id]');
  const orchCard = child.closest('.vv-sched-card');
  const orchOn   = orchCard?.querySelector('.vv-orch-row .vv-enabled')?.checked ?? false;
  const id       = child.dataset.id;
  const enabled  = el.checked;

  if (child.dataset.type === 'conf_flag') {
    const name  = child.dataset.flagName;
    child.dataset.flagValue = enabled ? '1' : '0';
    const badge = child.querySelector('.vv-flag-badge');
    if (badge) badge.style.display = enabled ? '' : 'none';
    vvPost('/plugins/varaverk/api/flag_toggle.php', {name, enabled: enabled ? '1' : '0'})
      .then(d => { if (d.ok) vvFlashSaved(child); });
    return;
  }

  if (orchOn) {
    vvPost('/plugins/varaverk/api/conf_toggle.php', {id, enabled: enabled ? '1' : '0'})
      .then(d => { if (d.ok) vvFlashSaved(child); });
  } else {
    const cron    = child.querySelector('input.vv-cron')?.value.trim() ?? '';
    const log_e   = child.querySelector('.vv-log-enabled')?.checked ? '1' : '0';
    vvPost('/plugins/varaverk/api/scheduler.php', {id, enabled: enabled ? '1' : '0', cron, log_enabled: log_e})
      .then(d => { if (d.ok) vvFlashSaved(child); });
  }
}


function vvToggleAdvanced(btn) {
  const card     = btn.closest('.vv-sched-card');
  const children = card.querySelector('.vv-children');
  const visible  = children.style.display !== 'none';
  children.style.display = visible ? 'none' : 'block';
  btn.textContent = visible ? 'Advanced ▸' : 'Advanced ▾';
}

function vvToggleTools(btn) {
  const card     = btn.closest('.vv-tools-card');
  const children = card.querySelector('.vv-children');
  const visible  = children.style.display !== 'none';
  children.style.display = visible ? 'none' : 'block';
  btn.textContent = visible ? 'Tools ▸' : 'Tools ▾';
}

function vvToggleCustom(btn) {
  const card     = btn.closest('.vv-custom-card');
  const children = card.querySelector('.vv-children');
  const visible  = children.style.display !== 'none';
  children.style.display = visible ? 'none' : 'block';
  btn.textContent = visible ? 'Scripts ▸' : 'Scripts ▾';
}

function vvSetLogBtnState(id, hasLog) {
  const job = document.querySelector('[data-id="' + CSS.escape(id) + '"]');
  if (!job) return;
  job.querySelector('.vv-log-btn').classList.toggle('vv-has-log', hasLog);
}

function vvClearRightLog() {
  if (!vvActiveId) return;
  vvPost('/plugins/varaverk/api/log.php', {id: vvActiveId, clear: '1'})
    .then(() => { vvSetLogBtnState(vvActiveId, false); vvFetchRight(); })
    .catch(() => {});
}

function vvAddScript() {
  vvEditorId = null;
  const nameEl = document.getElementById('vv-editor-name');
  nameEl.value    = '';
  nameEl.readOnly = false;
  document.getElementById('vv-editor-body').value = '#!/bin/bash\n\n';
  vvShowEditorMode('New Script');
  vvSyncHlOverlay();
  vvUndoCaptureInitial();
  vvEditorCursorMoved();
  nameEl.focus();
}

function vvEditScript(id) {
  if (vvActiveId) {
    const old = document.querySelector('[data-id="' + CSS.escape(vvActiveId) + '"]');
    if (old) old.querySelector('.vv-job-row').classList.remove('vv-row-selected');
  }
  vvActiveId = null;
  if (vvPollTimer) { clearInterval(vvPollTimer); vvPollTimer = null; }

  vvEditorId = id;
  const name = id.replace(/^Custom\//, '').replace(/\.sh$/, '');
  const nameEl = document.getElementById('vv-editor-name');
  nameEl.value    = name;
  nameEl.readOnly = true;
  document.getElementById('vv-editor-body').value = 'Loading…';

  fetch('/plugins/varaverk/api/script.php?id=' + encodeURIComponent(id))
    .then(r => r.json())
    .then(d => {
      document.getElementById('vv-editor-body').value = d.ok ? d.content : '# Error loading script';
      vvSyncHlOverlay();
      vvUndoCaptureInitial();
      vvEditorCursorMoved();
    })
    .catch(() => {
      document.getElementById('vv-editor-body').value = '# Error loading script';
      vvSyncHlOverlay();
    });

  vvShowEditorMode(name);
}

function vvShowEditorMode(title) {
  vvEditorReset();
  document.getElementById('vv-editor').classList.add('vv-editor-hl');
  document.getElementById('vv-editor-status').classList.add('vv-ed-active');
  document.getElementById('vv-es-lang').textContent = 'Bash';
  document.getElementById('vv-undo-btn').style.display = '';
  document.getElementById('vv-redo-btn').style.display = '';
  vvRestoreEditorPrefs();
  document.getElementById('vv-suggestions').style.display       = 'none';
  document.getElementById('vv-log-pre').style.display           = 'none';
  document.getElementById('vv-si-view').style.display           = 'none';
  document.getElementById('vv-arrange-workspace').style.display = 'none';
  document.getElementById('vv-editor').style.display            = 'flex';
  document.getElementById('vv-back-btn').style.display          = '';
  document.getElementById('vv-restore-btn').style.display       = 'none';
  document.getElementById('vv-save-script-btn').style.display   = '';
  document.getElementById('vv-save-rawconf-btn').style.display  = 'none';
  document.getElementById('vv-advanced-mode-btn').style.display = 'none';
  document.getElementById('vv-log-search').style.display        = 'none';
  document.getElementById('vv-log-search').value                = '';
  document.getElementById('vv-clear-btn').style.display         = 'none';
  document.getElementById('vv-stop-btn').style.display          = 'none';
  document.getElementById('vv-auto-scroll-label').style.display = 'none';
  document.getElementById('vv-invert-log-label').style.display  = 'none';
  document.getElementById('vv-log-title').textContent           = title;
  document.getElementById('vv-log-ts').textContent              = '';
  document.getElementById('vv-confform').style.display          = 'none';
  document.getElementById('vv-cancel-edit-btn').style.display   = '';
  document.getElementById('vv-save-conf-btn').style.display     = 'none';
  document.getElementById('vv-delete-script-btn').style.display = vvEditorId ? '' : 'none';
  if (vvPollTimer) { clearInterval(vvPollTimer); vvPollTimer = null; }
  requestAnimationFrame(vvFitRight);
}

function vvSaveScript() {
  const name    = document.getElementById('vv-editor-name').value.trim();
  const content = document.getElementById('vv-editor-body').value;
  if (!name || !/^[a-zA-Z0-9_\-]+$/.test(name)) {
    alert('Name must be letters, numbers, _ or - only (no spaces, no .sh)');
    return;
  }
  if (!confirm('Save changes to "' + name + '.sh"?')) return;
  const btn = document.getElementById('vv-save-script-btn');
  btn.disabled    = true;
  btn.textContent = 'Saving…';
  vvPost('/plugins/varaverk/api/script.php', {name, content})
    .then(d => {
      if (!d.ok) { alert('Save failed: ' + (d.error ?? 'Unknown error')); btn.disabled = false; btn.textContent = 'Save Script'; return; }
      localStorage.setItem('vv-last-job', d.id);
      window.location.reload();
    })
    .catch(() => { btn.disabled = false; btn.textContent = 'Save Script'; });
}

function vvDeleteScript() {
  if (!vvEditorId) return;
  const name = vvEditorId.replace(/^Custom\//, '').replace(/\.sh$/, '');
  if (!confirm('Delete "' + name + '.sh"? This cannot be undone.')) return;
  const btn = document.getElementById('vv-delete-script-btn');
  btn.disabled = true;
  btn.textContent = 'Deleting…';
  vvPost('/plugins/varaverk/api/script.php', {action: 'delete', name})
    .then(d => {
      if (!d.ok) { alert('Delete failed: ' + (d.error ?? 'Unknown error')); btn.disabled = false; btn.textContent = '\u{1F5D1} Delete'; return; }
      localStorage.removeItem('vv-last-job');
      window.location.reload();
    })
    .catch(() => { btn.disabled = false; btn.textContent = '\u{1F5D1} Delete'; });
}

// ── Import Script — browse the whole server, move a chosen .sh into Custom Scripts ──

let _vvImpSelected = null; // full path of the currently-selected file, or null

function vvImportScriptOpen() {
  let modal = document.getElementById('vv-import-modal');
  if (!modal) modal = _vvImpBuildModal();
  modal.style.display = 'flex';
  _vvImpSelected = null;
  _vvImpUpdateFooter();
  _vvImpBrowse('/');
}

function vvImportScriptClose() {
  const modal = document.getElementById('vv-import-modal');
  if (modal) modal.style.display = 'none';
  _vvImpSelected = null;
}

function _vvImpBuildModal() {
  const modal = document.createElement('div');
  modal.id = 'vv-import-modal';
  modal.style.cssText = 'display:none;position:fixed;inset:0;z-index:9000;background:rgba(0,0,0,.6);'
    + 'align-items:center;justify-content:center;';
  modal.addEventListener('mousedown', e => { if (e.target === modal) vvImportScriptClose(); });

  const box = document.createElement('div');
  box.style.cssText = 'background:#111;border:1px solid #222;border-radius:6px;width:560px;max-width:92vw;'
    + 'max-height:80vh;display:flex;flex-direction:column;overflow:hidden;';

  box.innerHTML = `
    <div style="padding:10px 14px;border-bottom:1px solid #1e1e1e;display:flex;align-items:center;justify-content:space-between;">
      <span style="font-size:13px;font-weight:bold;color:#ccc;">Import Script</span>
      <button onclick="vvImportScriptClose()" style="background:none;border:none;color:#666;cursor:pointer;font-size:14px;">&#10005;</button>
    </div>
    <div style="padding:10px 14px;border-bottom:1px solid #1e1e1e;">
      <div style="font-size:10px;color:#555;margin-bottom:6px;">
        Moves the selected script into Custom Scripts (<code>${_vvImpEsc(window.__vvCustomScriptsDir || '')}</code>).
        The original is removed once the copy is verified.
      </div>
      <div style="display:flex;gap:6px;align-items:center;">
        <input id="vv-imp-path" type="text" value="/" style="flex:1;min-width:0;background:#0a0a0a;border:1px solid #222;
          color:#aaa;border-radius:3px;padding:4px 8px;font-size:11px;font-family:monospace;"
          onkeydown="if(event.key==='Enter')_vvImpBrowse(document.getElementById('vv-imp-path').value.trim()||'/')">
        <button onclick="_vvImpNavUp()" 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;">&#8593;</button>
        <button onclick="_vvImpBrowse(document.getElementById('vv-imp-path').value.trim()||'/')" 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;">&#8635; Browse</button>
      </div>
      <span id="vv-imp-err" style="font-size:9px;color:#ef5350;display:none;margin-top:4px;"></span>
    </div>
    <div id="vv-imp-list" style="flex:1;overflow-y:auto;background:#080808;min-height:220px;"></div>
    <div style="padding:10px 14px;border-top:1px solid #1e1e1e;display:flex;align-items:center;justify-content:space-between;gap:10px;">
      <span id="vv-imp-selected" style="font-size:10px;color:#555;font-family:monospace;flex:1;min-width:0;
        overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></span>
      <div style="display:flex;gap:6px;flex-shrink:0;">
        <button onclick="vvImportScriptClose()" class="vv-btn-sm">Cancel</button>
        <button id="vv-imp-do-btn" class="vv-btn-sm vv-save-script-btn-style" disabled onclick="_vvImpDoImport()">Import</button>
      </div>
    </div>
  `;

  modal.appendChild(box);
  document.body.appendChild(modal);
  return modal;
}

function _vvImpEsc(s) {
  const d = document.createElement('div');
  d.textContent = s;
  return d.innerHTML;
}

function _vvImpBrowse(path) {
  const listEl = document.getElementById('vv-imp-list');
  const errEl  = document.getElementById('vv-imp-err');
  const pathEl = document.getElementById('vv-imp-path');
  errEl.style.display = 'none';
  listEl.innerHTML = '<div style="padding:10px;color:#444;font-size:11px;">Loading…</div>';

  fetch(`/plugins/varaverk/api/import_script.php?action=browse&path=${encodeURIComponent(path)}&_=${Date.now()}`)
    .then(r => r.json())
    .then(d => {
      if (!d.ok) {
        listEl.innerHTML = '';
        errEl.textContent = d.error || 'Browse failed';
        errEl.style.display = '';
        return;
      }
      pathEl.value = d.path;
      _vvImpRender(d);
    })
    .catch(e => {
      listEl.innerHTML = '';
      errEl.textContent = 'Request failed: ' + e;
      errEl.style.display = '';
    });
}

function _vvImpNavUp() {
  const cur = (document.getElementById('vv-imp-path').value || '/').replace(/\/$/, '') || '/';
  const up  = cur === '/' ? '/' : (cur.substring(0, cur.lastIndexOf('/')) || '/');
  _vvImpBrowse(up);
}

function _vvImpRender(d) {
  const listEl = document.getElementById('vv-imp-list');
  let html = '';

  if (d.parent !== null) {
    const pname = d.parent === '/' ? '/' : (d.parent.replace(/^.*\//, '') || d.parent) + '/';
    html += `<div class="vv-imp-row" onclick="_vvImpBrowse(${JSON.stringify(d.parent)})"
      style="padding:5px 12px;font-size:11px;color:#444;font-style:italic;cursor:pointer;font-family:monospace;">&#8593; ${pname}</div>`;
  }

  for (const dir of d.dirs) {
    const name = dir.replace(/^.*\//, '') || dir;
    html += `<div class="vv-imp-row" onclick="_vvImpBrowse(${JSON.stringify(dir)})" title="${_vvImpEsc(dir)}"
      style="padding:5px 12px;font-size:11px;color:#666;cursor:pointer;font-family:monospace;">&#9654; ${_vvImpEsc(name)}</div>`;
  }

  for (const file of d.files) {
    const name = file.replace(/^.*\//, '') || file;
    const sel  = file === _vvImpSelected;
    html += `<div class="vv-imp-row vv-imp-file${sel ? ' vv-imp-file-sel' : ''}" data-path="${_vvImpEsc(file)}"
      onclick="_vvImpSelectFile(${JSON.stringify(file)})" title="${_vvImpEsc(file)}"
      style="padding:5px 12px;font-size:11px;cursor:pointer;font-family:monospace;
      color:${sel ? '#4caf50' : '#4a9eff'};background:${sel ? '#0f1f0f' : 'transparent'};">&#128196; ${_vvImpEsc(name)}</div>`;
  }

  if (!d.dirs.length && !d.files.length) {
    html += '<div style="padding:10px 12px;color:#333;font-size:11px;">— empty —</div>';
  }

  listEl.innerHTML = html;
}

function _vvImpSelectFile(path) {
  _vvImpSelected = path;
  _vvImpUpdateFooter();
  // Re-render just the highlight without a re-fetch.
  document.querySelectorAll('#vv-imp-list .vv-imp-file').forEach(el => {
    const isSel = el.dataset.path === path;
    el.classList.toggle('vv-imp-file-sel', isSel);
    el.style.color = isSel ? '#4caf50' : '#4a9eff';
    el.style.background = isSel ? '#0f1f0f' : 'transparent';
  });
}

function _vvImpUpdateFooter() {
  const sel = document.getElementById('vv-imp-selected');
  const btn = document.getElementById('vv-imp-do-btn');
  if (!sel || !btn) return;
  sel.textContent = _vvImpSelected || '';
  btn.disabled = !_vvImpSelected;
}

function _vvImpDoImport() {
  if (!_vvImpSelected) return;
  const dest = (window.__vvCustomScriptsDir || 'Custom Scripts') + '/' + _vvImpSelected.replace(/^.*\//, '');
  if (!confirm('Move\n  ' + _vvImpSelected + '\n→ ' + dest + '\n\nThe original will be deleted once the copy is verified. Continue?')) return;

  const btn = document.getElementById('vv-imp-do-btn');
  btn.disabled = true;
  btn.textContent = 'Importing…';

  vvPost('/plugins/varaverk/api/import_script.php', {action: 'import', path: _vvImpSelected})
    .then(d => {
      if (!d.ok) {
        alert('Import failed: ' + (d.error || 'Unknown error'));
        btn.disabled = false;
        btn.textContent = 'Import';
        return;
      }
      if (d.warning) alert(d.warning);
      window.location.reload();
    })
    .catch(e => {
      alert('Import failed: ' + e);
      btn.disabled = false;
      btn.textContent = 'Import';
    });
}

function vvShowConfMode(title) {
  document.getElementById('vv-suggestions').style.display       = 'none';
  document.getElementById('vv-log-pre').style.display           = 'none';
  document.getElementById('vv-si-view').style.display           = 'none';
  document.getElementById('vv-arrange-workspace').style.display = 'none';
  document.getElementById('vv-editor').style.display            = 'none';
  document.getElementById('vv-confform').style.display          = '';
  document.getElementById('vv-back-btn').style.display          = '';
  document.getElementById('vv-restore-btn').style.display       = 'none';
  document.getElementById('vv-cancel-edit-btn').style.display   = '';
  document.getElementById('vv-save-script-btn').style.display   = 'none';
  document.getElementById('vv-save-conf-btn').style.display     = '';
  document.getElementById('vv-save-rawconf-btn').style.display  = 'none';
  document.getElementById('vv-advanced-mode-btn').style.display = 'none';
  document.getElementById('vv-delete-script-btn').style.display = 'none';
  document.getElementById('vv-log-search').style.display        = 'none';
  document.getElementById('vv-log-search').value                = '';
  document.getElementById('vv-clear-btn').style.display         = 'none';
  document.getElementById('vv-stop-btn').style.display          = 'none';
  document.getElementById('vv-auto-scroll-label').style.display = 'none';
  document.getElementById('vv-invert-log-label').style.display  = 'none';
  document.getElementById('vv-log-title').textContent           = title;
  document.getElementById('vv-log-ts').textContent              = '';
  if (vvPollTimer) { clearInterval(vvPollTimer); vvPollTimer = null; }
  requestAnimationFrame(vvFitRight);
}

function vvEditConf(id) {
  if (vvActiveId) {
    const old = document.querySelector('[data-id="' + CSS.escape(vvActiveId) + '"]');
    if (old) old.querySelector('.vv-job-row').classList.remove('vv-row-selected');
  }
  vvActiveId = null;
  if (vvPollTimer) { clearInterval(vvPollTimer); vvPollTimer = null; }
  vvConfId = id;
  const name = id.replace(/\.sh$/, '').split('/').pop();
  const cf = document.getElementById('vv-confform');
  cf.innerHTML = '<p class="vv-cf-empty">Loading…</p>';
  vvShowConfMode(name + ' — Config');
  fetch('/plugins/varaverk/api/confform.php?id=' + encodeURIComponent(id))
    .then(r => r.json())
    .then(d => {
      cf.innerHTML = (!d.ok || !d.groups || d.groups.length === 0)
        ? '<p class="vv-cf-empty">No configurable settings found for this host.</p>'
        : vvRenderConfForm(d.groups);
      requestAnimationFrame(vvFitRight);
    })
    .catch(() => { cf.innerHTML = '<p class="vv-cf-empty">Failed to load configuration.</p>'; });
}

function vvRenderConfForm(groups) {
  const esc = s => String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
  let html = '';
  for (const g of groups) {
    html += '<div class="vv-cf-group">';
    html += '<div class="vv-cf-group-header">' + esc(g.subsection)
          + ' <span class="vv-cf-file">' + esc(g.file) + '</span></div>';
    for (const f of g.fields) {
      html += '<div class="vv-cf-field">';
      html += '<div class="vv-cf-key">' + esc(f.key) + '</div>';
      if (f.desc) html += '<div class="vv-cf-desc">' + esc(f.desc) + '</div>';
      if (f.type === 'scalar') {
        html += '<input class="vv-cf-input vv-cf-scalar" type="text"'
              + ' data-key="' + esc(f.key) + '" data-file="' + esc(f.file) + '" data-type="scalar"'
              + ' value="' + esc(f.value) + '">';
      } else {
        const rows = Math.min(20, (f.value.match(/\n/g) || []).length + 3);
        html += '<textarea class="vv-cf-input vv-cf-array"'
              + ' data-key="' + esc(f.key) + '" data-file="' + esc(f.file) + '" data-type="' + esc(f.type) + '"'
              + ' rows="' + rows + '">' + esc(f.value) + '</textarea>';
      }
      html += '</div>';
    }
    html += '</div>';
  }
  return html;
}

function vvSaveConf() {
  if (!vvConfId) return;
  const _cfName = vvConfId.replace(/\.sh$/, '').split('/').pop();
  if (!confirm('Save configuration changes for "' + _cfName + '"?')) return;
  const inputs = document.querySelectorAll('#vv-confform .vv-cf-input, #vv-si-view .vv-cf-input');
  const changes = [];
  inputs.forEach(el => changes.push({key: el.dataset.key, file: el.dataset.file, type: el.dataset.type, value: el.value}));
  const btn = document.getElementById('vv-save-conf-btn');
  btn.disabled = true; btn.textContent = 'Saving…';
  vvPost('/plugins/varaverk/api/confform.php', {id: vvConfId, changes: JSON.stringify(changes)})
    .then(d => {
      btn.disabled = false;
      if (d.ok) {
        btn.textContent = '✓ Saved';
        setTimeout(() => { btn.textContent = 'Save Config'; }, 2500);
      } else {
        btn.textContent = 'Save Config';
        alert('Save failed: ' + (d.error ?? 'Unknown error'));
      }
    })
    .catch(() => { btn.disabled = false; btn.textContent = 'Save Config'; });
}

// vvToggleSugBlock: used by script browser blocks (header element passed as arg)
function vvToggleSugBlock(header) {
  const body    = header.nextElementSibling;
  const chevron = header.querySelector('.vv-sug-chevron');
  const open    = body.style.display !== 'none';
  body.style.display  = open ? 'none' : '';
  chevron.textContent = open ? '▸' : '▾';
  // When opening a script browser block in advanced mode, load full content
  if (!open && vvAdvancedMode) vvLoadScriptFull(body);
}

function vvToggleSug(header) {
  const body    = header.nextElementSibling;
  const chevron = header.querySelector('.vv-sug-chevron');
  const open    = body.style.display !== 'none';
  body.style.display  = open ? 'none' : '';
  chevron.textContent = open ? '▸' : '▾';
  const block = header.closest('[data-save-key]');
  if (block) localStorage.setItem('vv-sug-' + block.dataset.saveKey, open ? '0' : '1');
}

function vvRestoreSugStates() {
  document.querySelectorAll('[data-save-key]').forEach(block => {
    const saved = localStorage.getItem('vv-sug-' + block.dataset.saveKey);
    if (saved === null) return;
    const body    = block.querySelector('.vv-sug-body');
    const chevron = block.querySelector('.vv-sug-chevron');
    if (!body) return;
    const open = saved === '1';
    body.style.display  = open ? '' : 'none';
    if (chevron) chevron.textContent = open ? '▾' : '▸';
  });
}

// Suggested cron click in script browser tree: apply to the matching card cron input and save
function vvApplySugCron(el, event) {
  event.stopPropagation();
  const cron  = el.textContent.trim();
  const row   = el.closest('[data-id]');
  const id    = row.dataset.id;
  const jobEl = document.querySelector('#vv-sched-left [data-id="' + CSS.escape(id) + '"]');
  if (!jobEl) return;
  const cronInput = jobEl.querySelector('input.vv-cron');
  if (!cronInput || cronInput.disabled) {
    el.style.color = '#f44336';
    setTimeout(() => { el.style.color = ''; }, 1200);
    return;
  }
  cronInput.value = cron;
  el.style.color = '#4caf50';
  setTimeout(() => { el.style.color = ''; }, 1500);
  const enabled     = jobEl.querySelector('.vv-enabled')?.checked ? '1' : '0';
  const log_enabled = jobEl.querySelector('.vv-log-enabled')?.checked ? '1' : '0';
  vvPost('/plugins/varaverk/api/scheduler.php', {id, enabled, cron, log_enabled})
    .then(d => { if (d.ok) vvFlashSaved(jobEl); });
}

// Script name click: navigate to the matching row in the Orch tree and open its info.
// Falls back to cog behaviour for scripts not in the tree (e.g. custom scripts).
function vvClickLabel(el) {
  const job = el.closest('[data-id]');
  const id  = job.dataset.id;
  const treeRow = document.querySelector('#vv-sb-tree [data-id="' + CSS.escape(id) + '"]');
  if (!treeRow) { vvClickCog(el); return; }
  // Expand parent sb-children if collapsed so the row is visible on back-nav
  const parent = treeRow.parentElement;
  if (parent?.classList.contains('vv-sb-children') && parent.style.display === 'none') {
    parent.style.display = '';
    const expand = parent.previousElementSibling?.querySelector('.vv-sb-expand');
    if (expand) expand.textContent = '▾';
  }
  vvSelectScript(treeRow);
}

// Cog click: open settings / enriched info for this script (moved from label click).
function vvClickCog(el) {
  const job = el.closest('[data-id]');
  const id  = job.dataset.id;
  if (id.startsWith('Custom/')) {
    vvAdvancedMode ? vvEditScript(id) : vvEditConf(id);
    return;
  }
  if (vvActiveId) {
    const old = document.querySelector('[data-id="' + CSS.escape(vvActiveId) + '"]');
    if (old) old.querySelector('.vv-job-row').classList.remove('vv-row-selected');
  }
  vvActiveId = null;
  if (vvPollTimer) { clearInterval(vvPollTimer); vvPollTimer = null; }
  if (vvSelectedRow) { vvSelectedRow.classList.remove('vv-sb-selected'); vvSelectedRow = null; }
  vvCurrentSiId  = id;
  vvCurrentSiHdr = '';
  vvShowScriptInfoMode(id.replace(/\.sh$/, '').split('/').pop(), '', id);
}

// ── Advanced mode toggle ──────────────────────────────────────────────────────

let vvAdvancedMode = false;
let vvRawConfFile  = null;

function vvToggleAdvancedMode() {
  vvAdvancedMode = !vvAdvancedMode;
  localStorage.setItem('vv-advanced-mode', vvAdvancedMode ? '1' : '0');
  const btn = document.getElementById('vv-advanced-mode-btn');
  btn.classList.toggle('vv-adv-mode-on', vvAdvancedMode);
  document.querySelectorAll('.vv-adv-only').forEach(el => el.style.display = vvAdvancedMode ? '' : 'none');
  // If si-view is open, refresh it with the new mode
  if (vvCurrentSiId && document.getElementById('vv-si-view').style.display !== 'none') {
    const name = vvCurrentSiId.replace(/\.sh$/, '').split('/').pop();
    vvShowScriptInfoMode(name, vvCurrentSiHdr, vvCurrentSiId);
  }
}

// Load full script content into .vv-sb-full elements that still show "(loading…)"
function vvLoadScriptFull(body) {
  body.querySelectorAll('.vv-sb-full').forEach(el => {
    if (el.dataset.loaded) return;
    el.dataset.loaded = '1';
    const id = el.dataset.srcId;
    fetch('/plugins/varaverk/api/readscript.php?id=' + encodeURIComponent(id))
      .then(r => r.json())
      .then(d => { el.innerHTML = d.ok ? vvHl(d.content, false) : '<span class="vv-hl-comment"># Error loading script</span>'; })
      .catch(() => { el.textContent = '# Load failed'; });
  });
}

// ── Board: Next Runs, Errors, Locks, Partner ─────────────────────────────

let vvBoardTimer = null;

// Minimal cron parser — returns next Date after now, or null if unparseable.
function vvCronNext(expr) {
  if (!expr || expr === 'array_start' || expr === 'array_stop') return null;
  const parts = expr.trim().split(/\s+/);
  if (parts.length !== 5) return null;
  const [mF, hF, dF, moF, wF] = parts;
  const matches = (field, val) => {
    if (field === '*') return true;
    if (/^\*\/\d+$/.test(field)) return val % parseInt(field.slice(2)) === 0;
    if (/^\d+$/.test(field))     return parseInt(field) === val;
    if (/^\d+-\d+$/.test(field)) { const [a,b] = field.split('-').map(Number); return val >= a && val <= b; }
    if (field.includes(','))      return field.split(',').some(f => parseInt(f) === val);
    return false;
  };
  const t = new Date();
  t.setSeconds(0, 0);
  t.setMinutes(t.getMinutes() + 1);
  for (let i = 0; i < 10080; i++, t.setMinutes(t.getMinutes() + 1)) {
    if (matches(moF, t.getMonth()+1) && matches(dF, t.getDate()) &&
        matches(wF,  t.getDay())     && matches(hF, t.getHours()) &&
        matches(mF,  t.getMinutes())) return new Date(t);
  }
  return null;
}

function vvFmtDuration(ms) {
  const s = Math.floor(ms / 1000);
  if (s < 60)   return s + 's';
  if (s < 3600) return Math.floor(s/60) + 'm ' + (s%60) + 's';
  const h = Math.floor(s/3600), m = Math.floor((s%3600)/60);
  return h + 'h' + (m ? ' ' + m + 'm' : '');
}

function vvFmtAge(seconds) {
  if (seconds < 60)   return seconds + 's ago';
  if (seconds < 3600) return Math.floor(seconds/60) + 'm ago';
  if (seconds < 86400) return Math.floor(seconds/3600) + 'h ago';
  return Math.floor(seconds/86400) + 'd ago';
}

function vvBuildNextRuns() {
  const body = document.getElementById('vv-nextruns-body');
  if (!body) return;
  const now = new Date();
  const rows = [];
  document.querySelectorAll('.vv-sched-card').forEach(card => {
    const orchRow  = card.querySelector('.vv-orch-row');
    if (!orchRow) return;
    const enabled  = orchRow.querySelector('.vv-enabled')?.checked;
    if (!enabled) return;
    const cronEl   = orchRow.querySelector('.vv-cron');
    const cron     = cronEl?.value.trim() ?? '';
    if (!cron || cron === 'array_start' || cron === 'array_stop') return;
    const label    = orchRow.querySelector('.vv-job-label')?.textContent.trim() ?? card.dataset.id;
    const next     = vvCronNext(cron);
    if (next) rows.push({ label, cron, next, diffMs: next - now });
  });
  // Tools and custom scripts with their own cron
  document.querySelectorAll('.vv-tools-card .vv-script, .vv-custom-card .vv-script').forEach(script => {
    const enabled = script.querySelector('.vv-enabled')?.checked;
    if (!enabled) return;
    const cronEl  = script.querySelector('input.vv-cron');
    const cron    = cronEl?.value.trim() ?? '';
    if (!cron) return;
    const label   = script.querySelector('.vv-job-label')?.textContent.trim() ?? script.dataset.id;
    const next    = vvCronNext(cron);
    if (next) rows.push({ label, cron, next, diffMs: next - now });
  });
  if (!rows.length) {
    body.innerHTML = '<div class="vv-board-placeholder">No enabled scheduled jobs.</div>';
    return;
  }
  rows.sort((a,b) => a.diffMs - b.diffMs);
  let html = '<div class="vv-nextrun-list">';
  for (const r of rows) {
    const atStr = r.next.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
    html += '<div class="vv-nextrun-row">'
          + '<span class="vv-nr-label">'  + vvEscHtml(r.label) + '</span>'
          + '<span class="vv-nr-cron">'   + vvEscHtml(r.cron)  + '</span>'
          + '<span class="vv-nr-in">in '  + vvFmtDuration(r.diffMs) + '</span>'
          + '<span class="vv-nr-at">'     + atStr + '</span>'
          + '</div>';
  }
  body.innerHTML = html + '</div>';
}

function vvBoardPoll() {
  fetch('/plugins/varaverk/api/board.php')
    .then(r => r.json())
    .then(d => {
      if (!d.ok) return;
      vvUpdateLocks(d.locks   ?? []);
      vvUpdateErrors(d.errors ?? []);
      vvUpdatePartner(d.partner);
    })
    .catch(() => {});
}

function vvUpdateLocks(locks) {
  const body  = document.getElementById('vv-locks-body');
  const badge = document.getElementById('vv-locks-badge');
  if (!body) return;
  badge.style.display = locks.length ? '' : 'none';
  badge.textContent   = locks.length || '';
  if (!locks.length) {
    body.innerHTML = '<div class="vv-board-placeholder">No stale locks.</div>';
    return;
  }
  let html = '<div class="vv-locks-list">';
  for (const lk of locks) {
    html += '<div class="vv-lock-row">'
          + '<span class="vv-lk-name">'  + vvEscHtml(lk.name) + '</span>'
          + '<span class="vv-lk-age">'   + vvFmtAge(lk.age)   + '</span>'
          + '<button class="vv-btn-sm vv-lock-clear" onclick="vvClearLock(\''
          + vvEscHtml(lk.file) + '\',this)">Clear</button>'
          + '</div>';
  }
  body.innerHTML = html + '</div>';
}

// Auto-expand a suggestions block body if it's currently collapsed.
function vvAutoExpand(bodyId) {
  const body    = document.getElementById(bodyId);
  if (!body || body.style.display !== 'none') return;
  body.style.display = '';
  const chevron = body.previousElementSibling?.querySelector('.vv-sug-chevron');
  if (chevron) chevron.textContent = '▾';
}

function vvErrIsAcked(script, ts) {
  return parseInt(localStorage.getItem('vv-ack-' + script) || '0') >= ts;
}

function vvAckError(script, ts, btn) {
  localStorage.setItem('vv-ack-' + script, ts);
  const row  = btn.closest('.vv-err-row');
  const list = row?.closest('.vv-errors-list');
  row?.remove();
  const badge   = document.getElementById('vv-errors-badge');
  const body    = document.getElementById('vv-errors-body');
  const remaining = list?.querySelectorAll('.vv-err-row').length ?? 0;
  if (!remaining && body)
    body.innerHTML = '<div class="vv-board-placeholder">All errors acknowledged.</div>';
  const cur = Math.max(0, parseInt(badge?.textContent || '0') - 1);
  if (badge) { badge.textContent = cur || ''; badge.style.display = cur ? '' : 'none'; }
}

function vvUpdateErrors(errors) {
  const body  = document.getElementById('vv-errors-body');
  const badge = document.getElementById('vv-errors-badge');
  if (!body) return;
  if (!errors.length) {
    badge.style.display = 'none'; badge.textContent = '';
    body.innerHTML = '<div class="vv-board-placeholder">No recent errors.</div>';
    return;
  }
  // Filter out acknowledged entries
  const unacked = errors.filter(e => !vvErrIsAcked(e.script, e.ts));
  badge.style.display = unacked.length ? '' : 'none';
  badge.textContent   = unacked.length || '';
  if (!unacked.length) {
    body.innerHTML = '<div class="vv-board-placeholder">All errors acknowledged.</div>';
    return;
  }
  const now = Math.floor(Date.now() / 1000);
  let html = '<div class="vv-errors-list">';
  for (const e of unacked) {
    const jobId = "'" + (e.script + '.sh').replace(/\\/g,"\\\\").replace(/'/g,"\\'") + "'";
    const label = e.script.split('/').pop().replace(/_/g, ' ');
    html += '<div class="vv-err-row">'
          + '<div class="vv-err-top">'
          + '<span class="vv-err-script" onclick="vvOpenRight(' + jobId + ')" style="cursor:pointer" title="' + vvEscHtml(e.script) + '">' + vvEscHtml(label) + '</span>'
          + '<span class="vv-err-age">' + vvFmtAge(now - e.ts) + '</span>'
          + '<button class="vv-btn-sm vv-ack-btn" onclick="vvAckError(\'' + e.script.replace(/\\/g,"\\\\").replace(/'/g,"\\'") + "'," + e.ts + ',this)">Ack</button>'
          + '</div>'
          + '<div class="vv-err-line">' + vvEscHtml(e.line) + '</div>'
          + '</div>';
  }
  body.innerHTML = html + '</div>';
  vvAutoExpand('vv-errors-body');
}

function vvUpdatePartner(partner) {
  const hdr  = document.getElementById('vv-partner-hdr');
  const body = document.getElementById('vv-partner-body');
  if (!hdr || !body) return;
  if (!partner) {
    hdr.textContent  = '—';
    hdr.style.color  = '#555';
    body.innerHTML   = '<div class="vv-board-placeholder">No partner configured.</div>';
    return;
  }
  const col = partner.reachable ? '#4caf50' : '#f44336';
  hdr.style.color  = col;
  hdr.textContent  = partner.reachable
    ? '● ' + partner.host + (partner.latency ? ' ' + partner.latency + 'ms' : '')
    : '● ' + partner.host;
  body.innerHTML = '<div class="vv-partner-row">'
    + '<span style="color:' + col + ';font-size:16px;line-height:1;">●</span>'
    + '<span class="vv-partner-name">' + vvEscHtml(partner.host) + '</span>'
    + (partner.reachable
        ? '<span class="vv-partner-detail">' + (partner.latency ?? '?') + ' ms</span>'
        : '<span class="vv-partner-detail vv-partner-down">unreachable</span>')
    + '</div>';
}

function vvClearLock(file, btn) {
  if (!confirm('Clear lock "' + file + '"?\nOnly do this if the script has crashed and the lock is stale.')) return;
  btn.disabled = true; btn.textContent = '…';
  vvPost('/plugins/varaverk/api/clearlock.php', {file})
    .then(d => { if (d.ok) vvBoardPoll(); else { btn.disabled = false; btn.textContent = 'Clear'; }})
    .catch(() => { btn.disabled = false; btn.textContent = 'Clear'; });
}

// ── Cron validation (inline red border on bad expressions) ────────────────

function vvValidateCronExpr(expr) {
  if (!expr) return true;
  if (expr === 'array_start' || expr === 'array_stop') return true;
  const parts = expr.trim().split(/\s+/);
  if (parts.length !== 5) return false;
  const ranges = [[0,59],[0,23],[1,31],[1,12],[0,7]];
  return parts.every((f,i) => {
    if (f === '*') return true;
    if (/^\*\/\d+$/.test(f)) { const n=parseInt(f.slice(2)); return n>0 && n<=ranges[i][1]; }
    if (/^\d+$/.test(f))     return parseInt(f)>=ranges[i][0] && parseInt(f)<=ranges[i][1];
    if (/^\d+-\d+$/.test(f)) { const [a,b]=f.split('-').map(Number); return a<=b && a>=ranges[i][0] && b<=ranges[i][1]; }
    if (f.includes(','))      return f.split(',').every(p=>/^\d+$/.test(p));
    return false;
  });
}

// ── Log search ───────────────────────────────────────────────────────────────

function vvFilterLog(term) {
  const pre = document.getElementById('vv-log-pre');
  const raw = pre._raw || '';
  if (!term.trim()) { pre.textContent = raw; return; }
  const esc   = s => s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  const escRe = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const re    = new RegExp('(' + escRe + ')', 'gi');
  pre.innerHTML = raw.split('\n').map(line => {
    const e = esc(line);
    if (!line) return '';
    if (re.test(line)) { re.lastIndex = 0; return e.replace(re, '<mark>$1</mark>'); }
    re.lastIndex = 0;
    return '<span class="vv-log-dim">' + e + '</span>';
  }).join('\n');
}

// ── Cron humanizer ───────────────────────────────────────────────────────────

function vvHumanCron(expr) {
  expr = (expr || '').trim();
  if (!expr) return '';
  if (expr === 'array_start') return 'on array start';
  if (expr === 'array_stop')  return 'on array stop';
  const p = expr.split(/\s+/);
  if (p.length !== 5) return '';
  const [min, hour, dom, month, dow] = p;
  const DAYS = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
  const hhmm = (h, m) => {
    const H = parseInt(h), M = parseInt(m);
    if (H === 0  && M === 0) return 'midnight';
    if (H === 12 && M === 0) return 'noon';
    const ampm = H < 12 ? 'am' : 'pm';
    const h12  = H === 0 ? 12 : H > 12 ? H - 12 : H;
    return M === 0 ? `${h12}${ampm}` : `${h12}:${String(M).padStart(2,'0')}${ampm}`;
  };
  // Every minute / every N minutes
  if (min === '*' && [hour,dom,month,dow].every(f => f === '*')) return 'every minute';
  if (/^\*\/(\d+)$/.test(min) && [hour,dom,month,dow].every(f => f === '*'))
    return parseInt(min.slice(2)) === 1 ? 'every minute' : 'every ' + min.slice(2) + ' min';
  // Every hour / every N hours
  if (min === '0' && hour === '*' && [dom,month,dow].every(f => f === '*')) return 'every hour';
  if (min === '0' && /^\*\/(\d+)$/.test(hour) && [dom,month,dow].every(f => f === '*'))
    return parseInt(hour.slice(2)) === 1 ? 'every hour' : 'every ' + hour.slice(2) + ' hours';
  if (!/^\d+$/.test(min) || !/^\d+$/.test(hour)) return '';
  const T = hhmm(hour, min);
  // Weekdays / weekends
  if (dom === '*' && month === '*' && dow === '1-5')            return 'weekdays at ' + T;
  if (dom === '*' && month === '*' && /^(6,0|0,6)$/.test(dow)) return 'weekends at ' + T;
  // Nth weekday of month  (1-7 = 1st, 8-14 = 2nd, 15-21 = 3rd, 22-28 = 4th)
  const NTH = [[1,7,'1st'],[8,14,'2nd'],[15,21,'3rd'],[22,28,'4th']];
  for (const [lo,hi,lbl] of NTH) {
    if (dom === `${lo}-${hi}` && month === '*' && /^\d+$/.test(dow))
      return `${lbl} ${DAYS[parseInt(dow)] ?? 'day'} of month at ${T}`;
  }
  // Monthly on any specific day-of-month
  if (/^\d+$/.test(dom) && month === '*' && dow === '*') {
    const d = parseInt(dom);
    const sfx = [11,12,13].includes(d) ? 'th'
              : d % 10 === 1 ? 'st' : d % 10 === 2 ? 'nd' : d % 10 === 3 ? 'rd' : 'th';
    return (d === 1 ? 'monthly' : `${d}${sfx} of every month`) + ' at ' + T;
  }
  // Specific day(s) of week
  if (dom === '*' && month === '*' && /^\d+$/.test(dow))
    return (DAYS[parseInt(dow)] ?? 'day ' + dow) + 's at ' + T;
  if (dom === '*' && month === '*' && /^\d+(,\d+)+$/.test(dow))
    return dow.split(',').map(d => DAYS[parseInt(d)] ?? 'd'+d).join('/') + ' at ' + T;
  // Daily
  if (dom === '*' && month === '*' && dow === '*') return 'daily at ' + T;
  return '';
}

// ── Natural language → cron expression ──────────────────────────────────────

function vvNlToCron(text) {
  const t = text.toLowerCase().replace(/\s+/g, ' ').trim();
  let m;
  // Pure interval patterns (no time needed)
  if (/\bevery\s+minute\b/.test(t))                                    return '* * * * *';
  if (/\bhourly\b/.test(t))                                            return '0 * * * *';
  if (/\bevery\s+hour\b/.test(t) && !/\d+\s*h/.test(t))              return '0 * * * *';
  if ((m = t.match(/\bevery\s+(\d+)\s*(?:minutes?|mins?)\b/)))        return `*/${m[1]} * * * *`;
  if ((m = t.match(/\bevery\s+(\d+)\s*hours?\b/)))                    return `0 */${m[1]} * * *`;

  // Parse time — optional, defaults to midnight when not given
  const parseTime = s => {
    let h = -1, min = 0, tm;
    if (/\bmidnight\b/.test(s))                                         { h = 0;  min = 0; }
    else if (/\bnoon\b/.test(s))                                        { h = 12; min = 0; }
    else if ((tm = s.match(/\b(\d{1,2}):(\d{2})\s*(am|pm)?\b/)))       { h = parseInt(tm[1]); min = parseInt(tm[2]); if (tm[3]==='pm'&&h<12) h+=12; if (tm[3]==='am'&&h===12) h=0; }
    else if ((tm = s.match(/\b(\d{1,2})\s*(am|pm)\b/)))                { h = parseInt(tm[1]); if (tm[2]==='pm'&&h<12) h+=12; if (tm[2]==='am'&&h===12) h=0; }
    else if ((tm = s.match(/\bat\s+(\d{1,2}):(\d{2})\b/)))             { h = parseInt(tm[1]); min = parseInt(tm[2]); }
    return h >= 0 && h < 24 ? {h, min} : null;
  };
  const time = parseTime(t);
  const H = time?.h   ?? 0;   // default midnight
  const M = time?.min ?? 0;

  // Ordinals: 1st/2nd/3rd/4th and first/second/third/fourth
  const ORD = {first:1,'1st':1, second:2,'2nd':2, third:3,'3rd':3, fourth:4,'4th':4};
  const ordM = t.match(/\b(first|1st|second|2nd|third|3rd|fourth|4th)\b/);
  const ordN = ordM ? ORD[ordM[1]] : null;

  const DOW = {sun:0,sunday:0,mon:1,monday:1,tue:2,tuesday:2,wed:3,wednesday:3,
               thu:4,thursday:4,fri:5,friday:5,sat:6,saturday:6};
  const dayHits = [...t.matchAll(/\b(sun(?:day)?|mon(?:day)?|tue(?:s(?:day)?)?|wed(?:nesday)?|thu(?:rs(?:day)?)?|fri(?:day)?|sat(?:urday)?)\b/g)];

  // Nth weekday of month: "every 2nd sunday", "first monday of the month"
  if (ordN && dayHits.length) {
    const key    = dayHits[0][1].slice(0, 3);
    const dayNum = DOW[key] ?? DOW[dayHits[0][1]];
    if (dayNum !== undefined) {
      const lo = (ordN - 1) * 7 + 1, hi = ordN * 7;
      return `${M} ${H} ${lo}-${hi} * ${dayNum}`;
    }
  }

  // Monthly (no time required — defaults to midnight)
  if (/\bmonthly\b|\bevery\s+month\b|\b(?:once\s+a\s+month)\b/.test(t)) return `${M} ${H} 1 * *`;

  // Day-of-week patterns
  if (/\bweekdays?\b/.test(t) && !/weekend/.test(t))  return `${M} ${H} * * 1-5`;
  if (/\bweekends?\b/.test(t))                        return `${M} ${H} * * 6,0`;

  if (dayHits.length) {
    const nums = [...new Set(dayHits.map(d => { const k=d[1].slice(0,3); return DOW[k]??DOW[d[1]]; }).filter(n=>n!==undefined))];
    if (nums.length) return `${M} ${H} * * ${nums.join(',')}`;
  }

  // Require explicit time for a plain "daily" default
  if (!time) return null;
  return `${M} ${H} * * *`;
}

// ── Cron calculator state ────────────────────────────────────────────────────

let vvCalcTarget = null;
let vvCalcExpr   = '';

document.addEventListener('focusin', e => {
  if (!e.target.classList.contains('vv-cron')) return;
  if (e.target.classList.contains('vv-rsync-standalone')) return;
  if (!e.target.closest('#vv-sched-left')) return;
  vvCalcTarget = e.target;
  const label = e.target.closest('[data-id]')?.querySelector('.vv-job-label')?.textContent.trim() ?? '';
  const lbl   = document.getElementById('vv-calc-target-label');
  if (lbl) lbl.textContent = label || 'selected';
  const applyBtn = document.getElementById('vv-calc-apply');
  if (applyBtn && vvCalcExpr) applyBtn.style.display = '';
});

function vvCalcNextFires(expr, n) {
  const parts = expr.split(/\s+/);
  const [mF, hF, dF, moF, wF] = parts;
  const match = (f, v) => {
    if (f==='*') return true;
    if (/^\*\/\d+$/.test(f)) return v % parseInt(f.slice(2)) === 0;
    if (/^\d+$/.test(f))     return parseInt(f) === v;
    if (/^\d+-\d+$/.test(f)) { const [a,b]=f.split('-').map(Number); return v>=a&&v<=b; }
    if (f.includes(','))      return f.split(',').some(x=>parseInt(x)===v);
    return false;
  };
  const fires = [], t = new Date();
  t.setSeconds(0,0); t.setMinutes(t.getMinutes()+1);
  for (let i = 0; fires.length < n && i < 10080; i++, t.setMinutes(t.getMinutes()+1)) {
    if (match(moF,t.getMonth()+1) && match(dF,t.getDate()) && match(wF,t.getDay()) &&
        match(hF,t.getHours())    && match(mF,t.getMinutes())) fires.push(new Date(t));
  }
  return fires;
}

function vvCalcUpdate(val) {
  const result   = document.getElementById('vv-calc-result');
  const applyBtn = document.getElementById('vv-calc-apply');
  val = val.trim();
  if (!val) { result.innerHTML = ''; applyBtn.style.display = 'none'; vvCalcExpr = ''; return; }

  let expr = '', desc = '', hint = '';

  if (val === 'array_start' || val === 'array_stop') {
    expr = val; desc = vvHumanCron(val) || val;
  } else if (vvValidateCronExpr(val) && val.split(/\s+/).length === 5) {
    expr = val; desc = vvHumanCron(val);
  } else {
    const nl = vvNlToCron(val);
    if (nl) { expr = nl; desc = vvHumanCron(nl) || nl; }
    else hint = 'Try: "every 15 min", "daily at 3am", "sundays at 2:30am", "weekdays at 9am"';
  }

  vvCalcExpr = expr;

  if (hint) {
    result.innerHTML = '<div class="vv-calc-hint">' + vvEscHtml(hint) + '</div>';
    applyBtn.style.display = 'none';
    return;
  }

  const esc = vvEscHtml;
  let html  = '<div class="vv-calc-expr">' + esc(expr) + '</div>';
  if (desc) html += '<div class="vv-calc-desc">' + esc(desc) + '</div>';

  if (expr !== 'array_start' && expr !== 'array_stop') {
    const fires = vvCalcNextFires(expr, 5);
    if (fires.length) {
      const now = new Date();
      html += '<div class="vv-calc-runs-lbl">Next ' + fires.length + ' fires</div><div class="vv-calc-runs">';
      for (const f of fires) {
        const dur = vvFmtDuration(f - now);
        const at  = f.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
        const day = f.toLocaleDateString([], {weekday:'short', month:'short', day:'numeric'});
        html += '<div class="vv-calc-run-row"><span class="vv-calc-run-in">in ' + dur + '</span>'
              + '<span class="vv-calc-run-at">' + day + ' · ' + at + '</span></div>';
      }
      html += '</div>';
    }
  }

  result.innerHTML = html;
  applyBtn.style.display = vvCalcTarget ? '' : 'none';
}

function vvCalcApply() {
  if (!vvCalcExpr || !vvCalcTarget) return;
  vvCalcTarget.value = vvCalcExpr;
  vvUpdateCronHint(vvCalcTarget);
  vvSaveCronBlur(vvCalcTarget);
  const btn = document.getElementById('vv-calc-apply');
  const lbl = document.getElementById('vv-calc-target-label');
  const saved = btn.innerHTML;
  btn.innerHTML    = '✓ Applied to ' + (lbl?.textContent ?? 'job');
  btn.style.color  = '#4caf50';
  setTimeout(() => { btn.innerHTML = saved; btn.style.color = ''; }, 1800);
}

function vvUpdateCronHint(input) {
  if (!input || input.classList.contains('vv-rsync-standalone')) return;
  // No hint when the row already shows an ⚡ array-event badge
  const row = input.closest('.vv-job-row');
  if (row?.querySelector('.vv-event-badge')) {
    const stale = input.previousElementSibling;
    if (stale?.classList.contains('vv-cron-hint')) stale.style.display = 'none';
    return;
  }
  const hint = vvHumanCron(input.value);
  let el = input.previousElementSibling;
  if (!el || !el.classList.contains('vv-cron-hint')) {
    el = document.createElement('span');
    el.className = 'vv-cron-hint';
    input.before(el);
  }
  el.textContent   = hint;
  el.style.display = hint ? '' : 'none';
}

// ── Recent Activity ──────────────────────────────────────────────────────────

function vvTimeAgo(s) {
  if (s < 60)    return s + 's ago';
  if (s < 3600)  return Math.floor(s/60) + 'm ago';
  if (s < 86400) return Math.floor(s/3600) + 'h ago';
  return Math.floor(s/86400) + 'd ago';
}
function vvFmtDur(s) {
  if (s <= 0)  return '—';
  if (s < 60)  return s + 's';
  if (s < 3600) return Math.floor(s/60) + 'm ' + (s%60) + 's';
  return Math.floor(s/3600) + 'h ' + Math.floor((s%3600)/60) + 'm';
}

function vvLoadRecentActivity() {
  const body  = document.getElementById('vv-activity-body');
  const badge = document.getElementById('vv-activity-badge');
  if (!body) return;
  fetch('/plugins/varaverk/api/recent.php')
    .then(r => r.json())
    .then(d => {
      if (!d.ok || !d.runs?.length) {
        body.innerHTML = '<div class="vv-board-placeholder">No completed runs yet.</div>';
        if (badge) badge.style.display = 'none';
        return;
      }
      const now    = Math.floor(Date.now() / 1000);
      let errors   = 0;
      let html     = '<div class="vv-activity-list">';
      for (const r of d.runs) {
        const cls = r.status === 'ok' ? 'vv-stat-ok' : r.status === 'warn' ? 'vv-stat-warn' : 'vv-stat-error';
        if (r.status !== 'ok' && r.status !== 'skipped') errors++;
        html += '<div class="vv-activity-row" onclick="vvOpenRight(' + JSON.stringify(r.id) + ')">'
              + '<span class="vv-activity-dot ' + cls + '">●</span>'
              + '<span class="vv-activity-label">' + vvEscHtml(r.label) + '</span>'
              + '<span class="vv-activity-ago">'   + vvTimeAgo(now - r.start) + '</span>'
              + '<span class="vv-activity-dur">'   + vvFmtDur(r.dur) + '</span>'
              + '</div>';
      }
      html += '</div>';
      body.innerHTML = html;
      if (badge) { badge.textContent = errors || ''; badge.style.display = errors ? '' : 'none'; }
      if (errors) vvAutoExpand('vv-activity-body');
    })
    .catch(() => {});
}

function vvValidateCrons() {
  document.querySelectorAll('input.vv-cron').forEach(inp => {
    if (inp.disabled || inp.type === 'hidden') return;
    const ok = vvValidateCronExpr(inp.value.trim());
    inp.style.borderColor = inp.value.trim() && !ok ? '#f44336' : '';
    inp.title = ok ? '' : 'Invalid cron expression';
  });
}

function vvBoardInit() {
  vvBuildNextRuns();
  vvBoardPoll();
  vvValidateCrons();
  vvLoadRecentActivity();
  if (!vvBoardTimer) {
    vvBoardTimer = setInterval(() => { vvBuildNextRuns(); vvBoardPoll(); vvLoadRecentActivity(); }, 30000);
  }
  // Live cron validation + humanizer hint on input
  document.getElementById('vv-sched-left').addEventListener('input', e => {
    if (!e.target.classList.contains('vv-cron')) return;
    const ok = vvValidateCronExpr(e.target.value.trim());
    e.target.style.borderColor = e.target.value.trim() && !ok ? '#f44336' : '';
    vvUpdateCronHint(e.target);
  });
  // Seed hints for all existing cron values on load
  document.querySelectorAll('#vv-sched-left input.vv-cron').forEach(vvUpdateCronHint);
}

// Apply syntax highlighting to all embedded header blocks
function vvApplyHighlighting() {
  document.querySelectorAll('.vv-sb-hdr').forEach(el => {
    const text = el.textContent;
    el.innerHTML = vvHl(text, true);
  });
}

// ── Bash syntax highlighter ───────────────────────────────────────────────────

function vvHl(code, isHeader) {
  const esc = s => s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  return code.split('\n').map(line => isHeader ? vvHlHdr(line, esc) : vvHlSrc(line, esc)).join('\n');
}

// Header lines (# already stripped by PHP)
function vvHlHdr(line, esc) {
  if (!line.trim()) return '';
  const t = line.trim();
  // Separator / banner lines
  if (/^[─━═\-=#+\s]{4,}$/.test(t) || /^={2,}.*={2,}$/.test(t) || /^[─━]{3,}/.test(t)) {
    return '<span class="vv-hl-sep">' + esc(line) + '</span>';
  }
  // ALL CAPS section labels (PURPOSE, DESCRIPTION, HOST, etc.)
  if (/^[A-Z][A-Z_\s]{3,}:?\s*$/.test(t)) {
    return '<span class="vv-hl-section">' + esc(line) + '</span>';
  }
  // Key: value  (e.g. "Schedule: */7 * * * *", "Dependencies: common.sh")
  const kv = line.match(/^(\s*)([A-Za-z][A-Za-z _\-]{2,})(\s*:\s*)(.*)/);
  if (kv) {
    const key = kv[2].trim();
    const val = kv[4];
    if (/^schedule$/i.test(key)) {
      const cm = val.match(/^([@\d\/\*,\-]+ [\d\/\*,\-]+ [\d\/\*,\-]+ [\d\/\*,\-]+ [\d\/\*,\-]+|@[a-z_]+)(.*)/i);
      return esc(kv[1]) + '<span class="vv-hl-key">' + esc(kv[2]) + '</span>' + esc(kv[3])
           + (cm ? '<span class="vv-hl-cron">' + esc(cm[1]) + '</span>'
                 + '<span class="vv-hl-comment">' + esc(cm[2]) + '</span>'
                 : '<span class="vv-hl-cron">' + esc(val) + '</span>');
    }
    return esc(kv[1]) + '<span class="vv-hl-key">' + esc(kv[2]) + '</span>' + esc(kv[3])
         + '<span class="vv-hl-value">' + esc(val) + '</span>';
  }
  return '<span class="vv-hl-text">' + esc(line) + '</span>';
}

// Full source lines
function vvHlSrc(line, esc) {
  if (!line.trim()) return '';
  const t = line.trimStart();
  // Shebang
  if (t.startsWith('#!')) return '<span class="vv-hl-shebang">' + esc(line) + '</span>';
  // Full-line comment
  if (t.startsWith('#')) {
    const m = line.match(/^(\s*)(#+\s?)(.*)/);
    if (!m) return '<span class="vv-hl-comment">' + esc(line) + '</span>';
    const [, indent, hash, rest] = m;
    const rt = rest.trim();
    // Separator
    if (!rt || /^[─━═\-=#+\s]{4,}$/.test(rt) || /^={2,}.*={2,}$/.test(rt)) {
      return esc(indent) + '<span class="vv-hl-sep">' + esc(hash + rest) + '</span>';
    }
    // ALL CAPS label
    if (/^[A-Z][A-Z_\s]{3,}:?\s*$/.test(rt)) {
      return esc(indent) + '<span class="vv-hl-hash">' + esc(hash) + '</span>'
           + '<span class="vv-hl-section">' + esc(rest) + '</span>';
    }
    // Key: value
    const kv = rest.match(/^([A-Za-z][A-Za-z _\-]{2,})(\s*:\s*)(.*)/);
    if (kv) {
      const key = kv[1].trim();
      if (/^schedule$/i.test(key)) {
        const cm = kv[3].match(/^([@\d\/\*,\-]+ [\d\/\*,\-]+ [\d\/\*,\-]+ [\d\/\*,\-]+ [\d\/\*,\-]+|@[a-z_]+)(.*)/i);
        return esc(indent) + '<span class="vv-hl-hash">' + esc(hash) + '</span>'
             + '<span class="vv-hl-key">' + esc(kv[1]) + '</span>' + esc(kv[2])
             + (cm ? '<span class="vv-hl-cron">' + esc(cm[1]) + '</span>'
                   + '<span class="vv-hl-comment">' + esc(cm[2]) + '</span>'
                   : '<span class="vv-hl-cron">' + esc(kv[3]) + '</span>');
      }
      return esc(indent) + '<span class="vv-hl-hash">' + esc(hash) + '</span>'
           + '<span class="vv-hl-key">' + esc(kv[1]) + '</span>' + esc(kv[2])
           + '<span class="vv-hl-value">' + esc(kv[3]) + '</span>';
    }
    return esc(indent) + '<span class="vv-hl-comment">' + esc(hash + rest) + '</span>';
  }
  // Code line
  return vvHlCode(line, esc);
}

// Tokenize a bash code line
const VV_KEYWORDS = new Set('if then else elif fi for do done while until case esac in function return local export declare readonly unset shift break continue exit true false'.split(' '));
const VV_BUILTINS = new Set('echo printf read cd mkdir rm cp mv find grep sed awk cat chmod chown basename dirname date which command source eval exec wait kill trap sleep'.split(' '));

function vvHlCode(line, esc) {
  let result = '';
  let pos    = 0;
  const s    = line;

  while (pos < s.length) {
    const rest = s.slice(pos);
    let m;

    // Single-quoted string
    if ((m = rest.match(/^'([^']*)'/))) {
      result += '<span class="vv-hl-string">' + esc(m[0]) + '</span>';
      pos += m[0].length; continue;
    }
    // Double-quoted string (no nested escape for brevity)
    if ((m = rest.match(/^"[^"]*"/))) {
      result += '<span class="vv-hl-string">' + esc(m[0]) + '</span>';
      pos += m[0].length; continue;
    }
    // Variable: $((...)), $(...), ${...}, $WORD, $special
    if ((m = rest.match(/^\$\(\([^)]*\)\)/s))) { result += '<span class="vv-hl-var">' + esc(m[0]) + '</span>'; pos += m[0].length; continue; }
    if ((m = rest.match(/^\$\([^)]*\)/s)))     { result += '<span class="vv-hl-var">' + esc(m[0]) + '</span>'; pos += m[0].length; continue; }
    if ((m = rest.match(/^\$\{[^}]*\}/)))      { result += '<span class="vv-hl-var">' + esc(m[0]) + '</span>'; pos += m[0].length; continue; }
    if ((m = rest.match(/^\$[A-Za-z_]\w*/)))   { result += '<span class="vv-hl-var">' + esc(m[0]) + '</span>'; pos += m[0].length; continue; }
    if ((m = rest.match(/^\$[#@*?!0-9]/)))     { result += '<span class="vv-hl-var">' + esc(m[0]) + '</span>'; pos += m[0].length; continue; }

    // Word / keyword / builtin / identifier
    if ((m = rest.match(/^[A-Za-z_]\w*/))) {
      const word = m[0];
      if (VV_KEYWORDS.has(word))      result += '<span class="vv-hl-keyword">' + esc(word) + '</span>';
      else if (VV_BUILTINS.has(word)) result += '<span class="vv-hl-builtin">' + esc(word) + '</span>';
      else                            result += esc(word);
      pos += word.length; continue;
    }

    // Number
    if ((m = rest.match(/^[0-9]+(?:\.[0-9]+)?/))) {
      result += '<span class="vv-hl-number">' + esc(m[0]) + '</span>';
      pos += m[0].length; continue;
    }

    // Operator / punctuation
    if ((m = rest.match(/^[|&;<>!={}\[\]()+\-*\/\\@%^~,.:]+/))) {
      result += '<span class="vv-hl-op">' + esc(m[0]) + '</span>';
      pos += m[0].length; continue;
    }

    // Anything else (spaces, tabs, etc.)
    result += esc(s[pos]);
    pos++;
  }
  return result;
}

function vvRestoreAdvancedMode() {
  const saved = localStorage.getItem('vv-advanced-mode');
  if (saved === '1') {
    vvAdvancedMode = true;
    document.getElementById('vv-advanced-mode-btn').classList.add('vv-adv-mode-on');
    document.querySelectorAll('.vv-adv-only').forEach(el => el.style.display = '');
    document.querySelectorAll('.vv-sb-hdr').forEach(el => el.style.display = 'none');
    // full .vv-sb-full remain hidden until their block is opened
  }
}

// ── Raw conf editing ──────────────────────────────────────────────────────────

function vvEditRawConf(file) {
  vvRawConfFile = file;
  if (vvActiveId) {
    const old = document.querySelector('[data-id="' + CSS.escape(vvActiveId) + '"]');
    if (old) old.querySelector('.vv-job-row').classList.remove('vv-row-selected');
  }
  vvActiveId = null;
  if (vvPollTimer) { clearInterval(vvPollTimer); vvPollTimer = null; }

  document.getElementById('vv-editor-name').value    = file;
  document.getElementById('vv-editor-name').readOnly = true;
  document.getElementById('vv-editor-body').value    = 'Loading…';
  vvShowRawConfMode(file);

  fetch('/plugins/varaverk/api/rawconf.php?file=' + encodeURIComponent(file))
    .then(r => r.json())
    .then(d => {
      document.getElementById('vv-editor-body').value = d.ok ? d.content : '# Error loading file';
      document.getElementById('vv-editor').classList.add('vv-editor-hl');
      vvSyncHlOverlay();
      vvUndoCaptureInitial();
      vvEditorCursorMoved();
      requestAnimationFrame(vvFitRight);
    })
    .catch(() => { document.getElementById('vv-editor-body').value = '# Load failed'; });
}

function vvShowRawConfMode(title) {
  vvEditorReset();
  vvRestoreEditorPrefs();
  document.getElementById('vv-editor-status').classList.add('vv-ed-active');
  document.getElementById('vv-es-lang').textContent = 'Bash / Config';
  document.getElementById('vv-undo-btn').style.display = '';
  document.getElementById('vv-redo-btn').style.display = '';
  document.getElementById('vv-suggestions').style.display       = 'none';
  document.getElementById('vv-log-pre').style.display           = 'none';
  document.getElementById('vv-si-view').style.display           = 'none';
  document.getElementById('vv-editor').style.display            = 'flex';
  document.getElementById('vv-confform').style.display          = 'none';
  document.getElementById('vv-back-btn').style.display          = '';
  document.getElementById('vv-restore-btn').style.display       = 'none';
  document.getElementById('vv-cancel-edit-btn').style.display   = '';
  document.getElementById('vv-save-script-btn').style.display   = 'none';
  document.getElementById('vv-save-conf-btn').style.display     = 'none';
  document.getElementById('vv-save-rawconf-btn').style.display  = '';
  document.getElementById('vv-advanced-mode-btn').style.display = 'none';
  document.getElementById('vv-delete-script-btn').style.display = 'none';
  document.getElementById('vv-log-search').style.display        = 'none';
  document.getElementById('vv-log-search').value                = '';
  document.getElementById('vv-clear-btn').style.display         = 'none';
  document.getElementById('vv-stop-btn').style.display          = 'none';
  document.getElementById('vv-auto-scroll-label').style.display = 'none';
  document.getElementById('vv-invert-log-label').style.display  = 'none';
  document.getElementById('vv-log-title').textContent           = title;
  document.getElementById('vv-log-ts').textContent              = '';
  if (vvPollTimer) { clearInterval(vvPollTimer); vvPollTimer = null; }
  requestAnimationFrame(vvFitRight);
}

function vvSaveRawConf() {
  if (!vvRawConfFile) return;
  if (!vvSetupConf && !confirm('Save changes to "' + vvRawConfFile + '"?')) return;
  const content = document.getElementById('vv-editor-body').value;
  const btn     = document.getElementById('vv-save-rawconf-btn');
  btn.disabled    = true;
  btn.textContent = 'Saving…';
  vvPost('/plugins/varaverk/api/rawconf.php', {file: vvRawConfFile, content})
    .then(d => {
      btn.disabled = false;
      if (!d.ok) {
        btn.textContent = 'Save Conf';
        alert('Save failed: ' + (d.error ?? 'Unknown error'));
        return;
      }

      // Setup mode: forced editing sequence
      if (vvSetupConf) {
        if (vvRawConfFile === 'master.conf') {
          // master.conf saved → open host conf next
          btn.textContent = '✓ Saved — opening ' + vvLocalHostConf + '…';
          vvSetupConf = vvLocalHostConf;
          history.replaceState(null, '', location.pathname + '?tab=scheduler&vv_setup=' + encodeURIComponent(vvLocalHostConf));
          setTimeout(() => vvLoadRawConf(vvLocalHostConf), 400);
        } else {
          // Host conf saved → go to Partnership so SSH keys get set up
          vvSetupConf = '';
          btn.textContent = '✓ Done — opening Partnership…';
          setTimeout(() => { window.location.href = location.pathname + '?tab=partnership&vv_onboard=1'; }, 800);
        }
        return;
      }

      // Normal save: show push status
      // ready:false = partner not set up yet (pre-onboard) — treat as silent, not an error
      const push       = d.push ?? [];
      const synced     = push.filter(p => p.ok);
      const realFailed = push.filter(p => !p.ok && p.ready !== false);
      if (realFailed.length > 0) {
        btn.textContent = '✓ Saved · push failed: ' + realFailed.map(p => p.host).join(', ');
      } else if (synced.length > 0) {
        btn.textContent = '✓ Saved · synced to ' + synced.map(p => p.host).join(', ');
      } else {
        btn.textContent = '✓ Saved';
      }
      setTimeout(() => { btn.textContent = 'Save Conf'; }, 3500);
    })
    .catch(() => { btn.disabled = false; btn.textContent = 'Save Conf'; });
}

// ── Highlighted conf editor overlay ──────────────────────────────────────────

// ── Overlay helpers ────────────────────────────────────────────────────────────

// Annotate text occurrences in already-rendered HTML (text between > and < only).
// Returns modified HTML with <span class=cls> wrapping each whole-word match.
function vvMarkInHtml(html, word, cls, caseInsensitive) {
  if (!word || word.length < 1) return html;
  const hw  = word.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  const esc = hw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const fl  = caseInsensitive ? 'gi' : 'g';
  const re  = new RegExp('(?<![\\w$])' + esc + '(?![\\w])', fl);
  return html.replace(/>((?:[^<])*)</g, (_, txt) =>
    '>' + txt.replace(re, `<span class="${cls}">$&</span>`) + '<'
  );
}

// Like vvMarkInHtml but marks the curIdx-th match differently.
function vvMarkSearchInHtml(html, term, curIdx) {
  if (!term) return html;
  const hw  = term.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  const esc = hw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const re  = new RegExp(esc, 'gi');
  let   idx = 0;
  return html.replace(/>((?:[^<])*)</g, (_, txt) => {
    const repl = txt.replace(re, m => {
      const cls = idx++ === curIdx ? 'vv-search-mark-current' : 'vv-search-mark';
      return `<span class="${cls}">${m}</span>`;
    });
    return '>' + repl + '<';
  });
}

function vvSyncHlOverlay() {
  const ov = document.getElementById('vv-hl-overlay');
  if (!ov || !document.getElementById('vv-editor').classList.contains('vv-editor-hl')) return;
  const ta = document.getElementById('vv-editor-body');
  let html = vvHl(ta.value, false) + '\n';
  if (vvFindTerm) {
    html = vvMarkSearchInHtml(html, vvFindTerm, vvFindIdx);
  } else if (vvWordMatch) {
    html = vvMarkInHtml(html, vvWordMatch, 'vv-word-mark', false);
  }
  ov.innerHTML  = html;
  ov.scrollTop  = ta.scrollTop;
  ov.scrollLeft = ta.scrollLeft;
  vvUpdateLineNums();
  vvUpdateCurLine();
}

function vvSyncHlScroll() {
  const ta = document.getElementById('vv-editor-body');
  const ov = document.getElementById('vv-hl-overlay');
  if (!ov || !document.getElementById('vv-editor').classList.contains('vv-editor-hl')) return;
  ov.scrollTop  = ta.scrollTop;
  ov.scrollLeft = ta.scrollLeft;
  const gutter  = document.getElementById('vv-ln-gutter');
  if (gutter) gutter.scrollTop = ta.scrollTop;
  vvUpdateCurLine();
}

function vvUpdateLineNums() {
  const gutter = document.getElementById('vv-ln-gutter');
  const ta     = document.getElementById('vv-editor-body');
  if (!gutter || !ta || vvWordWrap) return;
  gutter.style.lineHeight = vvLH() + 'px';
  const lines  = ta.value.split('\n').length;
  const curLn  = ta.value.slice(0, ta.selectionStart).split('\n').length;
  let html = '';
  for (let i = 1; i <= lines; i++) {
    html += i === curLn ? `<span class="vv-gln-cur">${i}</span>\n` : `${i}\n`;
  }
  gutter.innerHTML = html;
  gutter.scrollTop = ta.scrollTop;
}

// Position the current-line highlight strip behind the overlay
function vvUpdateCurLine() {
  const ta = document.getElementById('vv-editor-body');
  const cl = document.getElementById('vv-cur-line');
  if (!cl || !ta || vvWordWrap) return;
  const lineH  = vvLH();
  const padTop = parseFloat(window.getComputedStyle(ta).paddingTop);
  const lineN  = ta.value.slice(0, ta.selectionStart).split('\n').length - 1; // 0-indexed
  cl.style.top     = (ta.offsetTop + padTop + lineN * lineH - ta.scrollTop) + 'px';
  cl.style.height  = lineH + 'px';
  cl.style.display = '';
}

// Called on cursor move (keyup, mouseup, click) — update word match, status bar, cur-line
function vvEditorCursorMoved() {
  vvUpdateCurLine();
  vvUpdateEditorStatus();

  if (vvFindTerm) { vvSyncHlOverlay(); return; } // find mode owns the overlay

  const ta    = document.getElementById('vv-editor-body');
  const start = ta.selectionStart;
  const end   = ta.selectionEnd;
  const val   = ta.value;

  // If user selected a word (not whitespace, at least 2 chars) → highlight all occurrences
  if (end > start) {
    const sel = val.slice(start, end);
    const word = /^[\w$]{2,}$/.test(sel) ? sel : null;
    if (word !== vvWordMatch) { vvWordMatch = word; vvSyncHlOverlay(); }
    return;
  }

  // Find the identifier word at cursor position
  const before  = val.slice(0, start);
  const after   = val.slice(start);
  const wBefore = before.match(/[$A-Za-z_][\w]*$/) ?? [''];
  const wAfter  = after.match(/^[\w]+/) ?? [''];
  const word    = wBefore[0].replace(/^\$/, '') + wAfter[0]; // strip leading $ for matching
  const newWord = word.length >= 2 ? word : null;
  if (newWord !== vvWordMatch) { vvWordMatch = newWord; vvSyncHlOverlay(); }
}

function vvUpdateEditorStatus() {
  const ta  = document.getElementById('vv-editor-body');
  const pos = document.getElementById('vv-es-pos');
  const sel = document.getElementById('vv-es-sel');
  if (!ta || !pos) return;
  const start  = ta.selectionStart;
  const end    = ta.selectionEnd;
  const val    = ta.value;
  const before = val.slice(0, start);
  const lines  = before.split('\n');
  pos.textContent = `Ln ${lines.length}, Col ${lines[lines.length - 1].length + 1}`;
  if (end > start) {
    const chars = end - start;
    const lns   = val.slice(start, end).split('\n').length;
    sel.textContent = lns > 1 ? `(${chars} chars · ${lns} lines)` : `(${chars} chars)`;
  } else {
    sel.textContent = '';
  }
}

// ── Find bar ───────────────────────────────────────────────────────────────────

function vvFindOpen() {
  const bar = document.getElementById('vv-editor-find');
  bar.classList.add('vv-find-open');
  const inp = document.getElementById('vv-find-input');
  // Pre-fill with selected word if any
  const ta  = document.getElementById('vv-editor-body');
  const sel = ta.value.slice(ta.selectionStart, ta.selectionEnd);
  if (sel && /^\w+$/.test(sel)) { inp.value = sel; }
  inp.focus(); inp.select();
  vvFindSearch();
}

function vvFindClose() {
  document.getElementById('vv-editor-find').classList.remove('vv-find-open');
  document.getElementById('vv-find-input').value = '';
  document.getElementById('vv-find-input').classList.remove('vv-find-no-match');
  document.getElementById('vv-find-count').textContent = '';
  vvFindTerm = ''; vvFindMatches = []; vvFindIdx = 0;
  document.getElementById('vv-editor-body').focus();
  vvSyncHlOverlay();
}

function vvFindSearch() {
  const inp = document.getElementById('vv-find-input');
  const cnt = document.getElementById('vv-find-count');
  vvFindTerm = inp.value;
  if (!vvFindTerm) {
    vvFindMatches = []; vvFindIdx = 0;
    cnt.textContent = '';
    inp.classList.remove('vv-find-no-match');
    vvSyncHlOverlay(); return;
  }
  const ta  = document.getElementById('vv-editor-body');
  const re  = new RegExp(vvFindTerm.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'gi');
  vvFindMatches = [];
  let m;
  while ((m = re.exec(ta.value)) !== null) vvFindMatches.push({start: m.index, end: m.index + m[0].length});
  if (!vvFindMatches.length) {
    vvFindIdx = 0;
    cnt.textContent = 'no matches';
    inp.classList.add('vv-find-no-match');
  } else {
    vvFindIdx = Math.min(vvFindIdx, vvFindMatches.length - 1);
    cnt.textContent = `${vvFindIdx + 1} / ${vvFindMatches.length}`;
    inp.classList.remove('vv-find-no-match');
    const cur = vvFindMatches[vvFindIdx];
    ta.selectionStart = cur.start; ta.selectionEnd = cur.end;
  }
  vvSyncHlOverlay();
}

function vvFindNav(dir) {
  if (!vvFindMatches.length) return;
  vvFindIdx = (vvFindIdx + dir + vvFindMatches.length) % vvFindMatches.length;
  document.getElementById('vv-find-count').textContent = `${vvFindIdx + 1} / ${vvFindMatches.length}`;
  const ta  = document.getElementById('vv-editor-body');
  const cur = vvFindMatches[vvFindIdx];
  ta.selectionStart = cur.start; ta.selectionEnd = cur.end;
  ta.focus();
  const lineN = ta.value.slice(0, cur.start).split('\n').length - 1;
  ta.scrollTop = Math.max(0, lineN * vvLH() - 80);
  vvSyncHlOverlay();
}

function vvFindKeydown(e) {
  if (e.key === 'Enter')  { e.preventDefault(); vvFindNav(e.shiftKey ? -1 : 1); }
  if (e.key === 'Escape') { e.preventDefault(); vvFindClose(); }
}

// ── Find & Replace ────────────────────────────────────────────────────────────

function vvFindToggleReplace(forceOpen) {
  const row = document.getElementById('vv-replace-row');
  const btn = document.getElementById('vv-find-expand');
  const show = forceOpen !== undefined ? forceOpen : !row.classList.contains('vv-repl-open');
  row.classList.toggle('vv-repl-open', show);
  btn.classList.toggle('open', show);
  if (show) document.getElementById('vv-replace-input').focus();
}

function vvReplaceOne() {
  if (!vvFindMatches.length) return;
  const ta  = document.getElementById('vv-editor-body');
  const rep = document.getElementById('vv-replace-input').value;
  const m   = vvFindMatches[vvFindIdx];
  vvUndoCapture(true);
  ta.value = ta.value.slice(0, m.start) + rep + ta.value.slice(m.end);
  const prevIdx = vvFindIdx;
  vvFindSearch();
  if (vvFindMatches.length) {
    vvFindIdx = Math.min(prevIdx, vvFindMatches.length - 1);
    document.getElementById('vv-find-count').textContent = `${vvFindIdx + 1} / ${vvFindMatches.length}`;
    const cur = vvFindMatches[vvFindIdx];
    ta.selectionStart = cur.start; ta.selectionEnd = cur.end;
  }
  vvSyncHlOverlay(); vvUndoCapture(true);
}

function vvReplaceAll() {
  if (!vvFindTerm) return;
  const ta  = document.getElementById('vv-editor-body');
  const rep = document.getElementById('vv-replace-input').value;
  const re  = new RegExp(vvFindTerm.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'gi');
  let n = 0;
  vvUndoCapture(true);
  ta.value = ta.value.replace(re, () => { n++; return rep; });
  ta.selectionStart = ta.selectionEnd = 0;
  vvFindSearch();
  document.getElementById('vv-find-count').textContent = n ? `replaced ${n}` : 'no matches';
  vvUndoCapture(true);
}

function vvReplaceKeydown(e) {
  if (e.key === 'Enter'  && !e.shiftKey) { e.preventDefault(); vvReplaceOne(); }
  if (e.key === 'Enter'  &&  e.shiftKey) { e.preventDefault(); vvFindNav(-1); }
  if (e.key === 'Escape')                { e.preventDefault(); vvFindClose(); }
}

// ── Font size ─────────────────────────────────────────────────────────────────

function vvFontSize(delta) {
  vvFontSizePx = Math.max(9, Math.min(22, vvFontSizePx + delta));
  localStorage.setItem('vv-editor-fontsize', vvFontSizePx);
  const fs  = vvFontSizePx + 'px';
  const lh  = (vvFontSizePx * 1.5) + 'px';
  const els = ['vv-editor-body', 'vv-hl-overlay', 'vv-ln-gutter'];
  els.forEach(id => {
    const el = document.getElementById(id);
    if (el) { el.style.fontSize = fs; el.style.lineHeight = lh; el.style.fontFamily = 'monospace'; }
  });
  const cl = document.getElementById('vv-cur-line');
  if (cl) cl.style.height = lh;
  const fs_el = document.getElementById('vv-es-fontsize');
  if (fs_el) fs_el.textContent = fs;
  vvSyncHlOverlay(); vvUpdateCurLine(); vvUpdateEditorStatus();
}

function vvRestoreEditorPrefs() {
  const saved = parseInt(localStorage.getItem('vv-editor-fontsize') || '12') || 12;
  vvFontSizePx = 12;
  vvFontSize(saved - 12); // always force inline fontSize+lineHeight — Unraid SPA CSS overrides line-height: 1.5 to normal
  vvWordWrap = localStorage.getItem('vv-editor-wordwrap') === '1';
  vvApplyWordWrap(false);
}

// ── Word wrap toggle ──────────────────────────────────────────────────────────

function vvApplyWordWrap(save) {
  const wrap = document.getElementById('vv-editor-wrap');
  const btn  = document.getElementById('vv-es-wrap-btn');
  if (!wrap) return;
  if (vvWordWrap) {
    wrap.classList.add('vv-ed-wrap');
    if (btn) { btn.classList.add('active'); btn.title = 'Word wrap ON — click to disable'; }
  } else {
    wrap.classList.remove('vv-ed-wrap');
    if (btn) { btn.classList.remove('active'); btn.title = 'Word wrap OFF — click to enable'; }
  }
  if (save) localStorage.setItem('vv-editor-wordwrap', vvWordWrap ? '1' : '0');
  vvSyncHlOverlay();
}

function vvToggleWordWrap() {
  vvWordWrap = !vvWordWrap;
  vvApplyWordWrap(true);
}

// ── Go to line ────────────────────────────────────────────────────────────────

function vvGotoOpen() {
  const bar = document.getElementById('vv-goto-bar');
  if (!bar) return;
  bar.classList.add('vv-goto-open');
  const ta  = document.getElementById('vv-editor-body');
  const inp = document.getElementById('vv-goto-input');
  inp.value = ta.value.slice(0, ta.selectionStart).split('\n').length;
  document.getElementById('vv-goto-info').textContent = 'of ' + ta.value.split('\n').length;
  inp.focus(); inp.select();
}

function vvGotoClose() {
  document.getElementById('vv-goto-bar').classList.remove('vv-goto-open');
  document.getElementById('vv-editor-body').focus();
}

function vvGotoApply() {
  const ta    = document.getElementById('vv-editor-body');
  const lines = ta.value.split('\n');
  const n     = Math.max(1, Math.min(parseInt(document.getElementById('vv-goto-input').value) || 1, lines.length));
  let pos = 0;
  for (let i = 0; i < n - 1; i++) pos += lines[i].length + 1;
  ta.selectionStart = ta.selectionEnd = pos;
  ta.scrollTop = Math.max(0, (n - 5) * vvLH());
  vvSyncHlOverlay();
  vvEditorCursorMoved();
}

function vvGotoKeydown(e) {
  if (e.key === 'Enter')  { e.preventDefault(); vvGotoApply(); vvGotoClose(); }
  if (e.key === 'Escape') { e.preventDefault(); vvGotoClose(); }
}

// ── Line operations ───────────────────────────────────────────────────────────

function vvMoveLine(dir) {
  const ta = document.getElementById('vv-editor-body');
  const v  = ta.value, s = ta.selectionStart;
  vvUndoCapture(true);
  const ls = v.lastIndexOf('\n', s - 1) + 1;
  const le = v.indexOf('\n', s);
  if (dir === -1) {
    if (ls === 0) return;
    const pls = v.lastIndexOf('\n', ls - 2) + 1;
    const prev = v.slice(pls, ls - 1), cur = v.slice(ls, le === -1 ? v.length : le);
    ta.value = v.slice(0, pls) + cur + '\n' + prev + v.slice(le === -1 ? v.length : le);
    ta.selectionStart = ta.selectionEnd = pls + (s - ls);
  } else {
    if (le === -1) return;
    const nle = v.indexOf('\n', le + 1);
    const next = v.slice(le + 1, nle === -1 ? v.length : nle), cur = v.slice(ls, le);
    ta.value = v.slice(0, ls) + next + '\n' + cur + v.slice(nle === -1 ? v.length : nle);
    ta.selectionStart = ta.selectionEnd = ls + next.length + 1 + (s - ls);
  }
  vvSyncHlOverlay(); vvUndoCapture(true); vvEditorCursorMoved();
}

function vvDuplicateLine(above) {
  const ta = document.getElementById('vv-editor-body');
  const v  = ta.value, s = ta.selectionStart;
  vvUndoCapture(true);
  const ls  = v.lastIndexOf('\n', s - 1) + 1;
  const le  = v.indexOf('\n', s);
  const cur = v.slice(ls, le === -1 ? v.length : le);
  if (above) {
    ta.value = v.slice(0, ls) + cur + '\n' + v.slice(ls);
    ta.selectionStart = ta.selectionEnd = ls + cur.length + 1 + (s - ls);
  } else {
    const ins = le === -1 ? v.length : le;
    ta.value = v.slice(0, ins) + '\n' + cur + v.slice(ins);
    ta.selectionStart = ta.selectionEnd = ins + 1 + (s - ls);
  }
  vvSyncHlOverlay(); vvUndoCapture(true); vvEditorCursorMoved();
}

// ── Select next occurrence (Ctrl+D) ──────────────────────────────────────────

function vvSelectNextOccurrence() {
  const ta  = document.getElementById('vv-editor-body');
  const v   = ta.value, s = ta.selectionStart, e = ta.selectionEnd;
  let word;
  if (e > s) {
    word = v.slice(s, e);
  } else {
    const wb = v.slice(0, s).match(/[\w$]+$/) ?? [''];
    const wa = v.slice(s).match(/^[\w]+/) ?? [''];
    word = wb[0] + wa[0];
    if (word.length < 1) return;
    // First call: just select the word at cursor
    const ws = s - wb[0].length;
    ta.selectionStart = ws; ta.selectionEnd = ws + word.length;
    vvWordMatch = word.replace(/^\$/, '');
    vvSyncHlOverlay(); vvEditorCursorMoved(); return;
  }
  if (!word) return;
  const re = new RegExp(word.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'g');
  re.lastIndex = e;
  let m = re.exec(v) ?? (() => { re.lastIndex = 0; return re.exec(v); })();
  if (!m || m.index === s) return;
  ta.selectionStart = m.index; ta.selectionEnd = m.index + word.length;
  ta.scrollTop = Math.max(0, (v.slice(0, m.index).split('\n').length - 5) * vvLH());
  vvWordMatch = word.replace(/^\$/, '');
  vvSyncHlOverlay(); vvEditorCursorMoved();
}

function vvEditorReset() {
  vvWordMatch = null; vvFindTerm = ''; vvFindMatches = []; vvFindIdx = 0;
  document.getElementById('vv-editor-find')?.classList.remove('vv-find-open');
  document.getElementById('vv-replace-row')?.classList.remove('vv-repl-open');
  document.getElementById('vv-find-expand')?.classList.remove('open');
  const inp = document.getElementById('vv-find-input');
  if (inp) { inp.value = ''; inp.classList.remove('vv-find-no-match'); }
  const rep = document.getElementById('vv-replace-input');
  if (rep) rep.value = '';
  const cnt = document.getElementById('vv-find-count');
  if (cnt) cnt.textContent = '';
  document.getElementById('vv-goto-bar')?.classList.remove('vv-goto-open');
  vvUndoReset();
}

// ── Undo / Redo ────────────────────────────────────────────────────────────────

function vvUndoSnapshot() {
  const ta = document.getElementById('vv-editor-body');
  return ta ? { v: ta.value, ss: ta.selectionStart, se: ta.selectionEnd } : null;
}

function _vvUndoCommit(snap) {
  if (!snap) return;
  const top = vvUndoStack[vvUndoStack.length - 1];
  if (top && top.v === snap.v) return; // identical — skip
  vvUndoStack.push(snap);
  if (vvUndoStack.length > VV_UNDO_MAX) vvUndoStack.shift();
  vvUndoUpdateBtns();
}

// immediate=true: flush debounce and commit now (use before/after programmatic edits)
// immediate=false (default): debounce — groups rapid typing into one undo step
function vvUndoCapture(immediate = false) {
  const snap = vvUndoSnapshot();
  if (!snap) return;
  if (immediate) {
    if (vvUndoTimer) { clearTimeout(vvUndoTimer); vvUndoTimer = null; }
    _vvUndoCommit(snap);
  } else {
    if (vvUndoTimer) clearTimeout(vvUndoTimer);
    vvUndoTimer = setTimeout(() => { vvUndoTimer = null; _vvUndoCommit(vvUndoSnapshot()); }, 600);
  }
}

function vvUndoCaptureInitial() {
  // Called after editor content loads — seeds the stack with the initial state
  if (vvUndoTimer) { clearTimeout(vvUndoTimer); vvUndoTimer = null; }
  vvUndoStack = [];
  vvRedoStack = [];
  _vvUndoCommit(vvUndoSnapshot());
}

function vvUndo() {
  if (vvUndoTimer) { clearTimeout(vvUndoTimer); vvUndoTimer = null; }
  if (vvUndoStack.length < 2) return;
  const ta = document.getElementById('vv-editor-body');
  // Push current state to redo before going back
  vvRedoStack.push({ v: ta.value, ss: ta.selectionStart, se: ta.selectionEnd });
  vvUndoStack.pop(); // discard current top (that's the state we're leaving)
  const prev = vvUndoStack[vvUndoStack.length - 1];
  ta.value = prev.v;
  ta.selectionStart = prev.ss;
  ta.selectionEnd   = prev.se;
  vvSyncHlOverlay();
  vvEditorCursorMoved();
  vvUndoUpdateBtns();
}

function vvRedo() {
  if (!vvRedoStack.length) return;
  const ta   = document.getElementById('vv-editor-body');
  const next = vvRedoStack.pop();
  _vvUndoCommit({ v: ta.value, ss: ta.selectionStart, se: ta.selectionEnd });
  ta.value = next.v;
  ta.selectionStart = next.ss;
  ta.selectionEnd   = next.se;
  vvSyncHlOverlay();
  vvEditorCursorMoved();
  vvUndoUpdateBtns();
}

function vvUndoUpdateBtns() {
  const ub = document.getElementById('vv-undo-btn');
  const rb = document.getElementById('vv-redo-btn');
  const uc = document.getElementById('vv-undo-count');
  const depth = vvUndoStack.length - 1; // steps available
  if (ub) ub.disabled = depth < 1;
  if (rb) rb.disabled = vvRedoStack.length === 0;
  if (uc) uc.textContent = depth > 0 ? depth : '';
}

function vvUndoReset() {
  if (vvUndoTimer) { clearTimeout(vvUndoTimer); vvUndoTimer = null; }
  vvUndoStack = []; vvRedoStack = [];
  vvUndoUpdateBtns();
}

function vvEditorKeydown(e) {
  const ta    = e.target;
  const start = ta.selectionStart;
  const end   = ta.selectionEnd;
  const val   = ta.value;

  // Ctrl/Cmd+S — save
  if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
    e.preventDefault();
    if (vvRawConfFile) vvSaveRawConf(); else vvSaveScript();
    return;
  }

  // ── Ctrl/Cmd shortcuts ────────────────────────────────────────────────────
  if (e.ctrlKey || e.metaKey) {
    if (e.key === 'z' && !e.shiftKey) { e.preventDefault(); vvUndo(); return; }
    if (e.key === 'y' || (e.key === 'z' && e.shiftKey)) { e.preventDefault(); vvRedo(); return; }
    if (e.key === 's') { e.preventDefault(); if (vvRawConfFile) vvSaveRawConf(); else vvSaveScript(); return; }
    if (e.key === 'f') { e.preventDefault(); vvFindOpen(); return; }
    if (e.key === 'h') { e.preventDefault(); vvFindOpen(); vvFindToggleReplace(true); return; }
    if (e.key === 'g') { e.preventDefault(); vvGotoOpen(); return; }
    if (e.key === 'l' && !e.shiftKey) {
      // Select current line
      e.preventDefault();
      const ls = val.lastIndexOf('\n', start - 1) + 1;
      const le = val.indexOf('\n', start);
      ta.selectionStart = ls;
      ta.selectionEnd   = le === -1 ? val.length : le + 1;
      vvEditorCursorMoved(); return;
    }
    if (e.key === 'd' && !e.shiftKey) { e.preventDefault(); vvSelectNextOccurrence(); return; }
    if (e.key === 'k' && e.shiftKey) {
      // Delete current line
      e.preventDefault(); vvUndoCapture(true);
      const ls = val.lastIndexOf('\n', start - 1) + 1;
      const le = val.indexOf('\n', start);
      if (le === -1) {
        ta.value = ls > 0 ? val.slice(0, ls - 1) : '';
        ta.selectionStart = ta.selectionEnd = Math.max(0, ls - 1);
      } else {
        ta.value = val.slice(0, ls) + val.slice(le + 1);
        ta.selectionStart = ta.selectionEnd = ls;
      }
      vvSyncHlOverlay(); vvUndoCapture(true); return;
    }
    if (e.key === '/') {
      // Toggle line comment — single line or entire multi-line selection
      e.preventDefault(); vvUndoCapture(true);
      const bs = val.lastIndexOf('\n', start - 1) + 1;
      const be = end > start ? val.indexOf('\n', end - 1) : val.indexOf('\n', start);
      const block = val.slice(bs, be === -1 ? val.length : be);
      const lines = block.split('\n');
      const allCommented = lines.every(l => !l.trim() || /^\s*#/.test(l));
      const newBlock = lines.map(l =>
        !l.trim() ? l
        : allCommented ? l.replace(/^(\s*)#\s?/, '$1')
        : l.replace(/^(\s*)/, '$1# ')
      ).join('\n');
      ta.value = val.slice(0, bs) + newBlock + val.slice(be === -1 ? val.length : be);
      if (end > start) {
        ta.selectionStart = bs; ta.selectionEnd = bs + newBlock.length; // keep block selected
      } else {
        const curLineDelta = newBlock.split('\n')[0].length - lines[0].length;
        ta.selectionStart = ta.selectionEnd = start + curLineDelta;
      }
      vvSyncHlOverlay(); vvUndoCapture(true); return;
    }
    return; // let browser handle other Ctrl combos
  }

  // ── Escape ─────────────────────────────────────────────────────────────────
  if (e.key === 'Escape') {
    const fb = document.getElementById('vv-editor-find');
    if (fb?.classList.contains('vv-find-open')) { e.preventDefault(); vvFindClose(); return; }
    const gb = document.getElementById('vv-goto-bar');
    if (gb?.classList.contains('vv-goto-open')) { e.preventDefault(); vvGotoClose(); return; }
  }

  // ── Alt shortcuts (line moves) ─────────────────────────────────────────────
  if (e.altKey) {
    if (e.key === 'ArrowUp'   && !e.shiftKey) { e.preventDefault(); vvMoveLine(-1); return; }
    if (e.key === 'ArrowDown' && !e.shiftKey) { e.preventDefault(); vvMoveLine(1);  return; }
    if (e.key === 'ArrowDown' &&  e.shiftKey) { e.preventDefault(); vvDuplicateLine(false); return; }
    if (e.key === 'ArrowUp'   &&  e.shiftKey) { e.preventDefault(); vvDuplicateLine(true);  return; }
  }

  // ── Smart Home ─────────────────────────────────────────────────────────────
  if (e.key === 'Home' && !e.altKey && !e.shiftKey) {
    e.preventDefault();
    const ls  = val.lastIndexOf('\n', start - 1) + 1;
    const fns = ls + val.slice(ls).match(/^(\s*)/)[1].length;
    ta.selectionStart = ta.selectionEnd = start === fns ? ls : fns;
    vvEditorCursorMoved(); return;
  }

  // ── Tab / Shift+Tab ────────────────────────────────────────────────────────
  if (e.key === 'Tab') {
    e.preventDefault(); vvUndoCapture(true);
    if (end > start && val.slice(start, end).includes('\n')) {
      const bs = val.lastIndexOf('\n', start - 1) + 1;
      const be = val.indexOf('\n', end - 1);
      const blk = val.slice(bs, be === -1 ? val.length : be);
      const nb  = e.shiftKey ? blk.replace(/^  /gm, '') : blk.replace(/^/gm, '  ');
      ta.value = val.slice(0, bs) + nb + val.slice(be === -1 ? val.length : be);
      ta.selectionStart = bs; ta.selectionEnd = bs + nb.length;
    } else if (e.shiftKey) {
      const ls = val.lastIndexOf('\n', start - 1) + 1;
      if (val.slice(ls, ls + 2) === '  ') {
        ta.value = val.slice(0, ls) + val.slice(ls + 2);
        ta.selectionStart = ta.selectionEnd = Math.max(ls, start - 2);
      }
    } else {
      ta.value = val.slice(0, start) + '  ' + val.slice(end);
      ta.selectionStart = ta.selectionEnd = start + 2;
    }
    vvSyncHlOverlay(); vvUndoCapture(true); return;
  }

  // ── Enter auto-indent ──────────────────────────────────────────────────────
  if (e.key === 'Enter') {
    e.preventDefault(); vvUndoCapture(true);
    const ls   = val.lastIndexOf('\n', start - 1) + 1;
    const curL = val.slice(ls, start);
    const ind  = curL.match(/^(\s*)/)[1];
    const xtra = /\{\s*$/.test(curL) ? '  ' : '';
    const ins  = '\n' + ind + xtra;
    ta.value = val.slice(0, start) + ins + val.slice(end);
    ta.selectionStart = ta.selectionEnd = start + ins.length;
    vvSyncHlOverlay(); vvUndoCapture(true); return;
  }

  // ── Auto-close brackets and quotes (no modifier keys) ─────────────────────
  if (!e.altKey) {
    // Skip over closing char if already there
    if (VV_CLOSE.has(e.key) && start === end && val[start] === e.key) {
      e.preventDefault();
      ta.selectionStart = ta.selectionEnd = start + 1;
      vvEditorCursorMoved(); return;
    }
    // Smart backspace: delete matched pair
    if (e.key === 'Backspace' && start === end && start > 0 && VV_PAIRS[val[start-1]] === val[start]) {
      e.preventDefault(); vvUndoCapture(true);
      ta.value = val.slice(0, start - 1) + val.slice(start + 1);
      ta.selectionStart = ta.selectionEnd = start - 1;
      vvSyncHlOverlay(); vvUndoCapture(true); return;
    }
    // Auto-close opening bracket/quote
    if (e.key in VV_PAIRS) {
      if (e.key === "'" && /\w/.test(val[start - 1] ?? '')) return; // skip contractions
      if (end === start && /\w/.test(val[start] ?? '')) return;     // skip if next is word char
      e.preventDefault(); vvUndoCapture(true);
      const cl = VV_PAIRS[e.key];
      if (end > start) {
        // Wrap selection in pair
        ta.value = val.slice(0, start) + e.key + val.slice(start, end) + cl + val.slice(end);
        ta.selectionStart = start + 1; ta.selectionEnd = end + 1;
      } else {
        ta.value = val.slice(0, start) + e.key + cl + val.slice(end);
        ta.selectionStart = ta.selectionEnd = start + 1;
      }
      vvSyncHlOverlay(); vvUndoCapture(true); return;
    }
  }
}

// ── Script browser tree ───────────────────────────────────────────────────────

function vvToggleSbChildren(expand, event) {
  event.stopPropagation();
  const entry = expand.closest('.vv-sb-entry');
  const kids  = entry.querySelector('.vv-sb-children');
  if (!kids) return;
  const open = kids.style.display !== 'none';
  kids.style.display = open ? 'none' : '';
  expand.textContent = open ? '▸' : '▾';
}

function vvSelectScript(row) {
  if (vvSelectedRow) vvSelectedRow.classList.remove('vv-sb-selected');
  vvSelectedRow  = row;
  row.classList.add('vv-sb-selected');
  const id   = row.dataset.id;
  const hdr  = row.dataset.hdr;
  const name = id.replace(/\.sh$/, '').split('/').pop();
  vvCurrentSiId  = id;
  vvCurrentSiHdr = hdr;
  vvShowScriptInfoMode(name, hdr, id);
}

function vvShowScriptInfoMode(name, hdr, id) {
  document.getElementById('vv-suggestions').style.display       = 'none';
  document.getElementById('vv-log-pre').style.display           = 'none';
  document.getElementById('vv-editor').style.display            = 'none';
  document.getElementById('vv-arrange-workspace').style.display = 'none';
  document.getElementById('vv-confform').style.display          = 'none';
  document.getElementById('vv-si-view').style.display           = '';
  document.getElementById('vv-back-btn').style.display          = '';
  document.getElementById('vv-restore-btn').style.display       = 'none';
  document.getElementById('vv-cancel-edit-btn').style.display   = 'none';
  document.getElementById('vv-save-script-btn').style.display   = 'none';
  document.getElementById('vv-save-conf-btn').style.display     = 'none';
  document.getElementById('vv-save-rawconf-btn').style.display  = 'none';
  const _advBtn = document.getElementById('vv-advanced-mode-btn');
  _advBtn.style.display = '';
  _advBtn.classList.toggle('vv-adv-mode-on', vvAdvancedMode);
  document.getElementById('vv-delete-script-btn').style.display = 'none';
  document.getElementById('vv-log-search').style.display        = 'none';
  document.getElementById('vv-log-search').value                = '';
  document.getElementById('vv-clear-btn').style.display         = 'none';
  document.getElementById('vv-stop-btn').style.display          = 'none';
  document.getElementById('vv-auto-scroll-label').style.display = 'none';
  document.getElementById('vv-invert-log-label').style.display  = 'none';
  document.getElementById('vv-log-title').textContent           = name;
  document.getElementById('vv-log-ts').textContent              = '';
  if (vvPollTimer) { clearInterval(vvPollTimer); vvPollTimer = null; }

  const si    = document.getElementById('vv-si-view');
  const _isMd = /\.md$/i.test(id);

  if (_isMd) {
    // Markdown files always show rendered content
    si.innerHTML = '<pre class="vv-si-src">(loading…)</pre>';
    fetch('/plugins/varaverk/api/readscript.php?id=' + encodeURIComponent(id))
      .then(r => r.json())
      .then(d => {
        si.innerHTML = '<pre class="vv-si-src">' + (d.ok ? vvEscHtml(d.content) : '# Error loading') + '</pre>';
        requestAnimationFrame(vvFitRight);
      })
      .catch(() => { si.innerHTML = '<pre class="vv-si-src"># Load failed</pre>'; });

  } else if (vvAdvancedMode) {
    // Advanced mode: header + README/Manual sections + inline config settings
    si.innerHTML = '<p class="vv-cf-empty">Loading…</p>';
    Promise.all([
      fetch('/plugins/varaverk/api/scriptinfo.php?id=' + encodeURIComponent(id)).then(r => r.json()),
      fetch('/plugins/varaverk/api/confform.php?id='   + encodeURIComponent(id)).then(r => r.json()),
    ]).then(([info, conf]) => {
      let html = '';
      const dispHdr = (info.ok && info.header) ? info.header : hdr;

      if (dispHdr) {
        html += '<div class="vv-sinfo-block">'
              + '<pre class="vv-si-hdr">' + vvHl(dispHdr, true) + '</pre>'
              + '</div>';
      }

      if (info.ok && info.sections?.length) {
        for (const sec of info.sections) {
          html += '<div class="vv-sinfo-block">'
                + '<div class="vv-sinfo-lbl">' + vvEscHtml(sec.source) + '</div>'
                + '<pre class="vv-readme-body">' + vvEscHtml(sec.body) + '</pre>'
                + '</div>';
        }
      }

      if (conf.ok && conf.groups?.length) {
        html += '<div class="vv-sinfo-block">'
              + '<div class="vv-sinfo-lbl">Config</div>'
              + vvRenderConfForm(conf.groups)
              + '</div>';
        vvConfId = id;
        document.getElementById('vv-cancel-edit-btn').style.display = '';
        document.getElementById('vv-save-conf-btn').style.display   = '';
      }

      si.innerHTML = html || '<p class="vv-cf-empty">No additional information found.</p>';
      requestAnimationFrame(vvFitRight);
    }).catch(() => { si.innerHTML = '<p class="vv-cf-empty">Failed to load info.</p>'; });

  } else {
    // Basic mode: header only
    si.innerHTML = '<pre class="vv-si-hdr">' + vvHl(hdr, true) + '</pre>';
  }
  requestAnimationFrame(vvFitRight);
}
</script>

<script>
// ── Arrange mode ─────────────────────────────────────────────────────────────

let vvArrangeMode    = false;
let vvArrangePending = [];  // [{script, fromArray, toArray, _undoFn}]
let vvDragScript     = null;
let vvDragFromArray  = null;  // null = unassigned/library
let vvDragEl         = null;

function vvToggleArrange() {
  vvArrangeMode ? vvCancelArrange() : vvEnterArrangeMode();
}

function vvEnterArrangeMode() {
  vvArrangeMode = true;
  document.getElementById('vv-sched-cards').classList.add('vv-arrange-active');

  // Show arrange workspace in right panel
  document.getElementById('vv-suggestions').style.display       = 'none';
  document.getElementById('vv-log-pre').style.display           = 'none';
  document.getElementById('vv-editor').style.display            = 'none';
  document.getElementById('vv-si-view').style.display           = 'none';
  document.getElementById('vv-confform').style.display          = 'none';
  document.getElementById('vv-arrange-workspace').style.display = '';
  document.getElementById('vv-back-btn').style.display          = '';
  document.getElementById('vv-restore-btn').style.display       = 'none';
  document.getElementById('vv-arrange-btn').textContent = 'Arranging…';
  document.getElementById('vv-arrange-btn').classList.add('vv-arrange-btn-active');
  document.getElementById('vv-arrange-save-btn').style.display  = '';
  document.getElementById('vv-arrange-cancel-btn').style.display = '';

  // Expand all orch children and make them droppable
  document.querySelectorAll('.vv-sched-card:not(.vv-custom-card)').forEach(card => {
    const childrenDiv = card.querySelector('.vv-children');
    if (!childrenDiv) return;
    if (childrenDiv.style.display === 'none') {
      childrenDiv.style.display = '';
      childrenDiv.dataset.arrangeExpanded = '1';
    }
    childrenDiv.addEventListener('dragover',  _vvChildDragOver);
    childrenDiv.addEventListener('dragleave', _vvChildDragLeave);
    childrenDiv.addEventListener('drop',      _vvChildDrop);

    // Add drag handles to conf-managed script children
    childrenDiv.querySelectorAll('.vv-script').forEach(s => {
      if (s.dataset.type === 'conf_flag' || !s.dataset.confArray) return;
      s.setAttribute('draggable', 'true');
      const h = document.createElement('span');
      h.className = 'vv-drag-handle';
      h.textContent = '⠿';
      s.querySelector('.vv-job-row').prepend(h);
      s.addEventListener('dragstart', _vvOrchScriptDragStart);
      s.addEventListener('dragend',   vvDragEnd);
    });
  });

  // Add drag handles to custom scripts (for folder drag)
  document.querySelectorAll('.vv-custom-card .vv-script').forEach(s => {
    s.setAttribute('draggable', 'true');
    const h = document.createElement('span');
    h.className = 'vv-drag-handle';
    h.textContent = '⠿';
    s.querySelector('.vv-job-row').prepend(h);
    s.addEventListener('dragstart', _vvCustomDragStart);
    s.addEventListener('dragend',   vvDragEnd);
  });

  // Make folder children droppable
  document.querySelectorAll('.vv-folder-children').forEach(fc => {
    fc.addEventListener('dragover',  _vvFolderDragOver);
    fc.addEventListener('dragleave', _vvFolderDragLeave);
    fc.addEventListener('drop',      _vvFolderDrop);
  });

  vvFitRight();
}

function vvExitArrangeMode() {
  vvArrangeMode = false;
  document.getElementById('vv-sched-cards').classList.remove('vv-arrange-active');
  document.querySelectorAll('.vv-drag-handle').forEach(h => h.remove());
  document.querySelectorAll('.vv-script[draggable]').forEach(s => {
    s.removeAttribute('draggable');
    s.removeEventListener('dragstart', _vvOrchScriptDragStart);
    s.removeEventListener('dragstart', _vvCustomDragStart);
    s.removeEventListener('dragend',   vvDragEnd);
  });
  document.querySelectorAll('.vv-children[data-arrange-expanded]').forEach(c => {
    c.style.display = 'none';
    delete c.dataset.arrangeExpanded;
  });
  document.querySelectorAll('.vv-children').forEach(c => {
    c.removeEventListener('dragover',  _vvChildDragOver);
    c.removeEventListener('dragleave', _vvChildDragLeave);
    c.removeEventListener('drop',      _vvChildDrop);
  });
  document.querySelectorAll('.vv-folder-children').forEach(fc => {
    fc.removeEventListener('dragover',  _vvFolderDragOver);
    fc.removeEventListener('dragleave', _vvFolderDragLeave);
    fc.removeEventListener('drop',      _vvFolderDrop);
  });
  const btn = document.getElementById('vv-arrange-btn');
  btn.textContent = 'Arrange';
  btn.classList.remove('vv-arrange-btn-active');
  document.getElementById('vv-arrange-save-btn').style.display  = 'none';
  document.getElementById('vv-arrange-cancel-btn').style.display = 'none';
  vvBackToSuggestions();
}

function vvCancelArrange() {
  vvArrangePending.forEach(p => { if (p._undoFn) p._undoFn(); });
  vvArrangePending = [];
  vvExitArrangeMode();
}

async function vvSaveArrange() {
  if (!vvArrangePending.length) { vvExitArrangeMode(); return; }
  const btn = document.getElementById('vv-arrange-save-btn');
  btn.textContent = 'Saving…';
  btn.disabled = true;

  // Collect the final ordered state of every orch array from the DOM.
  // Each .vv-children[data-conf-arrays] holds the live order; scripts that were
  // moved here from other arrays already have their data-conf-array updated.
  const arrayMap = new Map(); // arrayName → [{id, enabled}]
  document.querySelectorAll('.vv-children[data-conf-arrays]').forEach(div => {
    const primaryArray = (div.dataset.confArrays || '').split(',').filter(Boolean)[0];
    if (!primaryArray) return;
    const scripts = [];
    div.querySelectorAll('.vv-script[data-id]').forEach(s => {
      if (s.classList.contains('vv-drag-ghost')) return;
      const id      = s.dataset.id;
      const enabled = s.dataset.confEnabled !== '0';
      const arr     = s.dataset.confArray || primaryArray;
      // Group by target array (scripts may have been moved here from a different array)
      if (!arrayMap.has(arr)) arrayMap.set(arr, []);
      arrayMap.get(arr).push({ id, enabled });
    });
    // Mark primary array as explicitly visited (even if empty after removals)
    if (!arrayMap.has(primaryArray)) arrayMap.set(primaryArray, []);
  });

  let failed = false;
  for (const [arrayName, scripts] of arrayMap) {
    const r = await vvPost('/plugins/varaverk/api/reorderarray.php', {
      array_name: arrayName,
      scripts:    JSON.stringify(scripts)
    }).then(r => r.json()).catch(() => ({ ok: false }));
    if (!r.ok) { failed = true; break; }
  }

  if (failed) {
    btn.textContent = 'Error!';
    btn.style.color = '#f44336';
    setTimeout(() => { btn.textContent = 'Save Arrangement'; btn.disabled = false; btn.style.color = ''; }, 2500);
    return;
  }
  location.reload();
}

// ── Drag handlers ─────────────────────────────────────────────────────────────

function _vvOrchScriptDragStart(e) {
  vvDragEl         = this;
  vvDragScript     = this.dataset.id;
  vvDragFromArray  = this.dataset.confArray || null;
  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/plain', vvDragScript);
  document.body.classList.add('vv-is-dragging');
  setTimeout(() => this.classList.add('vv-drag-ghost'), 0);
}

function _vvCustomDragStart(e) {
  vvDragEl        = this;
  vvDragScript    = this.dataset.id;
  vvDragFromArray = null;
  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/plain', vvDragScript);
  document.body.classList.add('vv-is-dragging');
  setTimeout(() => this.classList.add('vv-drag-ghost'), 0);
}

function vvLibDragStart(e, el) {
  vvDragEl        = el;
  vvDragScript    = el.dataset.script;
  vvDragFromArray = null;
  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/plain', vvDragScript);
  document.body.classList.add('vv-is-dragging');
  setTimeout(() => el.classList.add('vv-drag-ghost'), 0);
}

function vvDragEnd(e) {
  if (vvDragEl) vvDragEl.classList.remove('vv-drag-ghost');
  document.body.classList.remove('vv-is-dragging');
  vvDragEl = null;
}
// Failsafe: clear dragging class if drop lands outside the window
document.addEventListener('dragend', () => document.body.classList.remove('vv-is-dragging'));

// Return the .vv-script element that the cursor is above the midpoint of,
// or null if cursor is below all items (meaning: append).
function _vvFindInsertBefore(container, clientY) {
  const items = [...container.querySelectorAll('.vv-script:not(.vv-drag-ghost):not(.vv-drop-line)')];
  for (const item of items) {
    const rect = item.getBoundingClientRect();
    if (clientY < rect.top + rect.height / 2) return item;
  }
  return null;
}

// Drop on an orch's children div → move to that orch array (with position)
function _vvChildDragOver(e) {
  if (!vvDragScript || vvDragScript.startsWith('Custom/')) return;
  e.preventDefault();
  e.dataTransfer.dropEffect = 'move';
  this.classList.add('vv-drop-target');

  // Show insertion line indicator
  let line = this.querySelector('.vv-drop-line');
  if (!line) {
    line = document.createElement('div');
    line.className = 'vv-drop-line';
  }
  const insertBefore = _vvFindInsertBefore(this, e.clientY);
  if (insertBefore) {
    this.insertBefore(line, insertBefore);
  } else {
    this.appendChild(line);
  }
}
function _vvChildDragLeave(e) {
  if (!this.contains(e.relatedTarget)) {
    this.classList.remove('vv-drop-target');
    this.querySelector('.vv-drop-line')?.remove();
  }
}
function _vvChildDrop(e) {
  e.preventDefault();
  const line = this.querySelector('.vv-drop-line');
  const insertBefore = line ? line.nextElementSibling : null;
  line?.remove();
  this.classList.remove('vv-drop-target');

  const toArrays = (this.dataset.confArrays || '').split(',').filter(Boolean);
  const toArray  = toArrays[0];
  if (!toArray || !vvDragScript) return;
  if (vvDragScript.startsWith('Custom/')) return;

  const srcEl     = vvDragEl;
  const fromArray = vvDragFromArray;
  const script    = vvDragScript;
  if (!srcEl) return;

  const srcParent  = srcEl.parentElement;
  const oldArray   = srcEl.dataset.confArray || fromArray;

  // Library card dropped onto orch
  if (!fromArray) {
    _vvMoveLibCardToOrch(srcEl, this, script, toArray, insertBefore);
    return;
  }

  // Reorder within same orch or move across — insert at position
  if (insertBefore && insertBefore !== srcEl && this.contains(insertBefore)) {
    this.insertBefore(srcEl, insertBefore);
  } else if (!insertBefore) {
    this.appendChild(srcEl);
  }
  // (if insertBefore === srcEl: dropped in same slot, no DOM change needed)

  srcEl.dataset.confArray = toArray;

  // Only record a pending entry if something actually changed
  const orderChanged = srcParent !== this || oldArray !== toArray;
  if (orderChanged) {
    vvArrangePending.push({
      script, fromArray: oldArray, toArray,
      _undoFn: () => { srcParent?.appendChild(srcEl); srcEl.dataset.confArray = oldArray; }
    });
    vvUpdatePendingUI();
  }
}

// Move a library card into an orch (dragging from unassigned pool)
function _vvMoveLibCardToOrch(libCard, destDiv, script, toArray, insertBefore = null) {
  const label = script.split('/').pop().replace(/\.sh$/, '');
  const row = document.createElement('div');
  row.className = 'vv-script';
  row.dataset.id = script;
  row.dataset.confArray = toArray;
  row.dataset.confEnabled = '1';
  row.setAttribute('draggable', 'true');
  row.innerHTML = `<div class="vv-job-row"><span class="vv-drag-handle">⠿</span><span class="vv-job-label">${vvEscHtml(label)}</span><span style="font-size:10px;color:#666;margin-left:4px;">(new)</span></div>`;
  row.addEventListener('dragstart', _vvOrchScriptDragStart);
  row.addEventListener('dragend',   vvDragEnd);
  if (insertBefore && destDiv.contains(insertBefore)) {
    destDiv.insertBefore(row, insertBefore);
  } else {
    destDiv.appendChild(row);
  }
  libCard.remove();
  // Remove "all assigned" placeholder if present
  const ph = document.getElementById('vv-library-cards').querySelector('.vv-board-placeholder');
  if (ph) ph.remove();

  vvArrangePending.push({
    script, fromArray: null, toArray,
    _undoFn: () => {
      row.remove();
      const libCards = document.getElementById('vv-library-cards');
      const restored = document.createElement('div');
      restored.className = 'vv-lib-card';
      restored.dataset.script = script;
      restored.draggable = true;
      const dir = script.split('/').slice(0,-1).join('/');
      restored.innerHTML = `<span class="vv-lib-card-name">${vvEscHtml(label)}</span><span class="vv-lib-card-path">${vvEscHtml(dir)}</span>`;
      restored.addEventListener('dragstart', function(ev){ vvLibDragStart(ev,this); });
      restored.addEventListener('dragend', vvDragEnd);
      libCards.appendChild(restored);
    }
  });
  vvUpdatePendingUI();
}

// Drop on library zone → remove from orch
function vvDropToLibrary(e, el) {
  e.preventDefault();
  el.classList.remove('vv-drop-target');
  const script    = vvDragScript;
  const fromArray = vvDragFromArray;
  const srcEl     = vvDragEl;
  if (!script || !fromArray || !srcEl) return;  // library→library or custom script noop

  const label     = script.split('/').pop().replace(/\.sh$/, '');
  const dir       = script.split('/').slice(0,-1).join('/');
  const srcParent = srcEl.parentElement;
  srcEl.remove();

  // Add a lib card
  const libCards = document.getElementById('vv-library-cards');
  const ph = libCards.querySelector('.vv-board-placeholder');
  if (ph) ph.remove();
  const libCard = document.createElement('div');
  libCard.className = 'vv-lib-card';
  libCard.dataset.script = script;
  libCard.draggable = true;
  libCard.innerHTML = `<span class="vv-lib-card-name">${vvEscHtml(label)}</span><span class="vv-lib-card-path">${vvEscHtml(dir)}</span>`;
  libCard.addEventListener('dragstart', function(ev){ vvLibDragStart(ev,this); });
  libCard.addEventListener('dragend', vvDragEnd);
  libCards.appendChild(libCard);

  vvArrangePending.push({
    script, fromArray, toArray: null,
    _undoFn: () => {
      libCard.remove();
      srcParent.appendChild(srcEl);
      srcEl.dataset.confArray = fromArray;
    }
  });
  vvUpdatePendingUI();
}

// Folder drag handlers (custom scripts only)
function _vvFolderDragOver(e) {
  if (!vvDragScript || !vvDragScript.startsWith('Custom/')) return;
  e.preventDefault();
  e.dataTransfer.dropEffect = 'move';
  this.classList.add('vv-drop-target');
}
function _vvFolderDragLeave(e) {
  if (!this.contains(e.relatedTarget)) this.classList.remove('vv-drop-target');
}
async function _vvFolderDrop(e) {
  e.preventDefault();
  this.classList.remove('vv-drop-target');
  const script = vvDragScript;
  const srcEl  = vvDragEl;
  if (!script || !script.startsWith('Custom/') || !srcEl) return;

  const oldParent = srcEl.parentElement;
  this.appendChild(srcEl);

  const folders = vvGetCurrentFolders();
  const r = await vvPost('/plugins/varaverk/api/savefolders.php', {
    folders: JSON.stringify(folders)
  }).then(r => r.json()).catch(() => ({ ok: false }));

  if (!r.ok) {
    oldParent.appendChild(srcEl);
    alert('Failed to save folder assignment.');
  }
}

function vvUpdatePendingUI() {
  const count    = vvArrangePending.length;
  const badge    = document.getElementById('vv-pending-badge');
  const pending  = document.getElementById('vv-arrange-pending');
  const list     = document.getElementById('vv-pending-list');
  badge.textContent = `${count} pending`;
  badge.style.display = count > 0 ? '' : 'none';
  pending.style.display = count > 0 ? '' : 'none';
  list.innerHTML = vvArrangePending.map(p => {
    const label = p.script.split('/').pop().replace(/\.sh$/, '');
    const from  = p.fromArray ? p.fromArray.replace(/_SCRIPTS$/, '') : 'unassigned';
    const to    = p.toArray   ? p.toArray.replace(/_SCRIPTS$/, '')   : 'unassigned';
    return `<div class="vv-pending-row"><span class="vv-pending-script">${vvEscHtml(label)}</span><span class="vv-pending-arrow">${vvEscHtml(from)} → ${vvEscHtml(to)}</span></div>`;
  }).join('');
}

// ── Custom script folders ─────────────────────────────────────────────────────

function vvToggleFolder(header) {
  const body    = header.nextElementSibling;
  const chevron = header.querySelector('.vv-folder-chevron');
  if (!body) return;
  const open = body.style.display === 'none';
  body.style.display = open ? '' : 'none';
  if (chevron) chevron.textContent = open ? '▾' : '▸';
}

// ── Snapshot footer poll ──────────────────────────────────────────────────────
let vvSnapTimer = null;
function vvPollSnapshot() {
  fetch('/plugins/varaverk/api/snapshot.php')
    .then(r => r.json())
    .then(d => {
      const cpuBar = document.getElementById('vv-snap-cpu-bar');
      const cpuVal = document.getElementById('vv-snap-cpu-val');
      if (cpuBar && cpuVal) {
        const p = d.cpu_pct ?? 0;
        cpuBar.style.width = p + '%';
        cpuBar.style.backgroundColor = p > 85 ? '#f44336' : p > 60 ? '#ff9800' : '#4caf50';
        cpuVal.textContent = p + '%';
      }
      const ramBar = document.getElementById('vv-snap-ram-bar');
      const ramVal = document.getElementById('vv-snap-ram-val');
      if (ramBar && ramVal) {
        const p = d.ram_pct ?? 0;
        ramBar.style.width = p + '%';
        ramBar.style.backgroundColor = p > 85 ? '#f44336' : p > 70 ? '#ff9800' : '#4caf50';
        ramVal.textContent = p + '%';
      }
      const fb = document.getElementById('vv-snap-fallback');
      if (fb) {
        const s = (d.fallback ?? 'UNKNOWN').toUpperCase();
        fb.textContent = s;
        fb.style.color = s === 'NORMAL'      ? '#4caf50'
                       : s === 'FAILOVER'    ? '#f44336'
                       : s === 'NO_INTERNET' ? '#ff9800' : '#666';
      }
      const pt = document.getElementById('vv-snap-partner');
      if (pt) {
        if (!d.partner_enabled) {
          pt.textContent = 'No partnership'; pt.style.color = '#444';
        } else {
          const peers = d.peers ?? [];
          const on  = peers.filter(p => p.online === true);
          const off = peers.filter(p => p.online !== true);
          if (off.length > 0 && peers.length === 1) {
            pt.textContent = off[0].hostname + ' ✕'; pt.style.color = '#f44336';
          } else if (off.length > 0) {
            pt.textContent = off.length + ' peer' + (off.length > 1 ? 's' : '') + ' down'; pt.style.color = '#f44336';
          } else if (on.length === 1) {
            pt.textContent = on[0].hostname + ' ●'; pt.style.color = '#4caf50';
          } else if (on.length > 1) {
            pt.textContent = on.length + ' peers ●'; pt.style.color = '#4caf50';
          } else {
            pt.textContent = '—'; pt.style.color = '#555';
          }
        }
      }
      const sm = document.getElementById('vv-snap-streams');
      if (sm) {
        const sc = d.stream_count ?? 0;
        sm.textContent = sc + (sc === 1 ? ' stream' : ' streams');
        sm.style.color = sc > 0 ? '#aaa' : '#444';
      }
      const tc = document.getElementById('vv-snap-transcodes');
      if (tc) {
        const n = d.transcode_count ?? 0;
        tc.textContent = n + (n === 1 ? ' transcode' : ' transcodes');
        tc.style.color = n > 0 ? '#ff9800' : '#444';
      }
    })
    .catch(() => {});
}
function vvStartSnapPoll() {
  if (vvSnapTimer) return;
  vvPollSnapshot();
  vvSnapTimer = setInterval(vvPollSnapshot, 10000);
}

function vvGetCurrentFolders() {
  const folders = {};
  document.querySelectorAll('.vv-folder-group').forEach(fg => {
    const name = fg.dataset.folder;
    folders[name] = [];
    fg.querySelectorAll('.vv-folder-children .vv-script[data-id]').forEach(s => {
      folders[name].push(s.dataset.id);
    });
  });
  return folders;
}

function vvNewFolder() {
  const customChildren = document.getElementById('vv-custom-children');
  if (!customChildren) return;
  // Expand if collapsed
  if (customChildren.style.display === 'none') customChildren.style.display = '';
  if (customChildren.querySelector('.vv-folder-new-row')) return;

  const wrap = document.createElement('div');
  wrap.className = 'vv-folder-new-row';
  const inp = document.createElement('input');
  inp.type = 'text';
  inp.className = 'vv-cron vv-folder-new-input';
  inp.placeholder = 'Folder name…';
  inp.style.cssText = 'width:160px;flex:none';
  const saveBtn = document.createElement('button');
  saveBtn.className = 'vv-btn-sm vv-save-script-btn-style';
  saveBtn.textContent = 'Create';
  saveBtn.onclick = () => _vvDoCreateFolder(inp.value.trim(), wrap);
  const cancelBtn = document.createElement('button');
  cancelBtn.className = 'vv-btn-sm';
  cancelBtn.textContent = '✕';
  cancelBtn.onclick = () => wrap.remove();
  inp.addEventListener('keydown', e => {
    if (e.key === 'Enter')  _vvDoCreateFolder(inp.value.trim(), wrap);
    if (e.key === 'Escape') wrap.remove();
  });
  wrap.append(inp, saveBtn, cancelBtn);
  customChildren.insertBefore(wrap, customChildren.firstChild);
  inp.focus();
}

async function _vvDoCreateFolder(name, wrap) {
  if (!name) return;
  const folders = vvGetCurrentFolders();
  if (folders[name] !== undefined) { alert(`Folder "${name}" already exists.`); return; }
  folders[name] = [];
  const r = await vvPost('/plugins/varaverk/api/savefolders.php', {
    folders: JSON.stringify(folders)
  }).then(r => r.json()).catch(() => ({ ok: false }));
  if (!r.ok) { alert('Failed to create folder.'); return; }
  wrap.remove();
  // Add folder group to DOM
  const customChildren = document.getElementById('vv-custom-children');
  const fg = document.createElement('div');
  fg.className = 'vv-folder-group';
  fg.dataset.folder = name;
  fg.innerHTML = `
    <div class="vv-folder-row" onclick="vvToggleFolder(this)">
      <span class="vv-folder-chevron">▸</span>
      <span class="vv-folder-name">${vvEscHtml(name)}</span>
      <span class="vv-folder-count">0</span>
    </div>
    <div class="vv-folder-children" style="display:none"></div>
  `;
  customChildren.insertBefore(fg, customChildren.firstChild);
  // Wire up drop listeners if arrange mode is active
  if (vvArrangeMode) {
    const fc = fg.querySelector('.vv-folder-children');
    fc.addEventListener('dragover',  _vvFolderDragOver);
    fc.addEventListener('dragleave', _vvFolderDragLeave);
    fc.addEventListener('drop',      _vvFolderDrop);
  }
}
</script>

<script>
// On page load: apply orch state for any orch that is already enabled.
document.querySelectorAll('.vv-sched-card').forEach(card => {
  const orchOn = card.querySelector('.vv-orch-row .vv-enabled')?.checked ?? false;
  if (orchOn) vvApplyOrchState(card, true);
});

requestAnimationFrame(function() {
  vvRestoreInvert();
  vvRestoreAdvancedMode();
  vvRestoreSugStates();
  vvApplyHighlighting();
  vvBoardInit();
  const last = localStorage.getItem('vv-last-job');
  if (last) {
    const lname = last.replace(/\.sh$/, '').split('/').pop();
    document.getElementById('vv-restore-label').textContent = lname;
    document.getElementById('vv-restore-btn').style.display = '';
  }
  vvFitRight();
  vvStartStatusPoll();
  vvStartSnapPoll();
});

// ── Keyboard navigation ───────────────────────────────────────────────────────
// ↑/↓ — navigate top-level orchestrator cards
// Home/End — jump to first/last card
// Escape — close right panel
// / — focus log search when visible
document.addEventListener('keydown', function(e) {
  // Don't intercept when typing in any input/textarea/select
  const t = document.activeElement;
  if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' ||
            t.tagName === 'SELECT' || t.isContentEditable)) return;

  // Escape — back to suggestions
  if (e.key === 'Escape' && vvActiveId) {
    e.preventDefault();
    vvBackToSuggestions();
    return;
  }

  // / — focus log search when the log panel is open
  if (e.key === '/' && document.getElementById('vv-log-search').style.display !== 'none') {
    e.preventDefault();
    document.getElementById('vv-log-search').focus();
    return;
  }

  // ↑/↓/Home/End — navigate top-level orchestrator cards
  if (!['ArrowUp','ArrowDown','Home','End'].includes(e.key)) return;
  const cards = [...document.querySelectorAll('#vv-sched-cards > .vv-sched-card')];
  if (!cards.length) return;
  const cur = vvActiveId ? cards.findIndex(c => c.dataset.id === vvActiveId) : -1;
  let next;
  if      (e.key === 'ArrowDown') next = cur < cards.length - 1 ? cur + 1 : cur;
  else if (e.key === 'ArrowUp')   next = cur > 0 ? cur - 1 : 0;
  else if (e.key === 'Home')      next = 0;
  else                            next = cards.length - 1;
  if (next === cur && cur !== -1) return;
  if (next < 0) next = 0;
  e.preventDefault();
  cards[next].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
  vvOpenRight(cards[next].dataset.id);
});
</script>
