Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 28 additions & 14 deletions source/compose.manager/compose.manager.dashboard.page
Original file line number Diff line number Diff line change
Expand Up @@ -457,24 +457,38 @@ $script = <<<'EOT'
if (response.containers && response.containers.length > 0) {
var html = '';
response.containers.forEach(function(ct) {
// API returns capitalized property names: State, ID, Icon, Name, WebUI, Image, UpdateStatus, LocalSha, RemoteSha
var isRunning = ct.State === 'running';
// Tolerate PascalCase/camelCase field variants from different API paths.
var ctName = ct.Name || ct.name || ct.Service || ct.service || 'unknown';
var ctState = ct.State || ct.state || '';
var ctIdRaw = ct.ID || ct.Id || ct.id || ctName;
var ctId = String(ctIdRaw || ctName);
var ctIdShort = ctId.substring(0, 12);
var ctIcon = ct.Icon || ct.icon || '/plugins/dynamix.docker.manager/images/question.png';
var ctShell = ct.Shell || ct.shell || '/bin/bash';
var ctWebUI = ct.WebUI || ct.webUI || ct.webui || '';
var ctImage = ct.Image || ct.image || '';
var ctUpdateStatus = ct.UpdateStatus || ct.updateStatus || 'unknown';
var ctLocalSha = ct.LocalSha || ct.localSha || '';
var ctRemoteSha = ct.RemoteSha || ct.remoteSha || '';
var ctStartedAt = ct.StartedAt || ct.startedAt || '';

var isRunning = ctState === 'running';
var stateIcon = isRunning ? 'fa-play' : 'fa-square';
var stateColor = isRunning ? 'green-text' : 'red-text';
var stateText = isRunning ? 'started' : 'stopped';
var ctElId = 'dash-ct-' + ct.ID.substring(0, 12);
var imgSrc = ct.Icon || '/plugins/dynamix.docker.manager/images/question.png';
var shell = ct.Shell || '/bin/bash';
var shortId = ct.ID.substring(0, 12);
var webui = resolveContainerWebUI(ct.WebUI);
var ctElId = 'dash-ct-' + ctIdShort;
var imgSrc = ctIcon;
var shell = ctShell;
var shortId = ctIdShort;
var webui = resolveContainerWebUI(ctWebUI);
// Parse image to get repo and tag
var image = ct.Image || '';
var image = ctImage;
var imageDisplay = image.replace(/^.*\//, ''); // Remove registry prefix

// Update status
var updateStatus = ct.UpdateStatus || 'unknown';
var localSha = ct.LocalSha || '';
var remoteSha = ct.RemoteSha || '';
var updateStatus = ctUpdateStatus;
var localSha = ctLocalSha;
var remoteSha = ctRemoteSha;
var updateHtml = '';
if (updateStatus === 'up-to-date') {
updateHtml = '<span class="green-text"><i class="fa fa-check"></i> current</span>';
Expand All @@ -485,14 +499,14 @@ $script = <<<'EOT'
}

// Container uptime - use shared smart formatting
var ctUptime = formatUptime(ct.StartedAt, isRunning);
var ctUptime = formatUptime(ctStartedAt, isRunning);

html += '<div class="compose-dash-container">';
html += '<span class="compose-dash-ct-icon" id="' + ctElId + '" data-ct-name="' + ct.Name + '" data-ct-id="' + shortId + '" data-ct-running="' + (isRunning ? '1' : '0') + '" data-ct-webui="' + escapeAttr(webui) + '" data-ct-shell="' + escapeAttr(shell) + '">';
html += '<span class="compose-dash-ct-icon" id="' + ctElId + '" data-ct-name="' + escapeAttr(ctName) + '" data-ct-id="' + escapeAttr(shortId) + '" data-ct-running="' + (isRunning ? '1' : '0') + '" data-ct-webui="' + escapeAttr(webui) + '" data-ct-shell="' + escapeAttr(shell) + '">';
html += '<img src="' + escapeAttr(imgSrc) + '" onerror="this.src=\'/plugins/dynamix.docker.manager/images/question.png\';">';
html += '</span>';
html += '<span class="compose-dash-ct-info">';
html += '<div class="compose-dash-ct-name">' + ct.Name + '</div>';
html += '<div class="compose-dash-ct-name">' + escapeHtml(ctName) + '</div>';
html += '<div class="compose-dash-ct-state ' + stateColor + '"><i class="fa ' + stateIcon + '"></i> ' + stateText + '</div>';
html += '</span>';
html += '<span class="compose-dash-ct-cols">';
Expand Down
101 changes: 23 additions & 78 deletions source/compose.manager/php/compose_list.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,47 +54,20 @@

$stackCount++;

$projectName = $project;
if (is_file("$compose_root/$project/name")) {
$projectName = trim(file_get_contents("$compose_root/$project/name"));
}
// Resolve stack identity and metadata via StackInfo
$stackInfo = StackInfo::fromProject($compose_root, $project);

$projectName = $stackInfo->getName();
$id = str_replace(".", "-", $project);
$id = str_replace(" ", "", $id);

// Get the compose file path
$basePath = is_file("$compose_root/$project/indirect")
? trim(file_get_contents("$compose_root/$project/indirect"))
: "$compose_root/$project";
$composeFile = findComposeFile($basePath) ?: "$basePath/" . COMPOSE_FILE_NAMES[0];
// Resolve override via centralized helper (prefer correctly-named indirect override)
$overridePath = OverrideInfo::fromStack($compose_root, $project)->getOverridePath();

// Use docker compose config --services to get accurate service count
// This properly parses YAML, handles overrides, extends, etc.
$definedServices = 0;
if (is_file($composeFile)) {
$files = "-f " . escapeshellarg($composeFile);
if (is_file($overridePath)) {
$files .= " -f " . escapeshellarg($overridePath);
}
// Get the compose file path and override via StackInfo
$composeFile = $stackInfo->composeFilePath ?? ($stackInfo->composeSource . '/' . COMPOSE_FILE_NAMES[0]);
$overridePath = $stackInfo->getOverridePath();

// Get env file if specified
$envFile = "";
if (is_file("$compose_root/$project/envpath")) {
$envPath = trim(file_get_contents("$compose_root/$project/envpath"));
if (is_file($envPath)) {
$envFile = "--env-file " . escapeshellarg($envPath);
}
}

// Use docker compose config --services to list all service names
$cmd = "docker compose $files $envFile config --services 2>/dev/null";
$output = shell_exec($cmd);
if ($output) {
$services = array_filter(explode("\n", trim($output)));
$definedServices = count($services);
}
}
// Use StackInfo's getDefinedServices for accurate service count
$definedServicesList = $stackInfo->getDefinedServices();
$definedServices = count($definedServicesList);

// Get running container info from $containersByProject
// Use directory basename (sanitized) as project key — this matches the -p flag in echoComposeCommand
Expand Down Expand Up @@ -137,48 +110,23 @@
$isrestarting = $restartingCount > 0;
$isup = $actualContainerCount > 0;

if (is_file("$compose_root/$project/description")) {
$description = @file_get_contents("$compose_root/$project/description");
$description = str_replace("\r", "", $description);
// Escape HTML first to prevent XSS, then convert newlines to <br>
$description = htmlspecialchars($description, ENT_QUOTES, 'UTF-8');
// Read metadata via StackInfo lazy getters
$descriptionRaw = $stackInfo->getDescription();
if ($descriptionRaw) {
$descriptionRaw = str_replace("\r", "", $descriptionRaw);
$description = htmlspecialchars($descriptionRaw, ENT_QUOTES, 'UTF-8');
$description = str_replace("\n", "<br>", $description);
} else {
$description = "";
}

$autostart = '';
if (is_file("$compose_root/$project/autostart")) {
$autostarttext = @file_get_contents("$compose_root/$project/autostart");
if (strpos($autostarttext, 'true') !== false) {
$autostart = 'checked';
}
}

// Check for custom project icon (URL-based only via icon_url file)
$projectIcon = '';
if (is_file("$compose_root/$project/icon_url")) {
$iconUrl = trim(@file_get_contents("$compose_root/$project/icon_url"));
if (filter_var($iconUrl, FILTER_VALIDATE_URL) && (strpos($iconUrl, 'http://') === 0 || strpos($iconUrl, 'https://') === 0)) {
$projectIcon = $iconUrl;
}
}
$autostart = $stackInfo->getAutostart() ? 'checked' : '';

// Check for stack-level WebUI URL
$webuiUrl = '';
if (is_file("$compose_root/$project/webui_url")) {
$webuiUrlTmp = trim(@file_get_contents("$compose_root/$project/webui_url"));
if (filter_var($webuiUrlTmp, FILTER_VALIDATE_URL) && (strpos($webuiUrlTmp, 'http://') === 0 || strpos($webuiUrlTmp, 'https://') === 0)) {
$webuiUrl = $webuiUrlTmp;
}
}
$projectIcon = $stackInfo->getIconUrl();
$webuiUrl = $stackInfo->getWebUIUrl();

$profiles = array();
if (is_file("$compose_root/$project/profiles")) {
$profilestext = @file_get_contents("$compose_root/$project/profiles");
$profiles = json_decode($profilestext, false);
}
$profilesJson = htmlspecialchars(json_encode($profiles ? $profiles : []), ENT_QUOTES, 'UTF-8');
$profiles = $stackInfo->getProfiles();
$profilesJson = htmlspecialchars(json_encode($profiles ?: []), ENT_QUOTES, 'UTF-8');

// Determine status text and class for badge
$statusText = "Stopped";
Expand Down Expand Up @@ -210,7 +158,7 @@
$projectHtml = htmlspecialchars($project, ENT_QUOTES, 'UTF-8');
$descriptionHtml = $description; // Already contains <br> tags from earlier processing
$pathHtml = htmlspecialchars("$compose_root/$project", ENT_QUOTES, 'UTF-8');
$projectIconUrl = htmlspecialchars($projectIcon, ENT_QUOTES, 'UTF-8');
$projectIconUrl = htmlspecialchars($projectIcon ?? '', ENT_QUOTES, 'UTF-8');

// Status like Docker tab (started/stopped with icon)
$status = $isrunning ? ($runningCount == $containerCount ? 'started' : 'partial') : 'stopped';
Expand All @@ -231,11 +179,8 @@
$statusLabel = "partial ($runningCount/$containerCount)";
}

// Get stack started_at timestamp from file for uptime calculation
$stackStartedAt = '';
if (is_file("$compose_root/$project/started_at")) {
$stackStartedAt = trim(file_get_contents("$compose_root/$project/started_at"));
}
// Get stack started_at timestamp via StackInfo
$stackStartedAt = $stackInfo->getStartedAt();

// Calculate uptime display from started_at timestamp
$stackUptime = '';
Expand Down
Loading