'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, '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); }