Files
Varaverk/Plugin/unraid/include/confform.php
T
Gmer4Lfe 86048b32d3 Plugin: common.php library, watchdog expansion, monitor + scheduler improvements
PHP architecture:
- Extract common.php from monitor.php — shared system functions (vv_system_info,
  vv_memory_breakdown, vv_remote_hosts_stats, disk/GPU/UPS/network/docker/parity, etc.)
  now live in one place; monitor.php and watchdog.php both require common.php
- Add unraid_api.php as explicit include (was implicit via config.php chain)
- confform.php: add missing require_once config.php (implicit dep made explicit)
- Delete orphaned pages/docs.php and pages/config.php (absorbed into scheduler)

Watchdog page:
- Add Storage watchdog card (growth + log strikes, baseline age, suppress ceilings)
- Add Network watchdog card (NPM strikes, DDNS domain/container, NPM URL)
- One host per row layout — all 5 watchdog cards equally spaced via inner grid
- Watchdog now uses Unraid API for local system stats; remote nodes with API key
  but no SSH get system info from vv_remote_hosts_stats() with api_only flag
- SSH bundle: /proc/meminfo passed as raw section instead of awk-parsed header
  fields — fixes RAM showing 0 on remote hosts where awk quoting was unreliable
- vv_wd_local_system() rewritten as thin wrapper over vv_system_info() + vv_memory_breakdown()

Monitor page:
- Watchdog card: add stability strikes, storage watchdog strikes, network NPM status,
  live system stats (rootfs/log/tmp %, RAM free, load, CPU temp, zombies, NIC, sshd)
- Row height: switch from max-height on cards to minmax(0, calc(...)) on grid track —
  all cards in a row now fill to the tallest card's height correctly (fixes Pools card
  being shorter than neighbours)

