Varaverk: arrange mode, folder management, rsync standalone, layout fixes

Scheduler UI:
- Arrange mode: drag scripts between orchs and reorder within arrays; right
  panel shows unassigned script pool; Save Arrangement commits to master.conf
- + Folder: named collapsible subfolders for Custom Scripts stored in schedule.json
- Rsync children: hide Run/Dry Run/Log/location when orch is ON; show standalone
  location + cron controls when orch is OFF; cron only fires when both filled
- Non-conf-managed children (transcode): toggles now show enabled when orch is on
- Right panel height sync: fix ResizeObserver feedback loop via align-self:flex-start
  on left panel and left.offsetHeight in vvFitRight
- How do I use this: updated to cover arrange, folders, rsync standalone, transcode

New API endpoints:
- board.php, clearlock.php, movescript.php, rawconf.php, readscript.php
- reorderarray.php, rsync_standalone.php, savefolders.php

run.php / dryrun.php: accept optional --location= arg for standalone rsync calls
This commit is contained in:
Gmer4Lfe
2026-05-25 21:46:12 -04:00
parent 6b30768853
commit 6078af0dbb
17 changed files with 2667 additions and 196 deletions
@@ -82,6 +82,24 @@ function vv_cron_rebuild(array $schedule): bool {
}
$lines[] = "";
// Standalone rsync entries: fire when orch is disabled but location + cron are both configured.
$scriptsDir = SCRIPTS_DIR;
foreach ($schedule as $key => $entry) {
if (!str_starts_with((string)$key, '__rsync_')) continue;
$orchId = $entry['orch_id'] ?? '';
$location = $entry['location'] ?? '';
$cron = $entry['cron'] ?? '';
if (!$orchId || !$location || !$cron) continue;
// Skip if orch is still enabled
if (!empty($schedule[$orchId]['enabled'])) continue;
$rsyncScript = "$scriptsDir/Rsync/rsync.sh";
if (!file_exists($rsyncScript)) continue;
$locArg = escapeshellarg('--location=' . $location);
$logFlag = !empty($entry['log_enabled']) ? ' --log' : '';
$lines[] = "$cron bash \"$runner\" \"Rsync/rsync.sh\" \"$rsyncScript\" $locArg$logFlag";
}
$lines[] = "";
// Write to the plugin cron file; update_cron merges all plugin *.cron files into /etc/cron.d/root.
if (file_put_contents(CRON_FILE, implode("\n", $lines)) === false) return false;
exec('/usr/local/sbin/update_cron');
@@ -210,6 +228,59 @@ function vv_custom_scripts(): array {
return $scripts;
}
// Load rsync standalone config (location + cron) for a flag name from schedule.json.
function vv_rsync_standalone(string $flagName): array {
$s = vv_schedule_load();
$r = $s['__rsync_' . $flagName] ?? [];
return [
'location' => (string)($r['location'] ?? ''),
'cron' => (string)($r['cron'] ?? ''),
];
}
// Extract *_SCRIPTS array variable names that an orchestrator iterates over.
function vv_orch_conf_arrays(string $orchPath): array {
$content = file_get_contents($orchPath) ?: '';
preg_match_all('/\$\{([A-Z_]+_SCRIPTS)\[@\]\}/', $content, $refs);
return array_unique($refs[1] ?? []);
}
// Return .sh scripts that exist in SCRIPTS_DIR but are not referenced in any
// master.conf *_SCRIPTS array and are not orchestrators or custom scripts.
function vv_script_library(): array {
$scriptsDir = SCRIPTS_DIR;
$confMap = vv_conf_script_map();
$orchIds = [];
foreach (glob("$scriptsDir/Orchestrators/*.sh") ?: [] as $p) {
$orchIds[] = 'Orchestrators/' . basename($p);
}
$exclude = ['Plugin', '.git', 'Orchestrators', 'Custom', 'Configurations'];
$library = [];
try {
$ri = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($scriptsDir, RecursiveDirectoryIterator::SKIP_DOTS)
);
$base = rtrim($scriptsDir, '/') . '/';
foreach ($ri as $rf) {
if (!$rf->isFile() || strtolower($rf->getExtension()) !== 'sh') continue;
$rel = ltrim(str_replace($base, '', $rf->getPathname()), '/');
$parts = explode('/', $rel);
if (count($parts) < 2 || in_array($parts[0], $exclude)) continue;
if (in_array($rel, $orchIds) || isset($confMap[$rel])) continue;
$library[] = ['id' => $rel, 'label' => basename($rel, '.sh')];
}
} catch (Exception $e) {}
usort($library, fn($a, $b) => strcmp($a['id'], $b['id']));
return $library;
}
// Load custom-script folder assignments from schedule.json (__folders key).
function vv_folders_load(): array {
$s = vv_schedule_load();
$f = $s['__folders'] ?? [];
return is_array($f) ? $f : [];
}
// Walk the scripts repo and return the job tree:
// hardcoded array-event entries first, then cron-scheduled orchestrators
function vv_job_tree(): array {
@@ -260,6 +331,7 @@ function vv_job_tree(): array {
'suggested_cron' => $suggested['cron'],
'suggested_label' => $suggested['label'],
'children' => vv_script_children($path, $schedule),
'conf_arrays' => vv_orch_conf_arrays($path),
];
}
return $orchs;
@@ -411,6 +483,7 @@ function vv_script_children(string $orchPath, array $schedule): array {
'log_enabled' => (bool)($entry['log_enabled'] ?? false),
'conf_managed' => $conf['managed'],
'conf_enabled' => $conf['enabled'], // null if not in any *_SCRIPTS array
'conf_array' => $conf['array'],
'suggested_cron' => $suggested['cron'],
'suggested_label' => $suggested['label'],
];
@@ -447,3 +520,63 @@ function vv_script_children(string $orchPath, array $schedule): array {
return $children;
}
// Extract the comment header block from a bash script (shebang + all leading comment lines).
// Returns raw lines with # markers intact.
function vv_script_header(string $path): string {
if (!file_exists($path)) return '';
$lines = array_slice(file($path) ?: [], 0, 80);
$out = [];
foreach ($lines as $line) {
$t = rtrim($line);
if (str_starts_with($t, '#') || ($out === [] && str_starts_with($t, '#!'))) {
$out[] = $t;
} elseif ($t === '' && !empty($out)) {
$out[] = $t; // allow blank lines within header
} else {
break;
}
}
// Trim trailing blank lines
while (!empty($out) && trim(end($out)) === '') array_pop($out);
return implode("\n", $out);
}
// Strip the leading # marker from each line of a script header for cleaner display.
// Also drops the shebang line (#!/bin/bash) since it's not informative in this context.
function vv_script_header_clean(string $path): string {
$raw = vv_script_header($path);
if (!$raw) return '';
$lines = explode("\n", $raw);
$out = [];
foreach ($lines as $line) {
if (str_starts_with($line, '#!')) continue; // shebang — not useful in header display
$out[] = preg_replace('/^#\s?/', '', $line); // strip # and optional space
}
while (!empty($out) && trim(end($out)) === '') array_pop($out);
return implode("\n", $out);
}
// Read a named section from a markdown file.
// Calls $matcher(heading, isIntro) where isIntro=true for content before the first heading.
// Returns the first matching section body, capped at $maxChars.
function vv_readme_section(string $readmePath, callable $matcher, int $maxChars = 3000): string {
if (!file_exists($readmePath)) return '';
$content = file_get_contents($readmePath) ?: '';
$parts = preg_split('/^(#{1,4}[^\n]*)/m', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
$heading = '';
$isIntro = true;
foreach ($parts as $i => $part) {
if ($i % 2 === 1) {
$heading = trim(preg_replace('/^#{1,4}\s*/', '', $part));
$isIntro = false;
continue;
}
$body = trim($part);
if ($body === '') continue;
if ($matcher($heading, $isIntro)) {
return strlen($body) > $maxChars ? substr($body, 0, $maxChars) . "\n[…]" : $body;
}
}
return '';
}