Varaverk: Docker tab — folder management with conf + folder.view3 JSON dual-write

- 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
This commit is contained in:
Gmer4Lfe
2026-05-28 22:39:44 -04:00
parent fb051b60c1
commit e95fd30e13
4 changed files with 661 additions and 2 deletions
+317
View File
@@ -0,0 +1,317 @@
<?php
// Docker tab — folder management and container inventory
// Reads/writes /boot/config/plugins/folder.view3/docker.json (shared with folder.view3 plugin)
// Reads/writes HOST*_DOCKER_FOLDER_MAP in host*.conf (used by onboard scripts)
require_once __DIR__ . '/config.php';
define('VV_DOCKER_JSON', '/boot/config/plugins/folder.view3/docker.json');
// ── JSON helpers ──────────────────────────────────────────────────────────────
function vv_dk_read_json(): array {
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;
$tmp = $path . '.vv.tmp';
if (file_put_contents($tmp, json_encode($data, JSON_UNESCAPED_SLASHES)) === false) return false;
return rename($tmp, $path);
}
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 ───────────────────────────────────────────────────────
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);
}