diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/api/scheduler.php b/Plugin/usr/local/emhttp/plugins/varaverk/api/scheduler.php index eb086b6..51159c1 100644 --- a/Plugin/usr/local/emhttp/plugins/varaverk/api/scheduler.php +++ b/Plugin/usr/local/emhttp/plugins/varaverk/api/scheduler.php @@ -12,8 +12,9 @@ if (!$id) { exit; } -// Basic cron validation — 5 fields or empty -if ($cron && !preg_match('/^(\S+\s+){4}\S+$/', $cron)) { +// Basic cron validation — 5 fields, or known @event trigger, or empty +if ($cron && !in_array($cron, ['@array_start', '@array_stop'], true) + && !preg_match('/^(\S+\s+){4}\S+$/', $cron)) { echo json_encode(['ok' => false, 'error' => 'Invalid cron expression']); exit; } diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/css/varaverk.css b/Plugin/usr/local/emhttp/plugins/varaverk/css/varaverk.css index ccb4930..5e8c2d5 100644 --- a/Plugin/usr/local/emhttp/plugins/varaverk/css/varaverk.css +++ b/Plugin/usr/local/emhttp/plugins/varaverk/css/varaverk.css @@ -57,6 +57,9 @@ cursor: default; } .vv-cron { flex: 0 0 110px; width: 110px; background: #111; border: 1px solid #444; color: #ddd; padding: 4px 6px; border-radius: 4px; font-family: monospace; font-size: 13px; } +.vv-event-badge { flex: 0 0 auto; padding: 3px 8px; border-radius: 4px; font-size: 12px; + background: #1a3a1a; border: 1px solid #2e6b2e; color: #6fcf6f; + white-space: nowrap; font-weight: 500; } .vv-log-label { display: flex; align-items: center; gap: 4px; font-size: 12px; color: #888; cursor: pointer; white-space: nowrap; flex-shrink: 0; } .vv-log-label input { cursor: pointer; accent-color: #4caf50; } diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/event/disks_mounted/array_start_jobs b/Plugin/usr/local/emhttp/plugins/varaverk/event/disks_mounted/array_start_jobs new file mode 100755 index 0000000..7351af4 --- /dev/null +++ b/Plugin/usr/local/emhttp/plugins/varaverk/event/disks_mounted/array_start_jobs @@ -0,0 +1,13 @@ +#!/bin/bash +# Varaverk: run array_started.sh if enabled in the schedule (background — non-blocking). +php -r " +require_once '/usr/local/emhttp/plugins/varaverk/include/scheduler.php'; +\$s = vv_schedule_load(); +\$e = \$s['Orchestrators/array_started.sh'] ?? []; +if (empty(\$e['enabled'])) exit(0); +\$runner = '/usr/local/emhttp/plugins/varaverk/run_job.sh'; +\$script = SCRIPTS_DIR . '/Orchestrators/array_started.sh'; +if (!file_exists(\$script)) exit(0); +\$flags = !empty(\$e['log_enabled']) ? ' --log' : ''; +exec('nohup bash ' . escapeshellarg(\$runner) . ' Orchestrators/array_started.sh ' . escapeshellarg(\$script) . \$flags . ' > /dev/null 2>&1 &'); +" 2>/dev/null diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/event/disks_unmounting/array_stop_jobs b/Plugin/usr/local/emhttp/plugins/varaverk/event/disks_unmounting/array_stop_jobs new file mode 100755 index 0000000..c95a51f --- /dev/null +++ b/Plugin/usr/local/emhttp/plugins/varaverk/event/disks_unmounting/array_stop_jobs @@ -0,0 +1,13 @@ +#!/bin/bash +# Varaverk: run array_stopping.sh if enabled in the schedule (foreground — blocks until done). +php -r " +require_once '/usr/local/emhttp/plugins/varaverk/include/scheduler.php'; +\$s = vv_schedule_load(); +\$e = \$s['Orchestrators/array_stopping.sh'] ?? []; +if (empty(\$e['enabled'])) exit(0); +\$runner = '/usr/local/emhttp/plugins/varaverk/run_job.sh'; +\$script = SCRIPTS_DIR . '/Orchestrators/array_stopping.sh'; +if (!file_exists(\$script)) exit(0); +\$flags = !empty(\$e['log_enabled']) ? ' --log' : ''; +passthru('bash ' . escapeshellarg(\$runner) . ' Orchestrators/array_stopping.sh ' . escapeshellarg(\$script) . \$flags); +" 2>/dev/null diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/include/scheduler.php b/Plugin/usr/local/emhttp/plugins/varaverk/include/scheduler.php index 9dceafa..56b1680 100644 --- a/Plugin/usr/local/emhttp/plugins/varaverk/include/scheduler.php +++ b/Plugin/usr/local/emhttp/plugins/varaverk/include/scheduler.php @@ -57,6 +57,8 @@ function vv_cron_rebuild(array $schedule): bool { $scriptsDir = SCRIPTS_DIR; foreach ($schedule as $entry) { if (empty($entry['enabled']) || empty($entry['cron']) || empty($entry['id'])) continue; + // Event-triggered jobs (@array_start / @array_stop) are handled by static event scripts, not cron. + if (str_starts_with($entry['cron'], '@array_')) continue; $script = "$scriptsDir/{$entry['id']}"; $flags = !empty($entry['log_enabled']) ? ' --log' : ''; $id = $entry['id']; @@ -193,31 +195,55 @@ function vv_custom_scripts(): array { } // Walk the scripts repo and return the job tree: -// orchestrators as top-level, individual scripts as children +// hardcoded array-event entries first, then cron-scheduled orchestrators function vv_job_tree(): array { $scriptsDir = SCRIPTS_DIR; $schedule = vv_schedule_load(); - // Orchestrators are in Orchestrators/ and their children are all scripts they call - // For now: walk top-level folders, treat *_management.sh or *_maintenance.sh as orchs - $orchPattern = "$scriptsDir/Orchestrators/*.sh"; - $orchs = []; - - foreach (glob($orchPattern) ?: [] as $path) { - $id = 'Orchestrators/' . basename($path); - $entry = $schedule[$id] ?? ['enabled' => false, 'cron' => '']; - $suggested = vv_script_suggested_cron($path); + // Hardcoded array-event entries — always present, trigger via Unraid event scripts + $eventDefs = [ + ['id' => 'Orchestrators/array_started.sh', 'cron' => '@array_start', 'label' => 'Array Starting'], + ['id' => 'Orchestrators/array_stopping.sh', 'cron' => '@array_stop', 'label' => 'Array Stopping'], + ]; + $orchs = []; + $eventIds = []; + foreach ($eventDefs as $ev) { + $id = $ev['id']; + $path = "$scriptsDir/$id"; + $entry = $schedule[$id] ?? []; + $eventIds[] = $id; $orchs[] = [ - 'id' => $id, - 'label' => basename($path, '.sh'), - 'desc' => vv_script_description($path), - 'type' => 'orchestrator', - 'enabled' => (bool)($entry['enabled'] ?? false), - 'cron' => $entry['cron'] ?? '', - 'log_enabled' => (bool)($entry['log_enabled'] ?? false), + 'id' => $id, + 'label' => $ev['label'], + 'desc' => vv_script_description($path), + 'type' => 'event', + 'enabled' => (bool)($entry['enabled'] ?? false), + 'cron' => $ev['cron'], + 'log_enabled' => (bool)($entry['log_enabled'] ?? false), + 'suggested_cron' => '', + 'suggested_label' => '', + 'children' => vv_script_children($path, $schedule), + ]; + } + + // Cron-scheduled orchestrators — discovered by glob, event entries excluded + $orchPattern = "$scriptsDir/Orchestrators/*.sh"; + foreach (glob($orchPattern) ?: [] as $path) { + $id = 'Orchestrators/' . basename($path); + if (in_array($id, $eventIds, true)) continue; + $entry = $schedule[$id] ?? ['enabled' => false, 'cron' => '']; + $suggested = vv_script_suggested_cron($path); + $orchs[] = [ + 'id' => $id, + 'label' => basename($path, '.sh'), + 'desc' => vv_script_description($path), + 'type' => 'orchestrator', + 'enabled' => (bool)($entry['enabled'] ?? false), + 'cron' => $entry['cron'] ?? '', + 'log_enabled' => (bool)($entry['log_enabled'] ?? false), 'suggested_cron' => $suggested['cron'], 'suggested_label' => $suggested['label'], - 'children' => vv_script_children($path, $schedule), + 'children' => vv_script_children($path, $schedule), ]; } return $orchs; diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/pages/scheduler.php b/Plugin/usr/local/emhttp/plugins/varaverk/pages/scheduler.php index e7a4ebd..d970ab8 100644 --- a/Plugin/usr/local/emhttp/plugins/varaverk/pages/scheduler.php +++ b/Plugin/usr/local/emhttp/plugins/varaverk/pages/scheduler.php @@ -36,8 +36,13 @@ foreach ($tree as $orch) { - + + + + + + @@ -186,6 +191,7 @@ foreach ($tree as $orch) { + @@ -363,6 +369,7 @@ function vvShowLogMode(id) { 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); @@ -424,6 +431,7 @@ function vvBackToSuggestions() { } 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); @@ -445,6 +453,7 @@ function vvOpenRight(id) { if (job) job.querySelector('.vv-job-row').classList.add('vv-row-selected'); vvShowLogMode(id); + vvSetStopBtn(vvRunningSet.has(id)); requestAnimationFrame(vvFitRight); vvFetchRight(); @@ -475,16 +484,51 @@ function vvFetchRight() { .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'; + 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'; + 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() { @@ -663,6 +707,7 @@ function vvShowEditorMode(title) { document.getElementById('vv-restore-btn').style.display = 'none'; document.getElementById('vv-save-script-btn').style.display = ''; 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; @@ -722,6 +767,7 @@ function vvShowConfMode(title) { document.getElementById('vv-save-conf-btn').style.display = ''; document.getElementById('vv-delete-script-btn').style.display = 'none'; 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;