scheduler: orch-god toggle model — children managed via master.conf
When orch is ON (god mode): - Children's toggle state is read from master.conf comment status - Toggling a child comments/uncomments its line in the *_SCRIPTS array - Child crons are suppressed in vv_cron_rebuild — orch is the sole trigger - Children not found in any *_SCRIPTS array are shown disabled (read-only) When orch is OFF: - All children flip to off; schedule.json updated immediately - A child with a cron value + enabled toggle gets its own independent cron entry - A child with no cron does nothing when enabled New: vv_conf_script_map() — cached per-request scan of master.conf arrays New: vv_parse_conf_array_full() — includes commented entries (disabled scripts) New: vv_conf_toggle_script() — comments/uncomments a script line in master.conf New: api/conf_toggle.php — endpoint for child toggle → master.conf write
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
$id = trim($_POST['id'] ?? '');
|
||||
$enabled = ($_POST['enabled'] ?? '0') === '1';
|
||||
|
||||
if (!$id || !preg_match('/^[a-zA-Z0-9_.\/\-]+\.sh$/', $id) || str_contains($id, '..')) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Invalid id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ok = vv_conf_toggle_script($id, $enabled);
|
||||
echo json_encode(['ok' => $ok, 'error' => $ok ? null : 'Failed to write master.conf']);
|
||||
@@ -54,14 +54,30 @@ function vv_cron_rebuild(array $schedule): bool {
|
||||
$lines[] = "# Regenerated: " . date('Y-m-d H:i:s');
|
||||
$lines[] = "";
|
||||
|
||||
// Build child→orch map so we can suppress a child's independent cron when its orch is enabled.
|
||||
$childToOrch = [];
|
||||
$confRaw = file_get_contents(CONF_DIR . '/master.conf') ?: '';
|
||||
foreach (glob(SCRIPTS_DIR . '/Orchestrators/*.sh') ?: [] as $orchPath) {
|
||||
$orchId = 'Orchestrators/' . basename($orchPath);
|
||||
$content = file_get_contents($orchPath) ?: '';
|
||||
preg_match_all('/\$[A-Z_]+\/(?:\.\.\/)?([A-Za-z][A-Za-z0-9_.\-]*\/[A-Za-z0-9_.\-]+\.sh)/', $content, $m1);
|
||||
foreach ($m1[1] as $rel) $childToOrch[$rel] = $orchId;
|
||||
preg_match_all('/\$\{([A-Z_]+_SCRIPTS)\[@\]\}/', $content, $refs);
|
||||
foreach (array_unique($refs[1] ?? []) as $var) {
|
||||
foreach (vv_parse_conf_array_full($confRaw, $var) as $item) $childToOrch[$item['path']] = $orchId;
|
||||
}
|
||||
}
|
||||
|
||||
$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.
|
||||
// Event-triggered jobs are handled by static event scripts, not cron.
|
||||
if (str_starts_with($entry['cron'], '@array_')) continue;
|
||||
$script = "$scriptsDir/{$entry['id']}";
|
||||
$id = $entry['id'];
|
||||
// When a child's orch is enabled it is the sole trigger — suppress independent cron.
|
||||
if (isset($childToOrch[$id]) && !empty($schedule[$childToOrch[$id]]['enabled'])) continue;
|
||||
$script = "$scriptsDir/$id";
|
||||
$flags = !empty($entry['log_enabled']) ? ' --log' : '';
|
||||
$id = $entry['id'];
|
||||
$lines[] = "{$entry['cron']} bash \"$runner\" \"$id\" \"$script\"$flags";
|
||||
}
|
||||
$lines[] = "";
|
||||
@@ -265,6 +281,77 @@ function vv_parse_conf_array(string $conf, string $varName): array {
|
||||
return $scripts;
|
||||
}
|
||||
|
||||
// Like vv_parse_conf_array but includes commented entries.
|
||||
// Returns array of ['path' => string, 'enabled' => bool].
|
||||
function vv_parse_conf_array_full(string $conf, string $varName): array {
|
||||
if (!preg_match('/^\s*' . preg_quote($varName, '/') . '\s*=\s*\((.*?)^\s*\)/ms', $conf, $m)) {
|
||||
return [];
|
||||
}
|
||||
$results = [];
|
||||
foreach (explode("\n", $m[1]) as $line) {
|
||||
if (!preg_match('/^\s*(#\s*)?"([^"]+)"/', $line, $e)) continue;
|
||||
$commented = trim($e[1]) !== '';
|
||||
$parts = preg_split('/\s+/', trim($e[2]));
|
||||
$path = $parts[0] ?? '';
|
||||
if (substr($path, -3) !== '.sh') continue;
|
||||
$results[] = ['path' => $path, 'enabled' => !$commented];
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
// Build a map of script rel-path → conf status by scanning all *_SCRIPTS arrays in master.conf.
|
||||
// Cached per-request so multiple callers only read the file once.
|
||||
function vv_conf_script_map(): array {
|
||||
static $cache = null;
|
||||
if ($cache !== null) return $cache;
|
||||
$confPath = CONF_DIR . '/master.conf';
|
||||
if (!file_exists($confPath)) return $cache = [];
|
||||
$lines = file($confPath, FILE_IGNORE_NEW_LINES) ?: [];
|
||||
$map = [];
|
||||
$inArray = false;
|
||||
$arrayVar = '';
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/^\s*([A-Z_]+_SCRIPTS)\s*=\s*\(/', $line, $am)) { $inArray = true; $arrayVar = $am[1]; }
|
||||
if ($inArray && preg_match('/^\s*\)\s*(?:#.*)?$/', $line) && !str_contains($line, '(')) $inArray = false;
|
||||
if (!$inArray) continue;
|
||||
if (!preg_match('/^\s*(#\s*)?"([^"]+)"/', $line, $e)) continue;
|
||||
$commented = trim($e[1]) !== '';
|
||||
$parts = preg_split('/\s+/', trim($e[2]));
|
||||
$path = $parts[0] ?? '';
|
||||
if (substr($path, -3) !== '.sh') continue;
|
||||
if (!isset($map[$path])) $map[$path] = ['array' => $arrayVar, 'enabled' => !$commented, 'managed' => true];
|
||||
}
|
||||
return $cache = $map;
|
||||
}
|
||||
|
||||
// Comment or uncomment a script's line in the first master.conf array that contains it.
|
||||
function vv_conf_toggle_script(string $rel, bool $enable): bool {
|
||||
$confPath = CONF_DIR . '/master.conf';
|
||||
$lines = file($confPath, FILE_KEEP_BLANK_LINES);
|
||||
if (!$lines) return false;
|
||||
$changed = false;
|
||||
$inArray = false;
|
||||
$relEsc = preg_quote($rel, '/');
|
||||
foreach ($lines as &$line) {
|
||||
if (preg_match('/^\s*[A-Z_]+_SCRIPTS\s*=\s*\(/', $line)) $inArray = true;
|
||||
if ($inArray && preg_match('/^\s*\)\s*(?:#.*)?$/', $line) && !str_contains($line, '(')) $inArray = false;
|
||||
if (!$inArray) continue;
|
||||
if (!preg_match('/^\s*(?:#\s*)?"' . $relEsc . '(?:\s[^"]*)?"/', $line)) continue;
|
||||
$isCommented = (bool)preg_match('/^\s*#/', $line);
|
||||
if ($enable && $isCommented) {
|
||||
$line = preg_replace('/^(\s*)#\s*("' . $relEsc . ')/', '$1$2', $line);
|
||||
$changed = true;
|
||||
} elseif (!$enable && !$isCommented) {
|
||||
$line = preg_replace('/^(\s*)("' . $relEsc . ')/', '$1# $2', $line);
|
||||
$changed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
unset($line);
|
||||
if (!$changed) return true;
|
||||
return file_put_contents($confPath, implode('', $lines)) !== false;
|
||||
}
|
||||
|
||||
// Parse an orchestrator script to find which child scripts it calls.
|
||||
// Two strategies, merged and deduped:
|
||||
// 1. Static paths: $SCRIPT_DIR/../Category/script.sh or $SCRIPTS_ROOT/Category/script.sh
|
||||
@@ -276,19 +363,23 @@ function vv_script_children(string $orchPath, array $schedule): array {
|
||||
$content = file_get_contents($orchPath) ?: '';
|
||||
$children = [];
|
||||
$seen = [];
|
||||
$confMap = vv_conf_script_map();
|
||||
|
||||
$addChild = function(string $rel) use ($scriptsDir, $schedule, &$children, &$seen) {
|
||||
$addChild = function(string $rel) use ($scriptsDir, $schedule, $confMap, &$children, &$seen) {
|
||||
if (isset($seen[$rel]) || !file_exists("$scriptsDir/$rel")) return;
|
||||
$seen[$rel] = true;
|
||||
$entry = $schedule[$rel] ?? ['enabled' => false, 'cron' => ''];
|
||||
$children[] = [
|
||||
'id' => $rel,
|
||||
'label' => basename($rel, '.sh'),
|
||||
'desc' => vv_script_description("$scriptsDir/$rel"),
|
||||
'type' => 'script',
|
||||
'enabled' => (bool)($entry['enabled'] ?? false),
|
||||
'cron' => $entry['cron'] ?? '',
|
||||
'log_enabled' => (bool)($entry['log_enabled'] ?? false),
|
||||
$seen[$rel] = true;
|
||||
$entry = $schedule[$rel] ?? ['enabled' => false, 'cron' => ''];
|
||||
$conf = $confMap[$rel] ?? ['array' => null, 'enabled' => null, 'managed' => false];
|
||||
$children[] = [
|
||||
'id' => $rel,
|
||||
'label' => basename($rel, '.sh'),
|
||||
'desc' => vv_script_description("$scriptsDir/$rel"),
|
||||
'type' => 'script',
|
||||
'enabled' => (bool)($entry['enabled'] ?? false),
|
||||
'cron' => $entry['cron'] ?? '',
|
||||
'log_enabled' => (bool)($entry['log_enabled'] ?? false),
|
||||
'conf_managed' => $conf['managed'],
|
||||
'conf_enabled' => $conf['enabled'], // null if not in any *_SCRIPTS array
|
||||
];
|
||||
};
|
||||
|
||||
@@ -299,12 +390,12 @@ function vv_script_children(string $orchPath, array $schedule): array {
|
||||
);
|
||||
foreach ($m[1] as $rel) $addChild($rel);
|
||||
|
||||
// Strategy 2: master.conf arrays — find every ${VARNAME_SCRIPTS[@]} the orch iterates
|
||||
// Strategy 2: master.conf arrays — includes commented (disabled) entries so they appear in the UI
|
||||
preg_match_all('/\$\{([A-Z_]+_SCRIPTS)\[@\]\}/', $content, $refs);
|
||||
if (!empty($refs[1])) {
|
||||
$confRaw = file_get_contents(CONF_DIR . '/master.conf') ?: '';
|
||||
foreach (array_unique($refs[1]) as $varName) {
|
||||
foreach (vv_parse_conf_array($confRaw, $varName) as $rel) $addChild($rel);
|
||||
foreach (vv_parse_conf_array_full($confRaw, $varName) as $item) $addChild($item['path']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,11 +28,11 @@ foreach ($tree as $orch) {
|
||||
<?php foreach ($tree as $orch): $oid = htmlspecialchars($orch['id']); ?>
|
||||
<div class="vv-card vv-wide vv-sched-card" data-id="<?= $oid ?>">
|
||||
|
||||
<div class="vv-job-row">
|
||||
<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="vvSaveJob(this)">
|
||||
onchange="vvSaveOrch(this)">
|
||||
<span class="vv-slider"></span>
|
||||
</label>
|
||||
<span class="vv-job-label"><?= htmlspecialchars($orch['label']) ?></span>
|
||||
@@ -71,17 +71,19 @@ foreach ($tree as $orch) {
|
||||
<?php if (!empty($orch['children'])): ?>
|
||||
<div class="vv-children" style="display:none;">
|
||||
<?php foreach ($orch['children'] as $child): $cid = htmlspecialchars($child['id']); ?>
|
||||
<div class="vv-script" data-id="<?= $cid ?>">
|
||||
<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' : '') ?>">
|
||||
<div class="vv-job-row">
|
||||
<label class="vv-toggle" title="Enable/disable">
|
||||
<input type="checkbox" class="vv-enabled"
|
||||
<?= $child['enabled'] ? 'checked' : '' ?>
|
||||
onchange="vvSaveJob(this)">
|
||||
onchange="vvSaveChild(this)">
|
||||
<span class="vv-slider"></span>
|
||||
</label>
|
||||
<span class="vv-job-label"><?= htmlspecialchars($child['label']) ?></span>
|
||||
<input type="text" class="vv-cron" value="<?= htmlspecialchars($child['cron']) ?>"
|
||||
placeholder="cron expression">
|
||||
placeholder="cron (orch off only)">
|
||||
<span class="vv-save-check"></span>
|
||||
</div>
|
||||
<?php if (!empty($child['desc'])): ?>
|
||||
@@ -608,26 +610,94 @@ function vvFlashStatus(el, msg, ok) {
|
||||
el._t = setTimeout(() => { el.textContent = ''; }, 3000);
|
||||
}
|
||||
|
||||
// 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 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 confManaged = child.dataset.confManaged === '1';
|
||||
const confEnabled = child.dataset.confEnabled === '1';
|
||||
const toggle = child.querySelector('.vv-enabled');
|
||||
const cronInput = child.querySelector('input.vv-cron');
|
||||
|
||||
if (orchEnabled) {
|
||||
// Orch is god: set toggle from master.conf state; hide independent cron
|
||||
toggle.checked = confManaged ? confEnabled : false;
|
||||
toggle.disabled = !confManaged;
|
||||
if (cronInput) { cronInput.disabled = true; cronInput.style.opacity = '0.35'; }
|
||||
} else {
|
||||
// Orch off: all children switch off; cron field becomes active
|
||||
toggle.checked = false;
|
||||
toggle.disabled = false;
|
||||
if (cronInput) { cronInput.disabled = false; cronInput.style.opacity = ''; }
|
||||
// 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.
|
||||
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 (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 vvSaveAll() {
|
||||
const status = document.getElementById('vv-save-all-status');
|
||||
const jobs = document.querySelectorAll('#vv-sched-left [data-id]');
|
||||
let pending = jobs.length, allOk = true;
|
||||
if (!pending) { vvFlashStatus(status, '✗ No jobs found', false); return; }
|
||||
// Collect jobs to save: orchs always; children only when their orch is OFF (orch manages them when on).
|
||||
const toSave = [];
|
||||
document.querySelectorAll('#vv-sched-left [data-id]').forEach(job => {
|
||||
if (job.classList.contains('vv-script')) {
|
||||
const orchCard = job.closest('.vv-sched-card');
|
||||
const orchOn = orchCard?.querySelector('.vv-orch-row .vv-enabled')?.checked ?? false;
|
||||
if (orchOn) return; // child under active orch — cron suppressed, skip
|
||||
}
|
||||
toSave.push(job);
|
||||
});
|
||||
let pending = toSave.length, allOk = true;
|
||||
if (!pending) { vvFlashStatus(status, '✓ Saved', true); return; }
|
||||
status.textContent = 'Saving…';
|
||||
jobs.forEach(job => {
|
||||
toSave.forEach(job => {
|
||||
const id = job.dataset.id;
|
||||
const enabled = job.querySelector('.vv-enabled').checked ? '1' : '0';
|
||||
const cron = job.querySelector('.vv-cron').value.trim();
|
||||
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 => {
|
||||
@@ -860,6 +930,12 @@ function vvToggleSugBlock(bi) {
|
||||
</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();
|
||||
const last = localStorage.getItem('vv-last-job');
|
||||
|
||||
Reference in New Issue
Block a user