$id, 'enabled' => $enabled, 'cron' => $cron, 'log_enabled' => $log_enabled, 'updated' => date('c'), ]; if (!vv_schedule_save($schedule)) return false; return vv_cron_rebuild($schedule); } function vv_job_flags(string $id): string { $schedule = vv_schedule_load(); return !empty($schedule[$id]['log_enabled']) ? '--log' : ''; } define('LOG_DIR', '/var/log/varaverk'); function vv_job_log_path(string $id): string { return LOG_DIR . '/' . preg_replace('/\.sh$/', '.log', $id); } function vv_cron_rebuild(array $schedule): bool { if (!is_dir(LOG_DIR)) mkdir(LOG_DIR, 0755, true); $lines = ["# Varaverk — managed by plugin, do not edit manually"]; $lines[] = "# Regenerated: " . date('Y-m-d H:i:s'); $lines[] = ""; $scriptsDir = SCRIPTS_DIR; foreach ($schedule as $entry) { if (empty($entry['enabled']) || empty($entry['cron']) || empty($entry['id'])) continue; $script = "$scriptsDir/{$entry['id']}"; $logFile = vv_job_log_path($entry['id']); $logDir = dirname($logFile); if (!is_dir($logDir)) mkdir($logDir, 0755, true); $flags = !empty($entry['log_enabled']) ? ' --log' : ''; $lines[] = "{$entry['cron']} bash -c 'echo; echo \"── \$(date \"+%Y-%m-%d %H:%M:%S\") ──────────────────────\"; bash \"$script\"$flags' >> \"$logFile\" 2>&1"; } $lines[] = ""; // dcron (dillon's cron, Unraid's system crond) reads /etc/cron.d/ but refuses // world-writable files and does not use a username field in system crontab format. if (file_put_contents(CRON_FILE, implode("\n", $lines)) === false) return false; chmod(CRON_FILE, 0600); return true; } // Extract the suggested cron and label from a bash script header. // Looks for: # Schedule: */7 * * * * (every 7 minutes via User Scripts plugin) function vv_script_suggested_cron(string $path): array { if (!file_exists($path)) return ['cron' => '', 'label' => '']; $lines = array_slice(file($path) ?: [], 0, 30); foreach ($lines as $raw) { $raw = rtrim($raw); if (!preg_match('/^#\s*Schedule:\s*(.+)$/i', $raw, $m)) continue; $tail = trim($m[1]); $parts = preg_split('/\s+/', $tail, 6); $cron = implode(' ', array_slice($parts, 0, 5)); $label = isset($parts[5]) ? trim($parts[5], '() ') : ''; return ['cron' => $cron, 'label' => $label]; } return ['cron' => '', 'label' => '']; } // Parse user_script_plug-in.sh into an array of script blocks. // Each block: title, schedule, desc (array of lines), scripts (array of {rel, cron}) function vv_parse_user_script_template(): array { $file = SCRIPTS_DIR . '/user_script_plug-in.sh'; if (!file_exists($file)) return []; $lines = file($file, FILE_IGNORE_NEW_LINES); $prefix = rtrim(SCRIPTS_DIR, '/') . '/'; $blocks = []; $cur = null; foreach ($lines as $line) { // Block header: # ── TITLE ──... if (preg_match('/^# ── (.+?) ─/', $line, $m)) { if ($cur) $blocks[] = $cur; $cur = ['title' => trim($m[1]), 'schedule' => '', 'desc' => [], 'scripts' => []]; continue; } if (!$cur) continue; // Schedule / Background — captured but not added to desc if (preg_match('/^# (Schedule|Background):\s+(.+)$/i', $line, $m)) { if (strtolower($m[1]) === 'schedule') $cur['schedule'] = trim($m[2]); continue; } // Sunday block inline cron: # 0 6 * * 0 bash /path/script.sh [args] if (preg_match('/^#\s+(\S+ +\S+ +\S+ +\S+ +\S+)\s+bash\s+(\S+\.sh)/', $line, $m)) { $rel = str_replace($prefix, '', trim($m[2])); $cur['scripts'][] = ['rel' => $rel, 'cron' => preg_replace('/\s+/', ' ', trim($m[1]))]; continue; } // Standard bash line: # bash /prefix/path/script.sh [args] if (preg_match('/^# bash\s+(\S+\.sh)/', $line, $m)) { $rel = str_replace($prefix, '', $m[1]); // Only add primary command (dedupe by rel) $rels = array_column($cur['scripts'], 'rel'); if (!in_array($rel, $rels)) { $cur['scripts'][] = ['rel' => $rel, 'cron' => '']; } continue; } // Description line if (preg_match('/^# ?(.*)$/', $line, $m)) { $inner = $m[1]; if (!preg_match('/^[─━=\-]{3,}\s*$/', $inner) && !preg_match('/^█/', $inner)) { $cur['desc'][] = $inner; } } } if ($cur) $blocks[] = $cur; return $blocks; } // Extract a one-line description from a bash script header. // Supports two patterns: // 1. # PURPOSE (or # DESCRIPTION) block — returns first non-separator line after it // 2. First meaningful comment line after the banner function vv_script_description(string $path): string { if (!file_exists($path)) return ''; $lines = array_slice(file($path) ?: [], 0, 50); $purposeNext = false; $first = ''; foreach ($lines as $raw) { $raw = rtrim($raw); if (str_starts_with($raw, '#!')) continue; // shebang if (!str_starts_with($raw, '#')) continue; // non-comment $inner = ltrim(substr($raw, 1)); // strip leading # if ($inner === '') continue; // blank if (preg_match('/^[\s=\-─━\*]+$/', $inner)) continue; // pure separator if (preg_match('/^={3,}/', $inner)) continue; // banner (=== Title ===) if (preg_match('/^\s*(PURPOSE|DESCRIPTION|Description)\s*$/i', $inner)) { $purposeNext = true; continue; } if ($purposeNext) { return mb_substr(trim($inner), 0, 200); } if (!$first) $first = mb_substr(trim($inner), 0, 200); } return $first; } function vv_custom_scripts(): array { $dir = SCRIPTS_DIR . '/Custom'; $schedule = vv_schedule_load(); $scripts = []; foreach (glob("$dir/*.sh") ?: [] as $path) { $rel = 'Custom/' . basename($path); $entry = $schedule[$rel] ?? []; $scripts[] = [ 'id' => $rel, 'label' => basename($path, '.sh'), 'desc' => vv_script_description($path), 'enabled' => (bool)($entry['enabled'] ?? false), 'cron' => $entry['cron'] ?? '', 'log_enabled' => (bool)($entry['log_enabled'] ?? false), ]; } return $scripts; } // Walk the scripts repo and return the job tree: // orchestrators as top-level, individual scripts as children 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); $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), ]; } return $orchs; } // Parse a bash array from master.conf content and return its script paths. // Handles entries with inline args ("script.sh --flag") and skips commented lines (#"..."). function vv_parse_conf_array(string $conf, string $varName): array { if (!preg_match('/^\s*' . preg_quote($varName, '/') . '\s*=\s*\((.*?)^\s*\)/ms', $conf, $m)) { return []; } $scripts = []; preg_match_all('/^\s*(?!#)"([^"]+)"/m', $m[1], $entries); foreach ($entries[1] as $entry) { $parts = preg_split('/\s+/', trim($entry)); $path = $parts[0] ?? ''; if (substr($path, -3) === '.sh') $scripts[] = $path; } return $scripts; } // 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 // 2. master.conf arrays: detects ${VARNAME[@]} iteration and reads the array from master.conf // Root-level files (load_config.sh etc.) excluded — must be in a subdirectory. // Each candidate validated against the filesystem. function vv_script_children(string $orchPath, array $schedule): array { $scriptsDir = SCRIPTS_DIR; $content = file_get_contents($orchPath) ?: ''; $children = []; $seen = []; $addChild = function(string $rel) use ($scriptsDir, $schedule, &$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), ]; }; // Strategy 1: static variable paths ($VAR/../Category/script.sh or $VAR/Category/script.sh) preg_match_all( '/\$[A-Z_]+\/(?:\.\.\/)?([A-Za-z][A-Za-z0-9_.\-]*\/[A-Za-z0-9_.\-]+\.sh)/', $content, $m ); foreach ($m[1] as $rel) $addChild($rel); // Strategy 2: master.conf arrays — find every ${VARNAME_SCRIPTS[@]} the orch iterates 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); } } return $children; }