Files
Gmer4Lfe 206a119a4b Show each container's fallback tier on the Monitor board
The row's left border carries it rather than the status dot, which already means running or
stopped.
2026-08-23 16:38:58 -04:00

150 lines
6.4 KiB
PHP

<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Small compatibility layer over docker.php — resolves a container's WebUI URL and returns
// the folder grouping in the shape older callers expect.
//
// DESIGN PRINCIPLES
// Thin by intent. The folder store and inventory live in docker.php; this file only adapts
// their output. New work belongs there, not here.
//
// WebUI resolution reads the dockerMan template, which is where Unraid records the port and
// path a container's UI actually lives on — not guessed from the published ports.
//
// OPERATIONAL SAFEGUARDS
// A container with no template, or no WebUI declared, returns empty and simply renders
// without a link. Absence is normal, not an error.
//
// Read-only. Resolves and reshapes; the store is written only through docker.php.
//
// EXPORTS
// vv_container_webui() WebUI URL for one container, or empty
// vv_container_tier_map() container name (lowercased) => fallback tier 1-4
// vv_get_docker_folders() folder grouping in the legacy shape, each container carrying 'tier'
//
// CONFIGURATION
// Inherits everything from docker.php — see that file's CONFIGURATION block.
// ═══════════════════════════════════════════════════════════════════════════════════════════════
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/docker.php';
function vv_container_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);
// [PORT:XXXX] → mapped host port
$url = preg_replace_callback('/\[PORT:(\d+)\]/', function($pm) use ($name, $portMap) {
return $portMap[$name][$pm[1]] ?? $pm[1];
}, $url);
// Scheme allowlist, applied here so a bad value never reaches the page rather than being
// filtered at each sink. A WebUI entry is http or https in every real template; anything else
// is either broken or a javascript: URL aimed at whoever clicks it. These files come from
// Community Applications and hand edits, so they are not ours to trust. include/docs.php
// applies the same rule to markdown links for the same reason.
if (!preg_match('#^https?://#i', $url)) return '';
return $url;
}
// Fallback tier for each container, from FALLBACK_<ME>_TIER1..4 in THIS host's own conf —
// that list is what the partner starts for us, so a lower tier means it comes back sooner.
// Keyed lowercase because conf spelling and docker's spelling of a name need not match.
function vv_container_tier_map(): array {
$me = strtoupper(vv_detect_host());
if ($me === 'UNKNOWN') return [];
$raw = vv_read_host_conf_raw(strtolower($me));
if ($raw === '') return [];
$map = [];
for ($t = 1; $t <= 4; $t++) {
foreach (vv_parse_conf_list($raw, "FALLBACK_{$me}_TIER{$t}") as $name) {
$key = strtolower(trim($name));
if ($key !== '' && !isset($map[$key])) $map[$key] = $t; // lowest tier wins a duplicate
}
}
return $map;
}
function vv_get_docker_folders(): array {
// Varaverk's own docker_folders.json is the primary store (see include/docker.php) —
// reading folder.view3's mirror directly here left this widget empty on any host
// without that optional third-party plugin installed.
$folderData = vv_dk_read_json();
$tierMap = vv_container_tier_map();
// One docker ps call: names, status, port mappings
$raw = shell_exec("docker ps -a --format '{{.Names}}\t{{.Status}}\t{{.Ports}}' 2>/dev/null") ?? '';
$statusMap = [];
$portMap = [];
foreach (explode("\n", trim($raw)) as $line) {
$parts = explode("\t", $line, 3);
if (count($parts) < 2) continue;
[$cname, $status, $ports] = array_pad($parts, 3, '');
$cname = trim($cname);
if ($cname === '') continue;
$statusMap[$cname] = trim($status);
foreach (explode(',', $ports) as $entry) {
if (preg_match('/(\d+)->(\d+)\/tcp/', trim($entry), $pm)) {
$portMap[$cname][$pm[2]] = $pm[1]; // containerPort => hostPort
}
}
}
$folderContainerNames = [];
$folders = [];
foreach ($folderData as $id => $f) {
$containers = [];
foreach ($f['containers'] ?? [] as $cname) {
$folderContainerNames[] = $cname;
$status = $statusMap[$cname] ?? '';
$running = str_starts_with($status, 'Up');
$containers[] = [
'name' => $cname,
'running' => $running,
'status' => $status,
'webui' => vv_container_webui($cname, $portMap),
'tier' => $tierMap[strtolower($cname)] ?? null,
];
}
usort($containers, fn($a, $b) => $b['running'] <=> $a['running'] ?: strcmp($a['name'], $b['name']));
$folders[] = [
'id' => $id,
'name' => $f['name'] ?? 'Unnamed',
'icon' => $f['icon'] ?? '',
'containers' => $containers,
];
}
usort($folders, fn($a, $b) => strcmp($a['name'], $b['name']));
$ungrouped = [];
foreach ($statusMap as $cname => $status) {
if (in_array($cname, $folderContainerNames, true)) continue;
$running = str_starts_with($status, 'Up');
$ungrouped[] = [
'name' => $cname,
'running' => $running,
'status' => $status,
'webui' => vv_container_webui($cname, $portMap),
'tier' => $tierMap[strtolower($cname)] ?? null,
];
}
usort($ungrouped, fn($a, $b) => strcmp($a['name'], $b['name']));
return [
'available' => true,
'folders' => $folders,
'ungrouped' => $ungrouped,
];
}