From e95fd30e13285c239be4c4c8ab878efceedee3d5 Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Thu, 28 May 2026 22:39:44 -0400 Subject: [PATCH] =?UTF-8?q?Varaverk:=20Docker=20tab=20=E2=80=94=20folder?= =?UTF-8?q?=20management=20with=20conf=20+=20folder.view3=20JSON=20dual-wr?= =?UTF-8?q?ite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New Docker tab (between Scheduler and Watchdog) - Reads /boot/config/plugins/folder.view3/docker.json — fully compatible with folder.view3 plugin - Writes changes to both docker.json AND HOST*_DOCKER_FOLDER_MAP in host*.conf simultaneously - Edit mode: rename folders (inline input), delete folders, move containers via popover picker, create new folders - Drift banner: highlights containers where conf desired state doesn't match json actual state - Sync conf→JSON: apply conf desired state to json (fixes drift after onboard) - Sync JSON→conf: capture manual json edits back into conf - Ungrouped section shows all containers not assigned to any folder - Onboard scripts can read HOST*_DOCKER_FOLDER_MAP to auto-place new containers --- Plugin/unraid/Varaverk.page | 4 +- Plugin/unraid/api/docker.php | 31 +++ Plugin/unraid/include/docker.php | 317 +++++++++++++++++++++++++++++++ Plugin/unraid/pages/docker.php | 311 ++++++++++++++++++++++++++++++ 4 files changed, 661 insertions(+), 2 deletions(-) create mode 100644 Plugin/unraid/api/docker.php create mode 100644 Plugin/unraid/include/docker.php create mode 100644 Plugin/unraid/pages/docker.php diff --git a/Plugin/unraid/Varaverk.page b/Plugin/unraid/Varaverk.page index a339f54..f901a8c 100644 --- a/Plugin/unraid/Varaverk.page +++ b/Plugin/unraid/Varaverk.page @@ -9,9 +9,9 @@ $pluginDir = "$docroot/plugins/$plugin"; // Determine active tab $tab = $_GET['tab'] ?? 'monitor'; -$validTabs = ['monitor', 'scheduler', 'partnership', 'fallback', 'watchdog', 'arrs']; +$validTabs = ['monitor', 'scheduler', 'docker', 'watchdog', 'partnership', 'fallback', 'arrs']; if (!in_array($tab, $validTabs)) $tab = 'monitor'; -$tabLabels = ['monitor' => 'Monitor', 'scheduler' => 'Scheduler', 'partnership' => 'Partnership', 'fallback' => 'FallBack', 'watchdog' => 'Watchdog', 'arrs' => 'Arrs']; +$tabLabels = ['monitor' => 'Monitor', 'scheduler' => 'Scheduler', 'docker' => 'Docker', 'watchdog' => 'Watchdog', 'partnership' => 'Partnership', 'fallback' => 'FallBack', 'arrs' => 'Arrs']; ?> diff --git a/Plugin/unraid/api/docker.php b/Plugin/unraid/api/docker.php new file mode 100644 index 0000000..d7871fc --- /dev/null +++ b/Plugin/unraid/api/docker.php @@ -0,0 +1,31 @@ + false, 'error' => 'GET or POST only']); + exit; +} + +$action = trim($_POST['action'] ?? ''); +$container = trim($_POST['container'] ?? ''); +$folderId = trim($_POST['folder_id'] ?? ''); +$name = trim($_POST['name'] ?? ''); + +$result = match ($action) { + 'move_container' => vv_dk_move_container($container, $folderId), + 'create_folder' => vv_dk_create_folder($name), + 'rename_folder' => vv_dk_rename_folder($folderId, $name), + 'delete_folder' => vv_dk_delete_folder($folderId), + 'sync_conf_to_json'=> vv_dk_sync_conf_to_json(), + 'sync_json_to_conf'=> vv_dk_sync_json_to_conf(), + default => ['ok' => false, 'error' => 'Unknown action: ' . $action], +}; + +echo json_encode($result); diff --git a/Plugin/unraid/include/docker.php b/Plugin/unraid/include/docker.php new file mode 100644 index 0000000..53bdf86 --- /dev/null +++ b/Plugin/unraid/include/docker.php @@ -0,0 +1,317 @@ + '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 ─────────────────────────────────────────────────────── + +function vv_dk_containers(): array { + $raw = shell_exec("docker ps -a --format '{{.Names}}\t{{.Status}}\t{{.Image}}\t{{.RunningFor}}' 2>/dev/null") ?: ''; + $out = []; + foreach (explode("\n", trim($raw)) as $line) { + if (!$line) continue; + [$name, $status, $image, $age] = array_pad(explode("\t", $line, 4), 4, ''); + $name = trim($name); + if (!$name) continue; + $running = str_starts_with(trim($status), 'Up'); + $out[$name] = [ + 'name' => $name, + 'running' => $running, + 'status' => trim($status), + 'image' => trim($image), + 'age' => trim($age), + ]; + } + return $out; +} + +// ── Main data builder ───────────────────────────────────────────────────────── + +function vv_dk_all(): array { + $jsonData = vv_dk_read_json(); + $containers = vv_dk_containers(); + $currentHost= vv_detect_host(); + $myId = strtoupper($currentHost); + $myRaw = vv_read_conf_raw($currentHost . '.conf'); + $confMap = vv_dk_read_conf_map($myRaw, $myId); + + // 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(['name' => $cname, 'running' => false, 'status' => '', 'image' => '', 'age' => ''], + $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', + 'icon' => $f['icon'] ?? '', + 'total' => count($ctrs), + 'running' => $running, + 'containers'=> $ctrs, + ]; + } + usort($folders, fn($a, $b) => strcmp($a['name'], $b['name'])); + + // Ungrouped: in docker ps 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, + 'json_path' => VV_DOCKER_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); +} diff --git a/Plugin/unraid/pages/docker.php b/Plugin/unraid/pages/docker.php new file mode 100644 index 0000000..f05d3d3 --- /dev/null +++ b/Plugin/unraid/pages/docker.php @@ -0,0 +1,311 @@ + + +
+ Docker + + + + +
+ +
+ +
+
Loading…
+
+ + + + +