scheduler: add Array Starting / Array Stopping event triggers

Two hardcoded event-triggered entries appear at the top of the scheduler
(array_started.sh / array_stopping.sh). They show an  Array Start /
 Array Stop badge instead of a cron field and are enabled/disabled via
the normal toggle.

Static event scripts fire them via Unraid's event system:
- event/disks_mounted/array_start_jobs  → runs in background (non-blocking)
- event/disks_unmounting/array_stop_jobs → runs foreground (blocks until done)

vv_cron_rebuild() skips @array_* entries so they never land in the cron file.
Scripts executed on each event are managed in master.conf via
ARRAY_START_SCRIPTS and ARRAY_STOP_SCRIPTS arrays.
This commit is contained in:
Gmer4Lfe
2026-05-24 21:54:13 -04:00
parent 1540c62d3a
commit 655c520ae8
6 changed files with 126 additions and 24 deletions
@@ -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;
}
@@ -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; }
@@ -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
@@ -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
@@ -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;
@@ -36,8 +36,13 @@ foreach ($tree as $orch) {
<span class="vv-slider"></span>
</label>
<span class="vv-job-label"><?= htmlspecialchars($orch['label']) ?></span>
<input type="text" class="vv-cron" value="<?= htmlspecialchars($orch['cron']) ?>"
placeholder="cron expression">
<?php if ($orch['type'] === 'event'): ?>
<span class="vv-event-badge"><?= $orch['cron'] === '@array_start' ? '⚡ Array Start' : '⚡ Array Stop' ?></span>
<input type="hidden" class="vv-cron" value="<?= htmlspecialchars($orch['cron']) ?>">
<?php else: ?>
<input type="text" class="vv-cron" value="<?= htmlspecialchars($orch['cron']) ?>"
placeholder="cron expression">
<?php endif; ?>
<span class="vv-save-check"></span>
</div>
<?php if (!empty($orch['desc'])): ?>
@@ -186,6 +191,7 @@ foreach ($tree as $orch) {
<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>
<span id="vv-log-ts" class="vv-log-ts"></span>
<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>
</div>
</div>
@@ -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;