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:
Gmer4Lfe
2026-05-24 22:24:14 -04:00
parent 3186097941
commit d0d56ed8b9
3 changed files with 208 additions and 27 deletions
@@ -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');