Compare commits
13
Commits
253d5bf2e1
...
6b30768853
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b30768853 | ||
|
|
8e0d68665c | ||
|
|
83ff9fca5e | ||
|
|
83195ebb9e | ||
|
|
dfb462a18e | ||
|
|
d6cfcaef46 | ||
|
|
eade6072b0 | ||
|
|
cdd2dfc990 | ||
|
|
d0d56ed8b9 | ||
|
|
3186097941 | ||
|
|
4832dd8d4e | ||
|
|
655c520ae8 | ||
|
|
1540c62d3a |
@@ -344,6 +344,22 @@ done
|
|||||||
|
|
||||||
END=$(date +%s)
|
END=$(date +%s)
|
||||||
|
|
||||||
|
# ==============================================================================================
|
||||||
|
# ━━━ Prune Old Images ━━━
|
||||||
|
# ==============================================================================================
|
||||||
|
# Restarts above swap containers onto new images — old images are now dangling. Prune immediately.
|
||||||
|
echo ""
|
||||||
|
echo "━━━ $ICON_SYNC Pruning Dangling Images — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
warn "DRY RUN — would prune dangling images"
|
||||||
|
PRUNED_SUMMARY="(dry run)"
|
||||||
|
else
|
||||||
|
PRUNED_OUTPUT=$(docker image prune -f 2>&1)
|
||||||
|
[[ "$ENABLE_LOGGING" == "true" ]] && echo "$PRUNED_OUTPUT" | sed 's/^/ /'
|
||||||
|
PRUNED_SUMMARY=$(echo "$PRUNED_OUTPUT" | grep -E "^Total reclaimed" || echo "nothing reclaimed")
|
||||||
|
fi
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ━━━ Summary ━━━
|
# ━━━ Summary ━━━
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
@@ -354,6 +370,7 @@ echo "$ICON_CONTAINERS Scope: ${#RESTARTED[@]} restarted, ${#SKIPPED[@]} skipp
|
|||||||
[[ ${#RESTARTED[@]} -gt 0 ]] && log "$ICON_STARTED Restarted: ${RESTARTED[*]}"
|
[[ ${#RESTARTED[@]} -gt 0 ]] && log "$ICON_STARTED Restarted: ${RESTARTED[*]}"
|
||||||
[[ ${#SKIPPED[@]} -gt 0 ]] && log "$ICON_NOT_RUNNING Skipped: ${SKIPPED[*]}"
|
[[ ${#SKIPPED[@]} -gt 0 ]] && log "$ICON_NOT_RUNNING Skipped: ${SKIPPED[*]}"
|
||||||
[[ ${#FAILED[@]} -gt 0 ]] && echo "$ICON_ERROR Failed: ${FAILED[*]}"
|
[[ ${#FAILED[@]} -gt 0 ]] && echo "$ICON_ERROR Failed: ${FAILED[*]}"
|
||||||
|
echo "$ICON_SYNC Pruned: ${PRUNED_SUMMARY:-none}"
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == true ]]; then
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
warn "DRY RUN — no changes made"
|
warn "DRY RUN — no changes made"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
# DAILY_RESTART_CONTAINERS — already updated daily
|
# DAILY_RESTART_CONTAINERS — already updated daily
|
||||||
# emby + critical-data profiles — updated inline by the weekly sync window
|
# emby + critical-data profiles — updated inline by the weekly sync window
|
||||||
# FALLBACK_*_TIER* — owned by the remote server's update cycle
|
# FALLBACK_*_TIER* — owned by the remote server's update cycle
|
||||||
# Pull → compare → restart if updated → prune dangling images.
|
# Pull → compare → prune dangling images.
|
||||||
#
|
#
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# DESIGN PRINCIPLES
|
# DESIGN PRINCIPLES
|
||||||
|
|||||||
@@ -571,12 +571,6 @@ for candidate in "${!CANDIDATE_SCORE[@]}"; do
|
|||||||
days_ago=0
|
days_ago=0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if (( seed_count < 2 )); then
|
|
||||||
log " $ICON_SKIP Single-seed candidate skipped: $candidate"
|
|
||||||
REJECT_LIST+=("0|${candidate}|${seed_count}|0|single-seed skip")
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
affinity_s=$(_affinity_score "$raw_score")
|
affinity_s=$(_affinity_score "$raw_score")
|
||||||
breadth_s=$(_breadth_score "$seed_count")
|
breadth_s=$(_breadth_score "$seed_count")
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,14 @@
|
|||||||
# 1. Server uptime >= MONTHLY_UPTIME_THRESHOLD_DAYS days
|
# 1. Server uptime >= MONTHLY_UPTIME_THRESHOLD_DAYS days
|
||||||
# 2. Last run was >= MONTHLY_RUN_INTERVAL_DAYS days ago (or never run)
|
# 2. Last run was >= MONTHLY_RUN_INTERVAL_DAYS days ago (or never run)
|
||||||
#
|
#
|
||||||
# ── WHY UPTIME-TRIGGERED, NOT CRON ───────────────────────────────────────────────────────────
|
# ── WHY UPTIME-GATED ─────────────────────────────────────────────────────────────────────────
|
||||||
# A scheduled reboot resets uptime. Monthly tasks (ZFS scrub, SMART long test) need a
|
# A scheduled reboot resets uptime. Monthly tasks (ZFS scrub, SMART long test) need a
|
||||||
# stable, settled system — not one that just rebooted. Uptime-gating ensures maintenance
|
# stable, settled system — not one that just rebooted. Uptime-gating ensures maintenance
|
||||||
# only runs after the server has been healthy for a full month, never immediately post-boot.
|
# only runs after the server has been healthy for a full month, never immediately post-boot.
|
||||||
# The daily cron is just the trigger mechanism. The uptime and interval checks inside
|
# If uptime or interval gate is not met on the 15th, the run is skipped until next month.
|
||||||
# the script are what enforce the monthly cadence.
|
|
||||||
#
|
#
|
||||||
# ── HOW TO CALL ──────────────────────────────────────────────────────────────────────────────
|
# ── HOW TO CALL ──────────────────────────────────────────────────────────────────────────────
|
||||||
# Cron: 0 3 * * * — daily 3am check. Script self-gates — calling it daily is safe.
|
# Schedule: 0 0 15 * * (15th of each month at midnight)
|
||||||
# Silent exit 0 when either gate is not met. Only outputs when maintenance actually fires.
|
# Silent exit 0 when either gate is not met. Only outputs when maintenance actually fires.
|
||||||
#
|
#
|
||||||
# ── STATE FILE ────────────────────────────────────────────────────────────────────────────────
|
# ── STATE FILE ────────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -153,7 +152,7 @@ if [[ "$SHOW_STATUS" == true ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " Schedule: 0 3 * * * (daily check — script self-gates)"
|
echo " Schedule: 0 0 15 * * (15th of each month at midnight)"
|
||||||
echo " Force flag: --force bypasses both gates"
|
echo " Force flag: --force bypasses both gates"
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -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']);
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||||
|
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$enabled = ($_POST['enabled'] ?? '0') === '1';
|
||||||
|
|
||||||
|
if (!$name || !preg_match('/^[A-Z_]+_RSYNC_ENABLED$/', $name)) {
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Invalid flag name']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ok = vv_conf_flag_set($name, $enabled);
|
||||||
|
echo json_encode(['ok' => $ok, 'error' => $ok ? null : 'Failed to write master.conf']);
|
||||||
@@ -12,8 +12,9 @@ if (!$id) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basic cron validation — 5 fields or empty
|
// Basic cron validation — 5 fields, or known @event trigger, or empty
|
||||||
if ($cron && !preg_match('/^(\S+\s+){4}\S+$/', $cron)) {
|
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']);
|
echo json_encode(['ok' => false, 'error' => 'Invalid cron expression']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||||
|
|
||||||
|
$id = trim($_POST['id'] ?? '');
|
||||||
|
|
||||||
|
if (!$id || !preg_match('/^[a-zA-Z0-9_.\/\-]+\.sh$/', $id) || str_contains($id, '..')) {
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Invalid id']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$statFile = vv_job_stat_path($id);
|
||||||
|
if (!file_exists($statFile)) {
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'No stat file — script may not be running']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stat = json_decode(file_get_contents($statFile) ?: '{}', true) ?: [];
|
||||||
|
|
||||||
|
if (($stat['status'] ?? '') !== 'running') {
|
||||||
|
echo json_encode(['ok' => true, 'msg' => 'Not running']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pid = (int)($stat['pid'] ?? 0);
|
||||||
|
if ($pid < 2) {
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'No valid PID in stat file']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill the whole process group so the script and all its children die together.
|
||||||
|
// pgid is usually the same as the session leader PID from run_job.sh.
|
||||||
|
$pgid = (int)trim(shell_exec("ps -o pgid= -p $pid 2>/dev/null") ?: '0');
|
||||||
|
|
||||||
|
if ($pgid > 1) {
|
||||||
|
shell_exec("kill -TERM -$pgid 2>/dev/null");
|
||||||
|
} else {
|
||||||
|
// Fallback: kill the direct PID and its children
|
||||||
|
shell_exec("pkill -TERM -P $pid 2>/dev/null");
|
||||||
|
shell_exec("kill -TERM $pid 2>/dev/null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give it up to 3s to exit gracefully
|
||||||
|
$dead = false;
|
||||||
|
for ($i = 0; $i < 6; $i++) {
|
||||||
|
usleep(500000);
|
||||||
|
if (!file_exists("/proc/$pid")) { $dead = true; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force-kill if still alive
|
||||||
|
if (!$dead) {
|
||||||
|
if ($pgid > 1) shell_exec("kill -KILL -$pgid 2>/dev/null");
|
||||||
|
shell_exec("pkill -KILL -P $pid 2>/dev/null");
|
||||||
|
shell_exec("kill -KILL $pid 2>/dev/null");
|
||||||
|
usleep(300000);
|
||||||
|
$dead = !file_exists("/proc/$pid");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any lock files in /tmp/unraid_locks whose content matches this PID
|
||||||
|
$lockDir = '/tmp/unraid_locks';
|
||||||
|
$cleared = [];
|
||||||
|
foreach (glob("$lockDir/*.lock") ?: [] as $lf) {
|
||||||
|
$content = trim(file_get_contents($lf) ?: '');
|
||||||
|
$lockPid = (int)explode(':', $content)[0];
|
||||||
|
if ($lockPid === $pid || !file_exists("/proc/$lockPid")) {
|
||||||
|
@unlink($lf);
|
||||||
|
$cleared[] = basename($lf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also clear by script name in case PID rotated
|
||||||
|
$scriptBase = basename($id, '.sh');
|
||||||
|
$namedLock = "$lockDir/{$scriptBase}.lock";
|
||||||
|
if (file_exists($namedLock)) {
|
||||||
|
@unlink($namedLock);
|
||||||
|
if (!in_array(basename($namedLock), $cleared)) $cleared[] = basename($namedLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update stat file
|
||||||
|
$now = time();
|
||||||
|
$stat['status'] = 'stopped';
|
||||||
|
$stat['end'] = $now;
|
||||||
|
$stat['exit'] = -1;
|
||||||
|
unset($stat['pid']);
|
||||||
|
file_put_contents($statFile, json_encode($stat));
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'killed' => $dead,
|
||||||
|
'locks' => $cleared,
|
||||||
|
]);
|
||||||
@@ -57,6 +57,16 @@
|
|||||||
cursor: default; }
|
cursor: default; }
|
||||||
.vv-cron { flex: 0 0 110px; width: 110px; background: #111; border: 1px solid #444; color: #ddd;
|
.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; }
|
padding: 4px 6px; border-radius: 4px; font-family: monospace; font-size: 13px; }
|
||||||
|
.vv-cron-hint { font-size: 11px; font-family: monospace; color: #444; cursor: pointer;
|
||||||
|
white-space: nowrap; flex-shrink: 0; padding: 3px 5px; border-radius: 3px;
|
||||||
|
user-select: none; transition: color 0.15s; }
|
||||||
|
.vv-cron-hint:hover { color: #888; background: rgba(255,255,255,0.04); }
|
||||||
|
.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-flag-badge { flex: 0 0 auto; padding: 2px 6px; border-radius: 3px; font-size: 11px;
|
||||||
|
background: #2e2200; border: 1px solid #6b4e00; color: #d4a017;
|
||||||
|
white-space: nowrap; font-family: monospace; }
|
||||||
.vv-log-label { display: flex; align-items: center; gap: 4px; font-size: 12px; color: #888;
|
.vv-log-label { display: flex; align-items: center; gap: 4px; font-size: 12px; color: #888;
|
||||||
cursor: pointer; white-space: nowrap; flex-shrink: 0; }
|
cursor: pointer; white-space: nowrap; flex-shrink: 0; }
|
||||||
.vv-log-label input { cursor: pointer; accent-color: #4caf50; }
|
.vv-log-label input { cursor: pointer; accent-color: #4caf50; }
|
||||||
@@ -189,6 +199,16 @@
|
|||||||
font-size: 11px; white-space: nowrap; }
|
font-size: 11px; white-space: nowrap; }
|
||||||
.vv-sug-path { color: #888; font-family: monospace; }
|
.vv-sug-path { color: #888; font-family: monospace; }
|
||||||
.vv-sug-configured { color: #4caf50; font-size: 11px; }
|
.vv-sug-configured { color: #4caf50; font-size: 11px; }
|
||||||
|
/* Info sections inside Scheduler Information panel */
|
||||||
|
.vv-info-block .vv-sug-title { color: #9ab; }
|
||||||
|
.vv-info-body { padding: 2px 10px 10px; }
|
||||||
|
.vv-info-cols { margin: 0; padding-left: 16px; columns: 2; column-gap: 20px; column-fill: balance; }
|
||||||
|
.vv-info-cols li { font-size: 12px; color: #aaa; margin-bottom: 5px; break-inside: avoid; line-height: 1.5; }
|
||||||
|
.vv-info-cols li strong { color: #ccc; }
|
||||||
|
.vv-info-cols code { background: #1a1a1a; padding: 0 4px; border-radius: 2px;
|
||||||
|
font-size: 11px; color: #9ab; border: 1px solid #333; }
|
||||||
|
.vv-info-divider { font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; color: #444;
|
||||||
|
padding: 10px 12px 4px; border-top: 1px solid #2a2a2a; margin-top: 2px; }
|
||||||
|
|
||||||
/* Log panel */
|
/* Log panel */
|
||||||
.vv-log-panel { margin-top: 10px; border-top: 1px solid #333; padding-top: 8px; }
|
.vv-log-panel { margin-top: 10px; border-top: 1px solid #333; padding-top: 8px; }
|
||||||
|
|||||||
+13
@@ -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
|
||||||
+13
@@ -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
|
||||||
@@ -54,12 +54,30 @@ function vv_cron_rebuild(array $schedule): bool {
|
|||||||
$lines[] = "# Regenerated: " . date('Y-m-d H:i:s');
|
$lines[] = "# Regenerated: " . date('Y-m-d H:i:s');
|
||||||
$lines[] = "";
|
$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;
|
$scriptsDir = SCRIPTS_DIR;
|
||||||
foreach ($schedule as $entry) {
|
foreach ($schedule as $entry) {
|
||||||
if (empty($entry['enabled']) || empty($entry['cron']) || empty($entry['id'])) continue;
|
if (empty($entry['enabled']) || empty($entry['cron']) || empty($entry['id'])) continue;
|
||||||
$script = "$scriptsDir/{$entry['id']}";
|
// Event-triggered jobs are handled by static event scripts, not cron.
|
||||||
$flags = !empty($entry['log_enabled']) ? ' --log' : '';
|
if (str_starts_with($entry['cron'], '@array_')) continue;
|
||||||
$id = $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' : '';
|
||||||
$lines[] = "{$entry['cron']} bash \"$runner\" \"$id\" \"$script\"$flags";
|
$lines[] = "{$entry['cron']} bash \"$runner\" \"$id\" \"$script\"$flags";
|
||||||
}
|
}
|
||||||
$lines[] = "";
|
$lines[] = "";
|
||||||
@@ -193,18 +211,42 @@ function vv_custom_scripts(): array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Walk the scripts repo and return the job tree:
|
// 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 {
|
function vv_job_tree(): array {
|
||||||
$scriptsDir = SCRIPTS_DIR;
|
$scriptsDir = SCRIPTS_DIR;
|
||||||
$schedule = vv_schedule_load();
|
$schedule = vv_schedule_load();
|
||||||
|
|
||||||
// Orchestrators are in Orchestrators/ and their children are all scripts they call
|
// Hardcoded array-event entries — always present, trigger via Unraid event scripts
|
||||||
// For now: walk top-level folders, treat *_management.sh or *_maintenance.sh as orchs
|
$eventDefs = [
|
||||||
$orchPattern = "$scriptsDir/Orchestrators/*.sh";
|
['id' => 'Orchestrators/array_started.sh', 'cron' => '@array_start', 'label' => 'Array Starting'],
|
||||||
|
['id' => 'Orchestrators/array_stopping.sh', 'cron' => '@array_stop', 'label' => 'Array Stopping'],
|
||||||
|
];
|
||||||
$orchs = [];
|
$orchs = [];
|
||||||
|
$eventIds = [];
|
||||||
|
foreach ($eventDefs as $ev) {
|
||||||
|
$id = $ev['id'];
|
||||||
|
$path = "$scriptsDir/$id";
|
||||||
|
$entry = $schedule[$id] ?? [];
|
||||||
|
$eventIds[] = $id;
|
||||||
|
$orchs[] = [
|
||||||
|
'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) {
|
foreach (glob($orchPattern) ?: [] as $path) {
|
||||||
$id = 'Orchestrators/' . basename($path);
|
$id = 'Orchestrators/' . basename($path);
|
||||||
|
if (in_array($id, $eventIds, true)) continue;
|
||||||
$entry = $schedule[$id] ?? ['enabled' => false, 'cron' => ''];
|
$entry = $schedule[$id] ?? ['enabled' => false, 'cron' => ''];
|
||||||
$suggested = vv_script_suggested_cron($path);
|
$suggested = vv_script_suggested_cron($path);
|
||||||
$orchs[] = [
|
$orchs[] = [
|
||||||
@@ -239,6 +281,101 @@ function vv_parse_conf_array(string $conf, string $varName): array {
|
|||||||
return $scripts;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a boolean flag value (e.g. INTERMEDIATE_RSYNC_ENABLED) from master.conf.
|
||||||
|
function vv_conf_flag_value(string $name): bool {
|
||||||
|
$conf = file_get_contents(CONF_DIR . '/master.conf') ?: '';
|
||||||
|
if (preg_match('/^\s*' . preg_quote($name, '/') . '\s*=\s*(true|false)\s*$/m', $conf, $m)) {
|
||||||
|
return $m[1] === 'true';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write a boolean flag value to master.conf.
|
||||||
|
function vv_conf_flag_set(string $name, bool $value): bool {
|
||||||
|
$confPath = CONF_DIR . '/master.conf';
|
||||||
|
$content = file_get_contents($confPath);
|
||||||
|
if ($content === false) return false;
|
||||||
|
$val = $value ? 'true' : 'false';
|
||||||
|
$new = preg_replace(
|
||||||
|
'/^(\s*' . preg_quote($name, '/') . '\s*=\s*)(true|false)(\s*(?:#.*)?)$/m',
|
||||||
|
'${1}' . $val . '${3}',
|
||||||
|
$content, -1, $count
|
||||||
|
);
|
||||||
|
if (!$count) return false;
|
||||||
|
return file_put_contents($confPath, $new) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
// Parse an orchestrator script to find which child scripts it calls.
|
||||||
// Two strategies, merged and deduped:
|
// Two strategies, merged and deduped:
|
||||||
// 1. Static paths: $SCRIPT_DIR/../Category/script.sh or $SCRIPTS_ROOT/Category/script.sh
|
// 1. Static paths: $SCRIPT_DIR/../Category/script.sh or $SCRIPTS_ROOT/Category/script.sh
|
||||||
@@ -250,11 +387,20 @@ function vv_script_children(string $orchPath, array $schedule): array {
|
|||||||
$content = file_get_contents($orchPath) ?: '';
|
$content = file_get_contents($orchPath) ?: '';
|
||||||
$children = [];
|
$children = [];
|
||||||
$seen = [];
|
$seen = [];
|
||||||
|
$confMap = vv_conf_script_map();
|
||||||
|
|
||||||
$addChild = function(string $rel) use ($scriptsDir, $schedule, &$children, &$seen) {
|
// Detect which tier rsync flag this orch controls (e.g. "INTERMEDIATE" → INTERMEDIATE_RSYNC_ENABLED)
|
||||||
|
$rsyncFlagName = null;
|
||||||
|
if (preg_match('/check_rsync_enabled\s+"([A-Z]+)"/', $content, $rm)) {
|
||||||
|
$rsyncFlagName = $rm[1] . '_RSYNC_ENABLED';
|
||||||
|
}
|
||||||
|
|
||||||
|
$addChild = function(string $rel) use ($scriptsDir, $schedule, $confMap, &$children, &$seen) {
|
||||||
if (isset($seen[$rel]) || !file_exists("$scriptsDir/$rel")) return;
|
if (isset($seen[$rel]) || !file_exists("$scriptsDir/$rel")) return;
|
||||||
$seen[$rel] = true;
|
$seen[$rel] = true;
|
||||||
$entry = $schedule[$rel] ?? ['enabled' => false, 'cron' => ''];
|
$entry = $schedule[$rel] ?? ['enabled' => false, 'cron' => ''];
|
||||||
|
$conf = $confMap[$rel] ?? ['array' => null, 'enabled' => null, 'managed' => false];
|
||||||
|
$suggested = vv_script_suggested_cron("$scriptsDir/$rel");
|
||||||
$children[] = [
|
$children[] = [
|
||||||
'id' => $rel,
|
'id' => $rel,
|
||||||
'label' => basename($rel, '.sh'),
|
'label' => basename($rel, '.sh'),
|
||||||
@@ -263,6 +409,10 @@ function vv_script_children(string $orchPath, array $schedule): array {
|
|||||||
'enabled' => (bool)($entry['enabled'] ?? false),
|
'enabled' => (bool)($entry['enabled'] ?? false),
|
||||||
'cron' => $entry['cron'] ?? '',
|
'cron' => $entry['cron'] ?? '',
|
||||||
'log_enabled' => (bool)($entry['log_enabled'] ?? false),
|
'log_enabled' => (bool)($entry['log_enabled'] ?? false),
|
||||||
|
'conf_managed' => $conf['managed'],
|
||||||
|
'conf_enabled' => $conf['enabled'], // null if not in any *_SCRIPTS array
|
||||||
|
'suggested_cron' => $suggested['cron'],
|
||||||
|
'suggested_label' => $suggested['label'],
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -273,14 +423,27 @@ function vv_script_children(string $orchPath, array $schedule): array {
|
|||||||
);
|
);
|
||||||
foreach ($m[1] as $rel) $addChild($rel);
|
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);
|
preg_match_all('/\$\{([A-Z_]+_SCRIPTS)\[@\]\}/', $content, $refs);
|
||||||
if (!empty($refs[1])) {
|
if (!empty($refs[1])) {
|
||||||
$confRaw = file_get_contents(CONF_DIR . '/master.conf') ?: '';
|
$confRaw = file_get_contents(CONF_DIR . '/master.conf') ?: '';
|
||||||
foreach (array_unique($refs[1]) as $varName) {
|
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']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Annotate Rsync/rsync.sh as a conf_flag child if this orch controls a rsync tier flag
|
||||||
|
if ($rsyncFlagName) {
|
||||||
|
foreach ($children as &$c) {
|
||||||
|
if ($c['id'] === 'Rsync/rsync.sh') {
|
||||||
|
$c['type'] = 'conf_flag';
|
||||||
|
$c['flag_name'] = $rsyncFlagName;
|
||||||
|
$c['flag_value'] = vv_conf_flag_value($rsyncFlagName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($c);
|
||||||
|
}
|
||||||
|
|
||||||
return $children;
|
return $children;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ foreach ($tree as $orch) {
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<div id="vv-scheduler">
|
<div id="vv-scheduler">
|
||||||
<p class="vv-hint">Toggle saves immediately. Fill in cron expressions and hit
|
|
||||||
<strong>Save Schedule</strong> at the bottom. <strong>Run</strong> fires now;
|
|
||||||
<strong>Dry Run</strong> sets DRY_RUN=1. Log opens on the right.</p>
|
|
||||||
|
|
||||||
<div id="vv-sched-layout">
|
<div id="vv-sched-layout">
|
||||||
|
|
||||||
<!-- ── Left: script cards ── -->
|
<!-- ── Left: script cards ── -->
|
||||||
@@ -28,16 +24,29 @@ foreach ($tree as $orch) {
|
|||||||
<?php foreach ($tree as $orch): $oid = htmlspecialchars($orch['id']); ?>
|
<?php foreach ($tree as $orch): $oid = htmlspecialchars($orch['id']); ?>
|
||||||
<div class="vv-card vv-wide vv-sched-card" data-id="<?= $oid ?>">
|
<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">
|
<label class="vv-toggle" title="Enable/disable">
|
||||||
<input type="checkbox" class="vv-enabled"
|
<input type="checkbox" class="vv-enabled"
|
||||||
<?= $orch['enabled'] ? 'checked' : '' ?>
|
<?= $orch['enabled'] ? 'checked' : '' ?>
|
||||||
onchange="vvSaveJob(this)">
|
onchange="vvSaveOrch(this)">
|
||||||
<span class="vv-slider"></span>
|
<span class="vv-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
<span class="vv-job-label"><?= htmlspecialchars($orch['label']) ?></span>
|
<span class="vv-job-label"><?= htmlspecialchars($orch['label']) ?></span>
|
||||||
|
<?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: ?>
|
||||||
|
<?php if ($orch['suggested_cron']): ?>
|
||||||
|
<span class="vv-cron-hint"
|
||||||
|
data-cron="<?= htmlspecialchars($orch['suggested_cron']) ?>"
|
||||||
|
onclick="this.nextElementSibling.value=this.dataset.cron"
|
||||||
|
title="Suggested: <?= htmlspecialchars($orch['suggested_cron'] . ($orch['suggested_label'] ? ' — ' . $orch['suggested_label'] : '')) ?>">
|
||||||
|
<?= htmlspecialchars($orch['suggested_cron']) ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
<input type="text" class="vv-cron" value="<?= htmlspecialchars($orch['cron']) ?>"
|
<input type="text" class="vv-cron" value="<?= htmlspecialchars($orch['cron']) ?>"
|
||||||
placeholder="cron expression">
|
placeholder="cron expression">
|
||||||
|
<?php endif; ?>
|
||||||
<span class="vv-save-check"></span>
|
<span class="vv-save-check"></span>
|
||||||
</div>
|
</div>
|
||||||
<?php if (!empty($orch['desc'])): ?>
|
<?php if (!empty($orch['desc'])): ?>
|
||||||
@@ -66,17 +75,55 @@ foreach ($tree as $orch) {
|
|||||||
<?php if (!empty($orch['children'])): ?>
|
<?php if (!empty($orch['children'])): ?>
|
||||||
<div class="vv-children" style="display:none;">
|
<div class="vv-children" style="display:none;">
|
||||||
<?php foreach ($orch['children'] as $child): $cid = htmlspecialchars($child['id']); ?>
|
<?php foreach ($orch['children'] as $child): $cid = htmlspecialchars($child['id']); ?>
|
||||||
<div class="vv-script" data-id="<?= $cid ?>">
|
<?php if (($child['type'] ?? 'script') === 'conf_flag'): ?>
|
||||||
|
<div class="vv-script" data-id="<?= $cid ?>"
|
||||||
|
data-type="conf_flag"
|
||||||
|
data-flag-name="<?= htmlspecialchars($child['flag_name']) ?>"
|
||||||
|
data-flag-value="<?= !empty($child['flag_value']) ? '1' : '0' ?>">
|
||||||
|
<div class="vv-job-row">
|
||||||
|
<label class="vv-toggle" title="Toggle <?= htmlspecialchars($child['flag_name']) ?> in master.conf">
|
||||||
|
<input type="checkbox" class="vv-enabled"
|
||||||
|
<?= !empty($child['flag_value']) ? 'checked' : '' ?>
|
||||||
|
onchange="vvSaveChild(this)">
|
||||||
|
<span class="vv-slider"></span>
|
||||||
|
</label>
|
||||||
|
<span class="vv-job-label"><?= htmlspecialchars($child['label']) ?></span>
|
||||||
|
<span class="vv-flag-badge"><?= htmlspecialchars($child['flag_name']) ?></span>
|
||||||
|
<span class="vv-save-check"></span>
|
||||||
|
</div>
|
||||||
|
<?php if (!empty($child['desc'])): ?>
|
||||||
|
<div class="vv-job-desc" title="<?= htmlspecialchars($child['desc']) ?>"><?= htmlspecialchars($child['desc']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="vv-job-actions">
|
||||||
|
<button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">▶ Run</button>
|
||||||
|
<button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">▶ Dry Run</button>
|
||||||
|
<?php $childLog = vv_job_log_path($child['id']); ?>
|
||||||
|
<button class="vv-btn-sm vv-log-btn<?= (file_exists($childLog) && filesize($childLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>
|
||||||
|
<span class="vv-job-dot"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<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">
|
<div class="vv-job-row">
|
||||||
<label class="vv-toggle" title="Enable/disable">
|
<label class="vv-toggle" title="Enable/disable">
|
||||||
<input type="checkbox" class="vv-enabled"
|
<input type="checkbox" class="vv-enabled"
|
||||||
<?= $child['enabled'] ? 'checked' : '' ?>
|
<?= $child['enabled'] ? 'checked' : '' ?>
|
||||||
onchange="vvSaveJob(this)">
|
onchange="vvSaveChild(this)">
|
||||||
<span class="vv-slider"></span>
|
<span class="vv-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
<span class="vv-job-label"><?= htmlspecialchars($child['label']) ?></span>
|
<span class="vv-job-label"><?= htmlspecialchars($child['label']) ?></span>
|
||||||
|
<?php if (!empty($child['suggested_cron'])): ?>
|
||||||
|
<span class="vv-cron-hint"
|
||||||
|
data-cron="<?= htmlspecialchars($child['suggested_cron']) ?>"
|
||||||
|
onclick="this.nextElementSibling.value=this.dataset.cron"
|
||||||
|
title="Suggested: <?= htmlspecialchars($child['suggested_cron'] . (!empty($child['suggested_label']) ? ' — ' . $child['suggested_label'] : '')) ?>">
|
||||||
|
<?= htmlspecialchars($child['suggested_cron']) ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
<input type="text" class="vv-cron" value="<?= htmlspecialchars($child['cron']) ?>"
|
<input type="text" class="vv-cron" value="<?= htmlspecialchars($child['cron']) ?>"
|
||||||
placeholder="cron expression">
|
placeholder="cron (orch off only)">
|
||||||
<span class="vv-save-check"></span>
|
<span class="vv-save-check"></span>
|
||||||
</div>
|
</div>
|
||||||
<?php if (!empty($child['desc'])): ?>
|
<?php if (!empty($child['desc'])): ?>
|
||||||
@@ -99,6 +146,7 @@ foreach ($tree as $orch) {
|
|||||||
<span class="vv-job-dot"></span>
|
<span class="vv-job-dot"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -173,28 +221,85 @@ foreach ($tree as $orch) {
|
|||||||
<div style="display:flex;align-items:center;gap:8px;">
|
<div style="display:flex;align-items:center;gap:8px;">
|
||||||
<button id="vv-back-btn" class="vv-btn-sm" onclick="vvBackToSuggestions()" style="display:none">☰ Scheduler Info</button>
|
<button id="vv-back-btn" class="vv-btn-sm" onclick="vvBackToSuggestions()" style="display:none">☰ Scheduler Info</button>
|
||||||
<button id="vv-restore-btn" class="vv-btn-sm" onclick="vvRestoreLastLog()" style="display:none">← <span id="vv-restore-label"></span></button>
|
<button id="vv-restore-btn" class="vv-btn-sm" onclick="vvRestoreLastLog()" style="display:none">← <span id="vv-restore-label"></span></button>
|
||||||
<span id="vv-log-title" style="font-size:13px;font-weight:bold;color:#ccc;">Suggested Schedules</span>
|
<span id="vv-log-title" style="font-size:13px;font-weight:bold;color:#ccc;">Scheduler Information</span>
|
||||||
<span id="vv-log-dot" style="display:none;width:8px;height:8px;border-radius:50%;background:#4caf50;flex-shrink:0;"></span>
|
<span id="vv-log-dot" style="display:none;width:8px;height:8px;border-radius:50%;background:#4caf50;flex-shrink:0;"></span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;align-items:center;gap:12px;">
|
<div style="display:flex;align-items:center;gap:12px;">
|
||||||
<label id="vv-auto-scroll-label" class="vv-log-label" title="Auto-scroll to newest line" style="display:none">
|
<label id="vv-auto-scroll-label" class="vv-log-label" title="Auto-scroll to newest line (pauses when you scroll up, resumes at bottom)" style="display:none">
|
||||||
<input type="checkbox" id="vv-auto-scroll" checked> <span>Auto Scroll</span>
|
<input type="checkbox" id="vv-auto-scroll" checked> <span>Auto Scroll</span>
|
||||||
</label>
|
</label>
|
||||||
<label id="vv-scroll-lock-label" class="vv-log-label" title="Lock viewport — cannot scroll while active" style="display:none">
|
|
||||||
<input type="checkbox" id="vv-scroll-lock" onchange="vvToggleScrollLock(this)"> <span>Scroll Lock</span>
|
|
||||||
</label>
|
|
||||||
<label id="vv-invert-log-label" class="vv-log-label" title="Show newest lines at top" style="display:none">
|
<label id="vv-invert-log-label" class="vv-log-label" title="Show newest lines at top" style="display:none">
|
||||||
<input type="checkbox" id="vv-invert-log" onchange="vvToggleInvert(this)"> <span>Invert</span>
|
<input type="checkbox" id="vv-invert-log" onchange="vvToggleInvert(this)"> <span>Invert</span>
|
||||||
</label>
|
</label>
|
||||||
<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-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>
|
<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>
|
<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>
|
<button id="vv-clear-btn" class="vv-btn-sm" onclick="vvClearRightLog()" style="display:none">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Suggestions view (default) -->
|
<!-- Suggestions view (default) -->
|
||||||
<div id="vv-suggestions">
|
<div id="vv-suggestions">
|
||||||
|
|
||||||
|
<!-- ── Static info: Controls ── -->
|
||||||
|
<div class="vv-sug-block vv-info-block">
|
||||||
|
<div class="vv-sug-header" onclick="vvToggleSug(this)">
|
||||||
|
<span class="vv-sug-chevron">▾</span>
|
||||||
|
<span class="vv-sug-title">Controls</span>
|
||||||
|
</div>
|
||||||
|
<div class="vv-sug-body vv-info-body">
|
||||||
|
<ul class="vv-info-cols">
|
||||||
|
<li><strong>Toggle</strong> — saves to schedule.json immediately</li>
|
||||||
|
<li><strong>Cron</strong> — 5-field expression; hit <strong>Save Schedule</strong> to apply all at once</li>
|
||||||
|
<li><strong>Run</strong> — fires script immediately regardless of schedule</li>
|
||||||
|
<li><strong>Dry Run</strong> — same as Run with <code>DRY_RUN=1</code></li>
|
||||||
|
<li><strong>Log</strong> — opens script log in this panel; auto-scrolls to latest</li>
|
||||||
|
<li><strong>Stop</strong> — sends SIGTERM to process group → 3 s → SIGKILL; clears stuck locks</li>
|
||||||
|
<li><strong>Verbose</strong> — appends <code>--log</code> for detailed per-step output</li>
|
||||||
|
<li><strong>Config</strong> — edit host-scoped settings for scripts that support it</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Static info: Orchestrators ── -->
|
||||||
|
<div class="vv-sug-block vv-info-block">
|
||||||
|
<div class="vv-sug-header" onclick="vvToggleSug(this)">
|
||||||
|
<span class="vv-sug-chevron">▾</span>
|
||||||
|
<span class="vv-sug-title">Orchestrators</span>
|
||||||
|
</div>
|
||||||
|
<div class="vv-sug-body vv-info-body">
|
||||||
|
<ul class="vv-info-cols">
|
||||||
|
<li><strong>Enabled (ON)</strong> — orchestrator is the sole trigger; its cron fires it and it calls all children in order</li>
|
||||||
|
<li><strong>Disabled (OFF)</strong> — orchestrator never runs; all children switch off and crons are suppressed</li>
|
||||||
|
<li><strong>Child toggles (orch ON)</strong> — comment or uncomment the script's line in master.conf <code>*_SCRIPTS</code> array</li>
|
||||||
|
<li><strong>Child cron (orch ON)</strong> — ignored; orch is the only trigger while it is enabled</li>
|
||||||
|
<li><strong>Child cron (orch OFF)</strong> — enable a child and give it a cron to run it on its own schedule, independent of the orch</li>
|
||||||
|
<li><strong>⚡ Array Start / Stop</strong> — triggered by Unraid array events; no cron field; toggle enables or disables the handler</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Static info: Children & Advanced ── -->
|
||||||
|
<div class="vv-sug-block vv-info-block">
|
||||||
|
<div class="vv-sug-header" onclick="vvToggleSug(this)">
|
||||||
|
<span class="vv-sug-chevron">▾</span>
|
||||||
|
<span class="vv-sug-title">Children & Advanced</span>
|
||||||
|
</div>
|
||||||
|
<div class="vv-sug-body vv-info-body">
|
||||||
|
<ul class="vv-info-cols">
|
||||||
|
<li><strong>Advanced ▸</strong> — expand to see child scripts managed by the orchestrator</li>
|
||||||
|
<li><strong>Toggle (orch ON)</strong> — comments or uncomments the script entry in master.conf; determines whether the orch calls it</li>
|
||||||
|
<li><strong>Toggle (orch OFF)</strong> — has no effect without a cron; pair with a cron expression to run standalone</li>
|
||||||
|
<li><strong>Cron (orch OFF)</strong> — child runs on its own schedule; completely independent of the parent orchestrator</li>
|
||||||
|
<li><strong>Rsync child</strong> — shows amber <code>TIER_RSYNC_ENABLED</code> badge; toggle writes <code>true</code> / <code>false</code> directly to master.conf — controls whether rsync runs inside that orchestrator</li>
|
||||||
|
<li><strong>Rsync (orch OFF)</strong> — toggle is disabled; rsync flag only matters when the orchestrator is running</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vv-info-divider">Suggested Schedules</div>
|
||||||
|
|
||||||
<?php foreach ($blocks as $bi => $block):
|
<?php foreach ($blocks as $bi => $block):
|
||||||
$scripts = $block['scripts'];
|
$scripts = $block['scripts'];
|
||||||
$schedule = $block['schedule'];
|
$schedule = $block['schedule'];
|
||||||
@@ -365,8 +470,12 @@ function vvShowLogMode(id) {
|
|||||||
document.getElementById('vv-delete-script-btn').style.display = 'none';
|
document.getElementById('vv-delete-script-btn').style.display = 'none';
|
||||||
document.getElementById('vv-confform').style.display = 'none';
|
document.getElementById('vv-confform').style.display = 'none';
|
||||||
document.getElementById('vv-auto-scroll-label').style.display = '';
|
document.getElementById('vv-auto-scroll-label').style.display = '';
|
||||||
document.getElementById('vv-scroll-lock-label').style.display = '';
|
|
||||||
document.getElementById('vv-invert-log-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);
|
||||||
|
_pre.addEventListener('scroll', vvOnLogScroll);
|
||||||
document.getElementById('vv-save-schedule-btn').disabled = false;
|
document.getElementById('vv-save-schedule-btn').disabled = false;
|
||||||
document.getElementById('vv-save-schedule-btn').style.opacity = '';
|
document.getElementById('vv-save-schedule-btn').style.opacity = '';
|
||||||
const name = id.replace(/\.sh$/, '').split('/').pop();
|
const name = id.replace(/\.sh$/, '').split('/').pop();
|
||||||
@@ -379,17 +488,11 @@ function vvRestoreLastLog() {
|
|||||||
if (last && document.querySelector('[data-id="' + CSS.escape(last) + '"]')) vvOpenRight(last);
|
if (last && document.querySelector('[data-id="' + CSS.escape(last) + '"]')) vvOpenRight(last);
|
||||||
}
|
}
|
||||||
|
|
||||||
function vvToggleScrollLock(cb) {
|
function vvOnLogScroll() {
|
||||||
document.getElementById('vv-log-pre').style.overflowY = cb.checked ? 'hidden' : '';
|
const pre = document.getElementById('vv-log-pre');
|
||||||
localStorage.setItem('vv-scroll-lock', cb.checked ? '1' : '0');
|
const atBottom = pre.scrollHeight - pre.scrollTop - pre.clientHeight < 20;
|
||||||
}
|
const cb = document.getElementById('vv-auto-scroll');
|
||||||
|
if (cb) cb.checked = atBottom;
|
||||||
function vvRestoreScrollLock() {
|
|
||||||
const val = localStorage.getItem('vv-scroll-lock');
|
|
||||||
const on = val === null ? true : val === '1';
|
|
||||||
const cb = document.getElementById('vv-scroll-lock');
|
|
||||||
cb.checked = on;
|
|
||||||
document.getElementById('vv-log-pre').style.overflowY = on ? 'hidden' : '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function vvToggleInvert(cb) {
|
function vvToggleInvert(cb) {
|
||||||
@@ -429,11 +532,13 @@ function vvBackToSuggestions() {
|
|||||||
document.getElementById('vv-restore-btn').style.display = '';
|
document.getElementById('vv-restore-btn').style.display = '';
|
||||||
}
|
}
|
||||||
document.getElementById('vv-auto-scroll-label').style.display = 'none';
|
document.getElementById('vv-auto-scroll-label').style.display = 'none';
|
||||||
document.getElementById('vv-scroll-lock-label').style.display = 'none';
|
|
||||||
document.getElementById('vv-invert-log-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;
|
document.getElementById('vv-auto-scroll').checked = true;
|
||||||
vvRestoreScrollLock();
|
const _preBack = document.getElementById('vv-log-pre');
|
||||||
document.getElementById('vv-log-title').textContent = 'Suggested Schedules';
|
_preBack.removeEventListener('scroll', vvOnLogScroll);
|
||||||
|
_preBack.style.overflowY = '';
|
||||||
|
document.getElementById('vv-log-title').textContent = 'Scheduler Information';
|
||||||
document.getElementById('vv-log-ts').textContent = '';
|
document.getElementById('vv-log-ts').textContent = '';
|
||||||
document.getElementById('vv-log-dot').style.display = 'none';
|
document.getElementById('vv-log-dot').style.display = 'none';
|
||||||
requestAnimationFrame(vvFitRight);
|
requestAnimationFrame(vvFitRight);
|
||||||
@@ -450,6 +555,7 @@ function vvOpenRight(id) {
|
|||||||
if (job) job.querySelector('.vv-job-row').classList.add('vv-row-selected');
|
if (job) job.querySelector('.vv-job-row').classList.add('vv-row-selected');
|
||||||
|
|
||||||
vvShowLogMode(id);
|
vvShowLogMode(id);
|
||||||
|
vvSetStopBtn(vvRunningSet.has(id));
|
||||||
requestAnimationFrame(vvFitRight);
|
requestAnimationFrame(vvFitRight);
|
||||||
|
|
||||||
vvFetchRight();
|
vvFetchRight();
|
||||||
@@ -464,16 +570,13 @@ function vvFetchRight() {
|
|||||||
const pre = document.getElementById('vv-log-pre');
|
const pre = document.getElementById('vv-log-pre');
|
||||||
const ts = document.getElementById('vv-log-ts');
|
const ts = document.getElementById('vv-log-ts');
|
||||||
const autoScroll = document.getElementById('vv-auto-scroll').checked;
|
const autoScroll = document.getElementById('vv-auto-scroll').checked;
|
||||||
const scrollLock = document.getElementById('vv-scroll-lock').checked;
|
|
||||||
const invert = document.getElementById('vv-invert-log').checked;
|
const invert = document.getElementById('vv-invert-log').checked;
|
||||||
const savedScroll = pre.scrollTop;
|
const savedScroll = pre.scrollTop;
|
||||||
if (!d.ok) { pre.textContent = '✗ ' + (d.error ?? 'Error'); return; }
|
if (!d.ok) { pre.textContent = '✗ ' + (d.error ?? 'Error'); return; }
|
||||||
const content = d.content || '';
|
const content = d.content || '';
|
||||||
vvSetLogBtnState(vvActiveId, content.trim().length > 0);
|
vvSetLogBtnState(vvActiveId, content.trim().length > 0);
|
||||||
pre.textContent = content.trim() ? (invert ? content.split('\n').reverse().join('\n') : content) : '(no log yet)';
|
pre.textContent = content.trim() ? (invert ? content.split('\n').reverse().join('\n') : content) : '(no log yet)';
|
||||||
if (scrollLock) {
|
if (autoScroll) {
|
||||||
requestAnimationFrame(() => { pre.scrollTop = savedScroll; });
|
|
||||||
} else if (autoScroll) {
|
|
||||||
pre.scrollTop = invert ? 0 : pre.scrollHeight;
|
pre.scrollTop = invert ? 0 : pre.scrollHeight;
|
||||||
} else {
|
} else {
|
||||||
requestAnimationFrame(() => { pre.scrollTop = savedScroll; });
|
requestAnimationFrame(() => { pre.scrollTop = savedScroll; });
|
||||||
@@ -483,16 +586,51 @@ function vvFetchRight() {
|
|||||||
.catch(() => {});
|
.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) {
|
function vvSetDot(id) {
|
||||||
const job = document.querySelector('[data-id="' + CSS.escape(id) + '"]');
|
const job = document.querySelector('[data-id="' + CSS.escape(id) + '"]');
|
||||||
if (job) job.querySelector('.vv-job-dot').classList.add('vv-dot-running');
|
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) {
|
function vvClearDot(id) {
|
||||||
const job = document.querySelector('[data-id="' + CSS.escape(id) + '"]');
|
const job = document.querySelector('[data-id="' + CSS.escape(id) + '"]');
|
||||||
if (job) job.querySelector('.vv-job-dot').classList.remove('vv-dot-running');
|
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() {
|
function vvPollStatus() {
|
||||||
@@ -572,26 +710,114 @@ function vvFlashStatus(el, msg, ok) {
|
|||||||
el._t = setTimeout(() => { el.textContent = ''; }, 3000);
|
el._t = setTimeout(() => { el.textContent = ''; }, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generic save — used for log_enabled toggles and custom scripts.
|
||||||
function vvSaveJob(el) {
|
function vvSaveJob(el) {
|
||||||
const job = el.closest('[data-id]');
|
const job = el.closest('[data-id]');
|
||||||
const id = job.dataset.id;
|
const id = job.dataset.id;
|
||||||
const enabled = job.querySelector('.vv-enabled').checked ? '1' : '0';
|
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';
|
const log_enabled = job.querySelector('.vv-log-enabled')?.checked ? '1' : '0';
|
||||||
vvPost('/plugins/varaverk/api/scheduler.php', {id, enabled, cron, log_enabled})
|
vvPost('/plugins/varaverk/api/scheduler.php', {id, enabled, cron, log_enabled})
|
||||||
.then(d => { if (d.ok) vvFlashSaved(job); });
|
.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 toggle = child.querySelector('.vv-enabled');
|
||||||
|
const cronInput = child.querySelector('input.vv-cron');
|
||||||
|
|
||||||
|
// conf_flag children (e.g. Rsync): toggle reflects real flag value, enabled only when orch is on
|
||||||
|
if (child.dataset.type === 'conf_flag') {
|
||||||
|
toggle.checked = child.dataset.flagValue === '1';
|
||||||
|
toggle.disabled = !orchEnabled;
|
||||||
|
if (cronInput) { cronInput.disabled = true; cronInput.style.opacity = '0.35'; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confManaged = child.dataset.confManaged === '1';
|
||||||
|
const confEnabled = child.dataset.confEnabled === '1';
|
||||||
|
|
||||||
|
if (orchEnabled) {
|
||||||
|
// Orch is god: set toggle from master.conf state; dim 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.
|
||||||
|
// conf_flag children always write directly to master.conf regardless of orch state.
|
||||||
|
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 (child.dataset.type === 'conf_flag') {
|
||||||
|
const name = child.dataset.flagName;
|
||||||
|
child.dataset.flagValue = enabled ? '1' : '0';
|
||||||
|
vvPost('/plugins/varaverk/api/flag_toggle.php', {name, enabled: enabled ? '1' : '0'})
|
||||||
|
.then(d => { if (d.ok) vvFlashSaved(child); });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
function vvSaveAll() {
|
||||||
const status = document.getElementById('vv-save-all-status');
|
const status = document.getElementById('vv-save-all-status');
|
||||||
const jobs = document.querySelectorAll('#vv-sched-left [data-id]');
|
// Collect jobs to save: orchs always; children only when their orch is OFF (orch manages them when on).
|
||||||
let pending = jobs.length, allOk = true;
|
// conf_flag children are always immediate-save (flag_toggle.php), never batch-saved.
|
||||||
if (!pending) { vvFlashStatus(status, '✗ No jobs found', false); return; }
|
const toSave = [];
|
||||||
|
document.querySelectorAll('#vv-sched-left [data-id]').forEach(job => {
|
||||||
|
if (job.classList.contains('vv-script')) {
|
||||||
|
if (job.dataset.type === 'conf_flag') return; // immediate-save only
|
||||||
|
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…';
|
status.textContent = 'Saving…';
|
||||||
jobs.forEach(job => {
|
toSave.forEach(job => {
|
||||||
const id = job.dataset.id;
|
const id = job.dataset.id;
|
||||||
const enabled = job.querySelector('.vv-enabled').checked ? '1' : '0';
|
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';
|
const log_enabled = job.querySelector('.vv-log-enabled')?.checked ? '1' : '0';
|
||||||
vvPost('/plugins/varaverk/api/scheduler.php', {id, enabled, cron, log_enabled})
|
vvPost('/plugins/varaverk/api/scheduler.php', {id, enabled, cron, log_enabled})
|
||||||
.then(d => {
|
.then(d => {
|
||||||
@@ -671,8 +897,8 @@ function vvShowEditorMode(title) {
|
|||||||
document.getElementById('vv-restore-btn').style.display = 'none';
|
document.getElementById('vv-restore-btn').style.display = 'none';
|
||||||
document.getElementById('vv-save-script-btn').style.display = '';
|
document.getElementById('vv-save-script-btn').style.display = '';
|
||||||
document.getElementById('vv-clear-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-auto-scroll-label').style.display = 'none';
|
||||||
document.getElementById('vv-scroll-lock-label').style.display = 'none';
|
|
||||||
document.getElementById('vv-invert-log-label').style.display = 'none';
|
document.getElementById('vv-invert-log-label').style.display = 'none';
|
||||||
document.getElementById('vv-log-title').textContent = title;
|
document.getElementById('vv-log-title').textContent = title;
|
||||||
document.getElementById('vv-log-ts').textContent = '';
|
document.getElementById('vv-log-ts').textContent = '';
|
||||||
@@ -731,8 +957,8 @@ function vvShowConfMode(title) {
|
|||||||
document.getElementById('vv-save-conf-btn').style.display = '';
|
document.getElementById('vv-save-conf-btn').style.display = '';
|
||||||
document.getElementById('vv-delete-script-btn').style.display = 'none';
|
document.getElementById('vv-delete-script-btn').style.display = 'none';
|
||||||
document.getElementById('vv-clear-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-auto-scroll-label').style.display = 'none';
|
||||||
document.getElementById('vv-scroll-lock-label').style.display = 'none';
|
|
||||||
document.getElementById('vv-invert-log-label').style.display = 'none';
|
document.getElementById('vv-invert-log-label').style.display = 'none';
|
||||||
document.getElementById('vv-log-title').textContent = title;
|
document.getElementById('vv-log-title').textContent = title;
|
||||||
document.getElementById('vv-log-ts').textContent = '';
|
document.getElementById('vv-log-ts').textContent = '';
|
||||||
@@ -821,23 +1047,32 @@ function vvToggleSugBlock(bi) {
|
|||||||
body.style.display = open ? 'none' : 'block';
|
body.style.display = open ? 'none' : 'block';
|
||||||
chevron.textContent = open ? '▸' : '▾';
|
chevron.textContent = open ? '▸' : '▾';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function vvToggleSug(header) {
|
||||||
|
const body = header.nextElementSibling;
|
||||||
|
const chevron = header.querySelector('.vv-sug-chevron');
|
||||||
|
const open = body.style.display !== 'none';
|
||||||
|
body.style.display = open ? 'none' : '';
|
||||||
|
chevron.textContent = open ? '▸' : '▾';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<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() {
|
requestAnimationFrame(function() {
|
||||||
vvRestoreScrollLock();
|
|
||||||
vvRestoreInvert();
|
vvRestoreInvert();
|
||||||
const last = localStorage.getItem('vv-last-job');
|
const last = localStorage.getItem('vv-last-job');
|
||||||
if (last && document.querySelector('[data-id="' + CSS.escape(last) + '"]')) {
|
|
||||||
vvOpenRight(last);
|
|
||||||
} else {
|
|
||||||
if (last) {
|
if (last) {
|
||||||
const lname = last.replace(/\.sh$/, '').split('/').pop();
|
const lname = last.replace(/\.sh$/, '').split('/').pop();
|
||||||
document.getElementById('vv-restore-label').textContent = lname;
|
document.getElementById('vv-restore-label').textContent = lname;
|
||||||
document.getElementById('vv-restore-btn').style.display = '';
|
document.getElementById('vv-restore-btn').style.display = '';
|
||||||
}
|
}
|
||||||
vvFitRight();
|
vvFitRight();
|
||||||
}
|
|
||||||
vvStartStatusPoll();
|
vvStartStatusPoll();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Regular → Executable
@@ -26,7 +26,7 @@
|
|||||||
# sunday_morning_coffee_report.sh 7am Sunday — full weekly digest
|
# sunday_morning_coffee_report.sh 7am Sunday — full weekly digest
|
||||||
# weekly_health_digest.sh 8am daily — profile-controlled health notification
|
# weekly_health_digest.sh 8am daily — profile-controlled health notification
|
||||||
# system_tuning_monitor.sh every 6 hours — inotify + php-fpm trend tracking
|
# system_tuning_monitor.sh every 6 hours — inotify + php-fpm trend tracking
|
||||||
# monthly_maintenance.sh 3am daily — uptime-gated monthly tasks (self-gates to monthly)
|
# monthly_maintenance.sh 15th monthly — uptime-gated heavy tasks (ZFS scrub, SMART long test)
|
||||||
#
|
#
|
||||||
# ── INDIVIDUAL SCRIPTS ────────────────────────────────────────────────────────────────────────
|
# ── INDIVIDUAL SCRIPTS ────────────────────────────────────────────────────────────────────────
|
||||||
# Every child script is also listed below, individually.
|
# Every child script is also listed below, individually.
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
# before rsync_stop) — fallback runs as a bare subprocess, not via /tmp/user.scripts,
|
# before rsync_stop) — fallback runs as a bare subprocess, not via /tmp/user.scripts,
|
||||||
# so it is not caught by user_scripts_stop.sh.
|
# so it is not caught by user_scripts_stop.sh.
|
||||||
# v2.6 — monthly_maintenance.sh: uptime-gated orchestrator for heavy monthly tasks (ZFS scrub,
|
# v2.6 — monthly_maintenance.sh: uptime-gated orchestrator for heavy monthly tasks (ZFS scrub,
|
||||||
# SMART long test). Schedule 0 3 * * * — script self-gates to monthly cadence.
|
# SMART long test). Schedule 0 0 15 * * — 15th of each month at midnight.
|
||||||
# storage_watchdog.sh + network_watchdog.sh: pool growth + connectivity watchdogs,
|
# storage_watchdog.sh + network_watchdog.sh: pool growth + connectivity watchdogs,
|
||||||
# called via SYSTEM_WATCHDOG_SCRIPTS by system_watchdog.sh (watchdog chain, no cron).
|
# called via SYSTEM_WATCHDOG_SCRIPTS by system_watchdog.sh (watchdog chain, no cron).
|
||||||
# webgui_watchdog.sh moved from standalone cron to SYSTEM_WATCHDOG_SCRIPTS chain.
|
# webgui_watchdog.sh moved from standalone cron to SYSTEM_WATCHDOG_SCRIPTS chain.
|
||||||
@@ -346,17 +346,16 @@
|
|||||||
|
|
||||||
|
|
||||||
# ── MONTHLY MAINTENANCE ───────────────────────────────────────────────────────────────────────
|
# ── MONTHLY MAINTENANCE ───────────────────────────────────────────────────────────────────────
|
||||||
# Schedule: 0 3 * * * (daily 3am check — script self-gates to monthly cadence)
|
# Schedule: 0 0 15 * * (15th of each month at midnight)
|
||||||
# Background: YES
|
# Background: YES
|
||||||
#
|
#
|
||||||
# Runs heavy tasks that require a long-stable, settled system. Fires only when BOTH gates pass:
|
# Runs heavy tasks that require a long-stable, settled system. Fires only when BOTH gates pass:
|
||||||
# 1. Server uptime >= MONTHLY_UPTIME_THRESHOLD_DAYS (default: 30 days)
|
# 1. Server uptime >= MONTHLY_UPTIME_THRESHOLD_DAYS (default: 30 days)
|
||||||
# 2. Last run was >= MONTHLY_RUN_INTERVAL_DAYS ago (default: 30 days)
|
# 2. Last run was >= MONTHLY_RUN_INTERVAL_DAYS ago (default: 30 days)
|
||||||
#
|
#
|
||||||
# Why uptime-gated not cron: a scheduled reboot resets uptime. Monthly tasks (ZFS scrub,
|
# If either gate is not met on the 15th, the run is skipped until next month's 15th.
|
||||||
# SMART extended tests) need a system that has been healthy for a full month — not one that
|
# A reboot between the 14th and 15th will defer maintenance — which is intentional;
|
||||||
# just rebooted. The daily 3am cron is just the trigger. The gates inside enforce the cadence.
|
# heavy tasks (ZFS scrub, SMART long test) should not run on a freshly rebooted system.
|
||||||
# Silent exit 0 when either gate is not met — calling daily is always safe.
|
|
||||||
#
|
#
|
||||||
# Configure via master.conf MONTHLY_MAINTENANCE_SCRIPTS. Scripts planned but not yet built:
|
# Configure via master.conf MONTHLY_MAINTENANCE_SCRIPTS. Scripts planned but not yet built:
|
||||||
# zfs_pool_scrub.sh ZFS pool integrity scrub (Tools/)
|
# zfs_pool_scrub.sh ZFS pool integrity scrub (Tools/)
|
||||||
@@ -1436,7 +1435,7 @@
|
|||||||
# 0 1 * * * 1am daily:
|
# 0 1 * * * 1am daily:
|
||||||
# Orchestrators/daily_sync_maintenance.sh
|
# Orchestrators/daily_sync_maintenance.sh
|
||||||
#
|
#
|
||||||
# 0 3 * * * 3am daily (fires monthly — self-gated by uptime + interval):
|
# 0 0 15 * * 15th of each month at midnight:
|
||||||
# Orchestrators/monthly_maintenance.sh
|
# Orchestrators/monthly_maintenance.sh
|
||||||
#
|
#
|
||||||
# 0 7 * * 0 Sunday 7am:
|
# 0 7 * * 0 Sunday 7am:
|
||||||
|
|||||||
Reference in New Issue
Block a user