- WEBGUI_PHP_WAIT was referenced by webgui_watchdog.sh but never defined
in master.conf, always silently falling back to a hardcoded default
- arrs.php/confform.php still pointed at Media/ for arr cleanup/discovery
scripts moved to Arrs_Stack/ in b4bc926 — broke the Arrs page's stats
and the per-script settings editor for those scripts
- docker_folders.php read directly from the optional folder.view3 plugin's
file instead of Varaverk's own docker_folders.json (the primary store
since the Docker tab got its own config) — left the Monitor page's
Docker Folders widget empty on any host without folder.view3 installed
- vv_wd_remote_data() read remote watchdog state files from hardcoded
/tmp or /boot/config paths instead of the remote's actual STATE_DIR
(which resolves dynamically and can differ under flash mode) — remote
node's Watchdog panel was always empty; same wrong path also used for
two local reads (system_watchdog_oom.db, watchdog_appdata_growth.db)
- rsync.php referenced a {HOST}_MONTHLY_SYNC_SHARES conf var that never
existed (monthly_maintenance.sh has no rsync section) — nulled out to
match the existing pattern used for the fallback window
- vv_arr_node_names() did a pointless identity array_map
- vv_dk_webui() had its own duplicate local-IP resolution instead of
using vv_local_ip(), despite config.php's comment claiming that exact
duplication was already consolidated
397 lines
16 KiB
PHP
397 lines
16 KiB
PHP
<?php
|
|
// Docker tab — folder management and container inventory
|
|
// Primary store: /boot/config/plugins/varaverk/docker_folders.json (Varaverk-owned, no external deps)
|
|
// Optional sync: /boot/config/plugins/folder.view3/docker.json (only if folder.view3 is installed)
|
|
// Reads/writes HOST*_DOCKER_FOLDER_MAP in host*.conf (used by onboard scripts)
|
|
|
|
require_once __DIR__ . '/config.php';
|
|
|
|
define('VV_DOCKER_JSON', SCRIPTS_DIR . '/docker_folders.json');
|
|
define('VV_FV3_JSON', '/boot/config/plugins/folder.view3/docker.json');
|
|
|
|
// ── JSON helpers ──────────────────────────────────────────────────────────────
|
|
|
|
function vv_dk_read_json(): array {
|
|
// Bootstrap: if our file doesn't exist yet but folder.view3's does, import it once
|
|
if (!file_exists(VV_DOCKER_JSON) && file_exists(VV_FV3_JSON)) {
|
|
$imported = json_decode(file_get_contents(VV_FV3_JSON), true) ?: [];
|
|
vv_dk_write_json($imported);
|
|
return $imported;
|
|
}
|
|
if (!file_exists(VV_DOCKER_JSON)) return [];
|
|
return json_decode(file_get_contents(VV_DOCKER_JSON), true) ?: [];
|
|
}
|
|
|
|
function vv_dk_write_json(array $data): bool {
|
|
$path = VV_DOCKER_JSON;
|
|
@mkdir(dirname($path), 0755, true);
|
|
$tmp = $path . '.vv.tmp';
|
|
if (file_put_contents($tmp, json_encode($data, JSON_UNESCAPED_SLASHES)) === false) return false;
|
|
if (!rename($tmp, $path)) return false;
|
|
|
|
// Mirror to folder.view3 if installed — keeps both in sync for users who want both UIs
|
|
if (file_exists(dirname(VV_FV3_JSON))) {
|
|
$t = VV_FV3_JSON . '.vv.tmp';
|
|
@file_put_contents($t, json_encode($data, JSON_UNESCAPED_SLASHES));
|
|
@rename($t, VV_FV3_JSON);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function vv_dk_gen_id(): string {
|
|
return substr(str_replace(['+', '/', '='], ['', '', ''], base64_encode(random_bytes(15))), 0, 20);
|
|
}
|
|
|
|
// ── Conf helpers ──────────────────────────────────────────────────────────────
|
|
|
|
// Read HOST*_DOCKER_FOLDER_MAP from raw conf text.
|
|
// Returns ['ContainerName' => 'Folder Name', ...]
|
|
function vv_dk_read_conf_map(string $raw, string $id): array {
|
|
$varname = "{$id}_DOCKER_FOLDER_MAP";
|
|
// Match: declare -A VARNAME=( ... ) — may span multiple lines
|
|
if (!preg_match('/declare\s+-A\s+' . preg_quote($varname, '/') . '\s*=\s*\(\s*(.*?)\s*\)/s', $raw, $m))
|
|
return [];
|
|
preg_match_all('/\["([^"]+)"\]\s*=\s*"([^"]*)"/', $m[1], $pairs);
|
|
$out = [];
|
|
foreach ($pairs[1] as $i => $k) $out[$k] = $pairs[2][$i];
|
|
return $out;
|
|
}
|
|
|
|
// Write HOST*_DOCKER_FOLDER_MAP back into raw conf text. Returns updated text.
|
|
function vv_dk_write_conf_map(string $raw, string $id, array $map): string {
|
|
$varname = "{$id}_DOCKER_FOLDER_MAP";
|
|
ksort($map);
|
|
|
|
$lines = [" declare -A {$varname}=("];
|
|
foreach ($map as $container => $folder) {
|
|
$lines[] = ' ["' . $container . '"]="' . $folder . '"';
|
|
}
|
|
$lines[] = ' )';
|
|
$block = implode("\n", $lines);
|
|
|
|
// Replace existing block
|
|
$pattern = '/[ \t]*declare\s+-A\s+' . preg_quote($varname, '/') . '\s*=\s*\(.*?\)/s';
|
|
if (preg_match($pattern, $raw)) {
|
|
return preg_replace($pattern, $block, $raw);
|
|
}
|
|
|
|
// Append before the "End Of HOST*" comment if present, otherwise at end
|
|
$endMarker = "# ── End Of {$id}";
|
|
$altMarker = "# ──────────────────────── End Of {$id}";
|
|
foreach ([$altMarker, $endMarker] as $marker) {
|
|
$pos = strpos($raw, $marker);
|
|
if ($pos !== false) {
|
|
$header = "\n# ── Docker Folder Map — managed by Varaverk Docker tab ──────────────────────\n";
|
|
return substr($raw, 0, $pos) . $header . $block . "\n\n" . substr($raw, $pos);
|
|
}
|
|
}
|
|
return $raw . "\n# ── Docker Folder Map ────────────────────────────────────────────────────────\n" . $block . "\n";
|
|
}
|
|
|
|
// ── Container inventory (full inspect) ───────────────────────────────────────
|
|
|
|
function vv_dk_webui(string $name, array $portMap): string {
|
|
$template = '/boot/config/plugins/dockerMan/templates-user/my-' . $name . '.xml';
|
|
if (!file_exists($template)) return '';
|
|
$xml = @file_get_contents($template) ?: '';
|
|
if (!preg_match('/<WebUI>(.*?)<\/WebUI>/s', $xml, $m)) return '';
|
|
$url = trim($m[1]);
|
|
if (!$url) return '';
|
|
$url = str_replace('[IP]', vv_local_ip(), $url);
|
|
$url = preg_replace_callback('/\[PORT:(\d+)\]/', fn($pm) => $portMap[$pm[1]] ?? $pm[1], $url);
|
|
return $url;
|
|
}
|
|
|
|
function vv_dk_icon(string $name): string {
|
|
$template = '/boot/config/plugins/dockerMan/templates-user/my-' . $name . '.xml';
|
|
if (!file_exists($template)) return '';
|
|
$xml = @file_get_contents($template) ?: '';
|
|
return preg_match('/<Icon>(.*?)<\/Icon>/s', $xml, $m) ? trim($m[1]) : '';
|
|
}
|
|
|
|
function vv_dk_inspect_all(): array {
|
|
$ids = trim(shell_exec("docker ps -aq 2>/dev/null") ?: '');
|
|
if (!$ids) return [];
|
|
|
|
$idList = implode(' ', array_map('escapeshellarg', explode("\n", $ids)));
|
|
$raw = shell_exec("docker inspect $idList 2>/dev/null") ?: '[]';
|
|
$data = json_decode($raw, true) ?: [];
|
|
|
|
$out = [];
|
|
foreach ($data as $c) {
|
|
$name = ltrim($c['Name'] ?? '', '/');
|
|
if (!$name) continue;
|
|
$status = $c['State']['Status'] ?? 'unknown';
|
|
$running = $status === 'running';
|
|
$image = $c['Config']['Image'] ?? '';
|
|
|
|
// Networks: name → IP
|
|
$networks = [];
|
|
foreach ($c['NetworkSettings']['Networks'] ?? [] as $netName => $net) {
|
|
$ip = $net['IPAddress'] ?? '';
|
|
if ($ip) $networks[$netName] = $ip;
|
|
}
|
|
|
|
// Port mappings: hostPort → containerPort/proto
|
|
$ports = [];
|
|
$portMap = []; // containerPort → hostPort (for WebUI resolution)
|
|
foreach ($c['HostConfig']['PortBindings'] ?? [] as $containerPort => $bindings) {
|
|
foreach ($bindings ?? [] as $b) {
|
|
$hp = $b['HostPort'] ?? '';
|
|
if ($hp) {
|
|
$ports[] = $hp . '→' . $containerPort;
|
|
[$cp] = explode('/', $containerPort);
|
|
$portMap[$cp] = $hp;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bind mounts only
|
|
$mounts = [];
|
|
foreach ($c['Mounts'] ?? [] as $m) {
|
|
if (($m['Type'] ?? '') === 'bind') {
|
|
$mounts[] = ['src' => $m['Source'] ?? '', 'dst' => $m['Destination'] ?? ''];
|
|
}
|
|
}
|
|
|
|
$webui = vv_dk_webui($name, $portMap);
|
|
$icon = vv_dk_icon($name);
|
|
|
|
$out[$name] = [
|
|
'name' => $name,
|
|
'running' => $running,
|
|
'status' => $status,
|
|
'image' => $image,
|
|
'networks' => $networks,
|
|
'ports' => $ports,
|
|
'mounts' => $mounts,
|
|
'webui' => $webui,
|
|
'icon' => $icon,
|
|
];
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
// ── Main data builder ─────────────────────────────────────────────────────────
|
|
|
|
function vv_dk_all(): array {
|
|
$jsonData = vv_dk_read_json();
|
|
$containers = vv_dk_inspect_all();
|
|
$currentHost= vv_detect_host();
|
|
$myId = strtoupper($currentHost);
|
|
$myRaw = vv_read_conf_raw($currentHost . '.conf');
|
|
$confMap = vv_dk_read_conf_map($myRaw, $myId);
|
|
|
|
$empty = ['name'=>'','running'=>false,'status'=>'','image'=>'','networks'=>[],'ports'=>[],'mounts'=>[],'webui'=>'','icon'=>''];
|
|
|
|
// Build folder list enriched with live container data
|
|
$assigned = [];
|
|
$folders = [];
|
|
foreach ($jsonData as $fid => $f) {
|
|
$ctrs = [];
|
|
foreach ($f['containers'] ?? [] as $cname) {
|
|
$assigned[$cname] = true;
|
|
$ctrs[] = array_merge($empty, ['name' => $cname], $containers[$cname] ?? []);
|
|
}
|
|
usort($ctrs, fn($a, $b) => $b['running'] <=> $a['running'] ?: strcmp($a['name'], $b['name']));
|
|
$running = count(array_filter($ctrs, fn($c) => $c['running']));
|
|
$folders[] = [
|
|
'id' => $fid,
|
|
'name' => $f['name'] ?? 'Unnamed',
|
|
'total' => count($ctrs),
|
|
'running' => $running,
|
|
'containers' => $ctrs,
|
|
];
|
|
}
|
|
usort($folders, fn($a, $b) => strcmp($a['name'], $b['name']));
|
|
|
|
// Ungrouped: running but not in any folder
|
|
$ungrouped = [];
|
|
foreach ($containers as $name => $c) {
|
|
if (isset($assigned[$name])) continue;
|
|
$ungrouped[] = $c;
|
|
}
|
|
usort($ungrouped, fn($a, $b) => strcmp($a['name'], $b['name']));
|
|
|
|
// Drift: containers in conf map but assigned to a different folder in json, or missing from json
|
|
$drift = [];
|
|
foreach ($confMap as $cname => $desiredFolder) {
|
|
// Find where it actually is in json
|
|
$actualFolder = null;
|
|
foreach ($jsonData as $f) {
|
|
if (in_array($cname, $f['containers'] ?? [], true)) {
|
|
$actualFolder = $f['name'];
|
|
break;
|
|
}
|
|
}
|
|
if ($actualFolder !== $desiredFolder) {
|
|
$drift[] = ['container' => $cname, 'conf' => $desiredFolder, 'actual' => $actualFolder];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'ts' => time(),
|
|
'host' => $myId,
|
|
'folders' => $folders,
|
|
'ungrouped' => $ungrouped,
|
|
'conf_map' => $confMap,
|
|
'drift' => $drift,
|
|
'fv3_synced'=> file_exists(dirname(VV_FV3_JSON)),
|
|
];
|
|
}
|
|
|
|
// ── Write actions ─────────────────────────────────────────────────────────────
|
|
|
|
// Move a container to a folder (or '' to ungroup). Writes json + conf.
|
|
function vv_dk_move_container(string $container, string $targetFolderId): array {
|
|
if (!$container) return ['ok' => false, 'error' => 'No container'];
|
|
$data = vv_dk_read_json();
|
|
|
|
// Remove from all folders
|
|
foreach ($data as &$f) {
|
|
$f['containers'] = array_values(array_filter($f['containers'] ?? [], fn($c) => $c !== $container));
|
|
if (isset($f['containerImages'][$container])) unset($f['containerImages'][$container]);
|
|
}
|
|
unset($f);
|
|
|
|
// Add to target folder
|
|
if ($targetFolderId && isset($data[$targetFolderId])) {
|
|
if (!in_array($container, $data[$targetFolderId]['containers'] ?? [], true)) {
|
|
$data[$targetFolderId]['containers'][] = $container;
|
|
}
|
|
}
|
|
|
|
if (!vv_dk_write_json($data)) return ['ok' => false, 'error' => 'JSON write failed'];
|
|
|
|
// Update conf
|
|
_vv_dk_sync_conf_from_json($data);
|
|
return ['ok' => true];
|
|
}
|
|
|
|
// Create a new folder. Returns new folder id.
|
|
function vv_dk_create_folder(string $name): array {
|
|
$name = trim($name);
|
|
if (!$name) return ['ok' => false, 'error' => 'Name required'];
|
|
$data = vv_dk_read_json();
|
|
$id = vv_dk_gen_id();
|
|
$data[$id] = [
|
|
'name' => $name,
|
|
'icon' => '',
|
|
'settings' => [],
|
|
'regex' => '',
|
|
'containers' => [],
|
|
'containerImages' => [],
|
|
'hidden_preview' => [],
|
|
'actions' => [],
|
|
];
|
|
if (!vv_dk_write_json($data)) return ['ok' => false, 'error' => 'JSON write failed'];
|
|
return ['ok' => true, 'id' => $id];
|
|
}
|
|
|
|
// Rename a folder. Updates json + conf (folder name strings in map).
|
|
function vv_dk_rename_folder(string $folderId, string $newName): array {
|
|
$newName = trim($newName);
|
|
if (!$newName || !$folderId) return ['ok' => false, 'error' => 'Missing params'];
|
|
$data = vv_dk_read_json();
|
|
if (!isset($data[$folderId])) return ['ok' => false, 'error' => 'Folder not found'];
|
|
$oldName = $data[$folderId]['name'];
|
|
$data[$folderId]['name'] = $newName;
|
|
if (!vv_dk_write_json($data)) return ['ok' => false, 'error' => 'JSON write failed'];
|
|
|
|
// Update conf: replace old folder name with new name in the map
|
|
$currentHost = vv_detect_host();
|
|
$myId = strtoupper($currentHost);
|
|
$raw = vv_read_conf_raw($currentHost . '.conf');
|
|
$map = vv_dk_read_conf_map($raw, $myId);
|
|
foreach ($map as &$v) {
|
|
if ($v === $oldName) $v = $newName;
|
|
}
|
|
unset($v);
|
|
$updated = vv_dk_write_conf_map($raw, $myId, $map);
|
|
vv_write_conf_raw($currentHost . '.conf', $updated);
|
|
return ['ok' => true];
|
|
}
|
|
|
|
// Delete a folder (containers become ungrouped). Writes json + conf.
|
|
function vv_dk_delete_folder(string $folderId): array {
|
|
if (!$folderId) return ['ok' => false, 'error' => 'No folder id'];
|
|
$data = vv_dk_read_json();
|
|
if (!isset($data[$folderId])) return ['ok' => false, 'error' => 'Folder not found'];
|
|
$folderName = $data[$folderId]['name'];
|
|
unset($data[$folderId]);
|
|
if (!vv_dk_write_json($data)) return ['ok' => false, 'error' => 'JSON write failed'];
|
|
|
|
// Remove from conf map
|
|
$currentHost = vv_detect_host();
|
|
$myId = strtoupper($currentHost);
|
|
$raw = vv_read_conf_raw($currentHost . '.conf');
|
|
$map = vv_dk_read_conf_map($raw, $myId);
|
|
$map = array_filter($map, fn($v) => $v !== $folderName);
|
|
$updated = vv_dk_write_conf_map($raw, $myId, $map);
|
|
vv_write_conf_raw($currentHost . '.conf', $updated);
|
|
return ['ok' => true];
|
|
}
|
|
|
|
// Sync conf desired state → json (apply conf map, fix drift)
|
|
function vv_dk_sync_conf_to_json(): array {
|
|
$currentHost = vv_detect_host();
|
|
$myId = strtoupper($currentHost);
|
|
$raw = vv_read_conf_raw($currentHost . '.conf');
|
|
$confMap = vv_dk_read_conf_map($raw, $myId);
|
|
$data = vv_dk_read_json();
|
|
|
|
// Build folder name → id map
|
|
$nameToId = [];
|
|
foreach ($data as $fid => $f) $nameToId[$f['name']] = $fid;
|
|
|
|
foreach ($confMap as $container => $folderName) {
|
|
// Create folder if missing
|
|
if (!isset($nameToId[$folderName])) {
|
|
$newId = vv_dk_gen_id();
|
|
$data[$newId] = ['name' => $folderName, 'icon' => '', 'settings' => [],
|
|
'regex' => '', 'containers' => [], 'containerImages' => [],
|
|
'hidden_preview' => [], 'actions' => []];
|
|
$nameToId[$folderName] = $newId;
|
|
}
|
|
$targetId = $nameToId[$folderName];
|
|
|
|
// Remove from all folders
|
|
foreach ($data as &$f) {
|
|
$f['containers'] = array_values(array_filter($f['containers'] ?? [], fn($c) => $c !== $container));
|
|
}
|
|
unset($f);
|
|
|
|
// Add to target
|
|
if (!in_array($container, $data[$targetId]['containers'], true))
|
|
$data[$targetId]['containers'][] = $container;
|
|
}
|
|
|
|
if (!vv_dk_write_json($data)) return ['ok' => false, 'error' => 'JSON write failed'];
|
|
return ['ok' => true, 'applied' => count($confMap)];
|
|
}
|
|
|
|
// Sync json current state → conf (capture manual json edits)
|
|
function vv_dk_sync_json_to_conf(): array {
|
|
$data = vv_dk_read_json();
|
|
$currentHost = vv_detect_host();
|
|
$myId = strtoupper($currentHost);
|
|
$raw = vv_read_conf_raw($currentHost . '.conf');
|
|
_vv_dk_sync_conf_from_json($data, $raw, $currentHost, $myId);
|
|
return ['ok' => true];
|
|
}
|
|
|
|
// Internal: rebuild conf map from current json state and write it
|
|
function _vv_dk_sync_conf_from_json(array $data, string $raw = '', string $host = '', string $id = ''): void {
|
|
if (!$host) $host = vv_detect_host();
|
|
if (!$id) $id = strtoupper($host);
|
|
if (!$raw) $raw = vv_read_conf_raw($host . '.conf');
|
|
|
|
$map = [];
|
|
foreach ($data as $f) {
|
|
$name = $f['name'] ?? '';
|
|
foreach ($f['containers'] ?? [] as $c) $map[$c] = $name;
|
|
}
|
|
$updated = vv_dk_write_conf_map($raw, $id, $map);
|
|
vv_write_conf_raw($host . '.conf', $updated);
|
|
}
|