Scheduler page:
- Add Tools section above Custom Scripts — lists Tools/*.sh with run/dry-run/cron/log
- vv_tools_scripts() function in scheduler.php include

Tools:
- Add docker_prune_images.sh — removes dangling Docker images; --dry-run and --status modes
2026-05-29 23:33:52 -04:00

212 lines
9.3 KiB
PHP

<?php
require_once __DIR__ . '/config.php';
// confform.php — script→conf-section mapping, field parsing, and write-back.
// Map: script relative id → subsection names (must match # ━━━ Name ━━━ or # ── Name ── headers).
const VV_SCRIPT_CONF_SECTIONS = [
// Orchestrators
'Orchestrators/array_started.sh' => ['Array Start'],
'Orchestrators/array_stopping.sh' => ['Array Stop'],
'Orchestrators/watchdog_orchestrator.sh' => ['Watchdog Orchestrator', 'System Watchdog'],
'Orchestrators/critical_sync_maintenance.sh' => ['Critical Sync Maintenance', 'Critical Sync Shares'],
'Orchestrators/intermediate_sync_maintenance.sh' => ['Intermediate Sync Maintenance', 'Intermediate Sync Shares'],
'Orchestrators/daily_sync_maintenance.sh' => ['Daily Sync Maintenance', 'Daily Sync Shares'],
'Orchestrators/weekly_sync_maintenance.sh' => ['Weekly Sync Maintenance', 'Weekly Sync Shares'],
'Orchestrators/monthly_maintenance.sh' => ['Monthly Maintenance'],
'Orchestrators/transcode_management.sh' => ['Transcode Manager', 'Transcode Server Array', 'Transcodes'],
// Docker Essentials
'Docker_Essentials/docker_daily_restart.sh' => ['Docker Daily Restart'],
'Docker_Essentials/docker_weekly_restart.sh' => ['Docker Weekly Restart'],
'Docker_Essentials/docker_network_connect.sh' => ['Docker Network Connect'],
'Docker_Essentials/downloaders_reset.sh' => ['Downloaders Reset', 'Downloaders'],
// Watchdogs
'Watchdogs/docker_watchdog.sh' => ['Docker Watchdog'],
'Watchdogs/resource_watchdog.sh' => ['Pressure Levels'],
'Watchdogs/System/network_watchdog.sh' => ['Network Watchdog'],
'Watchdogs/System/webgui_watchdog.sh' => ['WebGUI Watchdog'],
// Media
'Media/media_cleaner.sh' => ['Media Cleaner'],
'Media/media_shares_permissions.sh' => ['Media Permissions'],
'Media/arrs_failed_stalled_recovery.sh' => ['Arr Failed/Stalled Recovery'],
'Media/radarr_cleanup.sh' => ['Arr Cleanup'],
'Media/lidarr_cleanup.sh' => ['Arr Cleanup'],
'Media/sonarr_cleanup.sh' => ['Arr Cleanup'],
// Monitors
'Monitors/cert_monitor.sh' => ['Certificate Monitor'],
'Monitors/backup_verify.sh' => ['Backup Verify'],
'Monitors/smart_health.sh' => ['SMART Health'],
'Monitors/bandwidth_monitor.sh' => ['Bandwidth Monitor'],
'Monitors/emby_session_report.sh' => ['Emby Session Report'],
'Monitors/zfs_memory_snapshot.sh' => ['ZFS Report'],
];
function vv_conf_has_sections(string $id): bool {
return !empty(VV_SCRIPT_CONF_SECTIONS[$id] ?? []);
}
// Parse fields from a named subsection in raw conf content.
// Headers accepted: # ━━━ Name ━━━ OR # ── Name ── (any mix of ━ ─ chars).
// Returns array of field defs, or null if subsection not found.
function vv_conf_parse_subsection(string $raw, string $subName, string $filename): ?array {
$lines = explode("\n", $raw);
$n = count($lines);
$needle = mb_strtolower(trim(preg_replace('/\s+/', ' ', $subName)));
$start = -1;
for ($i = 0; $i < $n; $i++) {
if (!preg_match('/^#\s*[━─]{2,}\s+([A-Za-z].+?)\s+[━─]{2,}/', $lines[$i], $m)) continue;
$t = mb_strtolower(trim(preg_replace('/\s+/', ' ', $m[1])));
if ($t === $needle) { $start = $i + 1; break; }
}
if ($start === -1) return null;
// End at next section/subsection line (3+ consecutive divider chars after #)
$end = $n;
for ($i = $start; $i < $n; $i++) {
if (preg_match('/^#\s*[━─═=]{3,}/', $lines[$i])) { $end = $i; break; }
}
$fields = [];
$pendingDesc = [];
for ($i = $start; $i < $end; $i++) {
$line = rtrim($lines[$i]);
if ($line === '' || $line === '#') { $pendingDesc = []; continue; }
// Pure comment line
if (preg_match('/^#\s*(.*)$/', $line, $cm)) {
$inner = trim($cm[1]);
if ($inner !== '' && !preg_match('/^[━─═=\-\s]+$/', $inner)) {
$pendingDesc[] = $inner;
}
continue;
}
$desc = implode(' ', $pendingDesc);
$pendingDesc = [];
// declare -A KEY=(
if (preg_match('/^(\s*)declare\s+-A\s+([A-Z_][A-Z0-9_]*)\s*=\s*\(/', $line, $m)) {
$indent = $m[1]; $key = $m[2];
$blockLines = [];
$j = $i + 1;
while ($j < $end && !preg_match('/^\s*\)\s*$/', $lines[$j])) {
$blockLines[] = rtrim($lines[$j]);
$j++;
}
$fields[] = ['key' => $key, 'value' => implode("\n", $blockLines),
'type' => 'assoc_array', 'desc' => $desc, 'file' => $filename, 'indent' => $indent];
$i = $j;
continue;
}
// KEY=( (array)
if (preg_match('/^(\s*)([A-Z_][A-Z0-9_]*)\s*=\s*\(/', $line, $m)) {
$indent = $m[1]; $key = $m[2];
// Single-line: KEY=( ... )
if (preg_match('/^[^(]*\(([^)]*)\)/', $line, $sm)) {
$fields[] = ['key' => $key, 'value' => $sm[1],
'type' => 'array_single', 'desc' => $desc, 'file' => $filename, 'indent' => $indent];
continue;
}
// Multi-line
$blockLines = [];
$j = $i + 1;
while ($j < $end && !preg_match('/^\s*\)\s*$/', $lines[$j])) {
$blockLines[] = rtrim($lines[$j]);
$j++;
}
$fields[] = ['key' => $key, 'value' => implode("\n", $blockLines),
'type' => 'array', 'desc' => $desc, 'file' => $filename, 'indent' => $indent];
$i = $j;
continue;
}
// Scalar: KEY="value" or KEY=value
if (preg_match('/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*"([^"]*)"(?:\s+#\s*(.+))?$/', $line, $m)) {
$fields[] = ['key' => $m[1], 'value' => $m[2],
'type' => 'scalar', 'desc' => ($m[3] ?? '') ?: $desc, 'file' => $filename];
continue;
}
if (preg_match('/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*([^(\n#]*?)(?:\s+#\s*(.+))?$/', $line, $m)) {
$val = trim($m[2]);
if ($val === '') continue;
$fields[] = ['key' => $m[1], 'value' => $val,
'type' => 'scalar', 'desc' => ($m[3] ?? '') ?: $desc, 'file' => $filename];
}
}
return $fields ?: null;
}
// Return all conf groups (subsection + fields) for a script on the current host.
function vv_conf_fields_for_script(string $id): array {
$sectionNames = VV_SCRIPT_CONF_SECTIONS[$id] ?? [];
if (!$sectionNames) return [];
$groups = [];
foreach ($sectionNames as $name) {
foreach (vv_get_conf_files() as $filename) {
$fields = vv_conf_parse_subsection(vv_read_conf_raw($filename), $name, $filename);
if ($fields !== null) {
$groups[] = ['subsection' => $name, 'file' => $filename, 'fields' => $fields];
}
}
}
return $groups;
}
// Write a batch of field changes back to their respective conf files.
// Each change: {file, key, value, type}
function vv_conf_write_changes(array $changes): array {
$byFile = [];
foreach ($changes as $c) {
if (!empty($c['file']) && !empty($c['key'])) $byFile[$c['file']][] = $c;
}
$results = [];
foreach ($byFile as $file => $fileChanges) {
$raw = vv_read_conf_raw($file);
if ($raw === '') { $results[$file] = false; continue; }
foreach ($fileChanges as $c) {
$qKey = preg_quote($c['key'], '/');
$value = $c['value'];
$type = $c['type'] ?? 'scalar';
if ($type === 'scalar') {
$raw = preg_replace_callback(
'/^(\s*' . $qKey . '\s*=\s*)("(?:[^"\\\\]|\\\\.)*"|\'(?:[^\'\\\\]|\\\\.)*\'|[^#\n]*?)(\s*(?:#[^\n]*)?)$/m',
fn($m) => $m[1] . '"' . str_replace(['"', '\\'], ['\\"', '\\\\'], $value) . '"' . $m[3],
$raw
) ?? $raw;
} elseif ($type === 'array_single') {
$raw = preg_replace_callback(
'/^(\s*' . $qKey . '\s*=\s*\()([^)]*)(\)(?:\s*(?:#[^\n]*)?)?)$/m',
fn($m) => $m[1] . $value . $m[3],
$raw
) ?? $raw;
} elseif ($type === 'array') {
$raw = preg_replace_callback(
'/^(\s*)(' . $qKey . '\s*=\s*\()[^)]*\)/ms',
fn($m) => $m[1] . $m[2] . "\n" . $value . "\n" . $m[1] . ")",
$raw
) ?? $raw;
} elseif ($type === 'assoc_array') {
$raw = preg_replace_callback(
'/^(\s*)(declare\s+-A\s+' . $qKey . '\s*=\s*\()[^)]*\)/ms',
fn($m) => $m[1] . $m[2] . "\n" . $value . "\n" . $m[1] . ")",
$raw
) ?? $raw;
}
}
$results[$file] = vv_write_conf_raw($file, $raw);
}
return $results;
}