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) {
= htmlspecialchars($orch['label']) ?>
-
+
+ = $orch['cron'] === '@array_start' ? '⚡ Array Start' : '⚡ Array Stop' ?>
+
+
+
+
@@ -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;