|
54 | 54 |
|
55 | 55 | $stackCount++; |
56 | 56 |
|
57 | | - $projectName = $project; |
58 | | - if (is_file("$compose_root/$project/name")) { |
59 | | - $projectName = trim(file_get_contents("$compose_root/$project/name")); |
60 | | - } |
| 57 | + // Resolve stack identity and metadata via StackInfo |
| 58 | + $stackInfo = StackInfo::fromProject($compose_root, $project); |
| 59 | + |
| 60 | + $projectName = $stackInfo->getName(); |
61 | 61 | $id = str_replace(".", "-", $project); |
62 | 62 | $id = str_replace(" ", "", $id); |
63 | 63 |
|
64 | | - // Get the compose file path |
65 | | - $basePath = is_file("$compose_root/$project/indirect") |
66 | | - ? trim(file_get_contents("$compose_root/$project/indirect")) |
67 | | - : "$compose_root/$project"; |
68 | | - $composeFile = findComposeFile($basePath) ?: "$basePath/" . COMPOSE_FILE_NAMES[0]; |
69 | | - // Resolve override via centralized helper (prefer correctly-named indirect override) |
70 | | - $overridePath = OverrideInfo::fromStack($compose_root, $project)->getOverridePath(); |
71 | | - |
72 | | - // Use docker compose config --services to get accurate service count |
73 | | - // This properly parses YAML, handles overrides, extends, etc. |
74 | | - $definedServices = 0; |
75 | | - if (is_file($composeFile)) { |
76 | | - $files = "-f " . escapeshellarg($composeFile); |
77 | | - if (is_file($overridePath)) { |
78 | | - $files .= " -f " . escapeshellarg($overridePath); |
79 | | - } |
| 64 | + // Get the compose file path and override via StackInfo |
| 65 | + $composeFile = $stackInfo->composeFilePath ?? ($stackInfo->composeSource . '/' . COMPOSE_FILE_NAMES[0]); |
| 66 | + $overridePath = $stackInfo->getOverridePath(); |
80 | 67 |
|
81 | | - // Get env file if specified |
82 | | - $envFile = ""; |
83 | | - if (is_file("$compose_root/$project/envpath")) { |
84 | | - $envPath = trim(file_get_contents("$compose_root/$project/envpath")); |
85 | | - if (is_file($envPath)) { |
86 | | - $envFile = "--env-file " . escapeshellarg($envPath); |
87 | | - } |
88 | | - } |
89 | | - |
90 | | - // Use docker compose config --services to list all service names |
91 | | - $cmd = "docker compose $files $envFile config --services 2>/dev/null"; |
92 | | - $output = shell_exec($cmd); |
93 | | - if ($output) { |
94 | | - $services = array_filter(explode("\n", trim($output))); |
95 | | - $definedServices = count($services); |
96 | | - } |
97 | | - } |
| 68 | + // Use StackInfo's getDefinedServices for accurate service count |
| 69 | + $definedServicesList = $stackInfo->getDefinedServices(); |
| 70 | + $definedServices = count($definedServicesList); |
98 | 71 |
|
99 | 72 | // Get running container info from $containersByProject |
100 | 73 | // Use directory basename (sanitized) as project key — this matches the -p flag in echoComposeCommand |
|
137 | 110 | $isrestarting = $restartingCount > 0; |
138 | 111 | $isup = $actualContainerCount > 0; |
139 | 112 |
|
140 | | - if (is_file("$compose_root/$project/description")) { |
141 | | - $description = @file_get_contents("$compose_root/$project/description"); |
142 | | - $description = str_replace("\r", "", $description); |
143 | | - // Escape HTML first to prevent XSS, then convert newlines to <br> |
144 | | - $description = htmlspecialchars($description, ENT_QUOTES, 'UTF-8'); |
| 113 | + // Read metadata via StackInfo lazy getters |
| 114 | + $descriptionRaw = $stackInfo->getDescription(); |
| 115 | + if ($descriptionRaw) { |
| 116 | + $descriptionRaw = str_replace("\r", "", $descriptionRaw); |
| 117 | + $description = htmlspecialchars($descriptionRaw, ENT_QUOTES, 'UTF-8'); |
145 | 118 | $description = str_replace("\n", "<br>", $description); |
146 | 119 | } else { |
147 | 120 | $description = ""; |
148 | 121 | } |
149 | 122 |
|
150 | | - $autostart = ''; |
151 | | - if (is_file("$compose_root/$project/autostart")) { |
152 | | - $autostarttext = @file_get_contents("$compose_root/$project/autostart"); |
153 | | - if (strpos($autostarttext, 'true') !== false) { |
154 | | - $autostart = 'checked'; |
155 | | - } |
156 | | - } |
157 | | - |
158 | | - // Check for custom project icon (URL-based only via icon_url file) |
159 | | - $projectIcon = ''; |
160 | | - if (is_file("$compose_root/$project/icon_url")) { |
161 | | - $iconUrl = trim(@file_get_contents("$compose_root/$project/icon_url")); |
162 | | - if (filter_var($iconUrl, FILTER_VALIDATE_URL) && (strpos($iconUrl, 'http://') === 0 || strpos($iconUrl, 'https://') === 0)) { |
163 | | - $projectIcon = $iconUrl; |
164 | | - } |
165 | | - } |
| 123 | + $autostart = $stackInfo->getAutostart() ? 'checked' : ''; |
166 | 124 |
|
167 | | - // Check for stack-level WebUI URL |
168 | | - $webuiUrl = ''; |
169 | | - if (is_file("$compose_root/$project/webui_url")) { |
170 | | - $webuiUrlTmp = trim(@file_get_contents("$compose_root/$project/webui_url")); |
171 | | - if (filter_var($webuiUrlTmp, FILTER_VALIDATE_URL) && (strpos($webuiUrlTmp, 'http://') === 0 || strpos($webuiUrlTmp, 'https://') === 0)) { |
172 | | - $webuiUrl = $webuiUrlTmp; |
173 | | - } |
174 | | - } |
| 125 | + $projectIcon = $stackInfo->getIconUrl(); |
| 126 | + $webuiUrl = $stackInfo->getWebUIUrl(); |
175 | 127 |
|
176 | | - $profiles = array(); |
177 | | - if (is_file("$compose_root/$project/profiles")) { |
178 | | - $profilestext = @file_get_contents("$compose_root/$project/profiles"); |
179 | | - $profiles = json_decode($profilestext, false); |
180 | | - } |
181 | | - $profilesJson = htmlspecialchars(json_encode($profiles ? $profiles : []), ENT_QUOTES, 'UTF-8'); |
| 128 | + $profiles = $stackInfo->getProfiles(); |
| 129 | + $profilesJson = htmlspecialchars(json_encode($profiles ?: []), ENT_QUOTES, 'UTF-8'); |
182 | 130 |
|
183 | 131 | // Determine status text and class for badge |
184 | 132 | $statusText = "Stopped"; |
|
210 | 158 | $projectHtml = htmlspecialchars($project, ENT_QUOTES, 'UTF-8'); |
211 | 159 | $descriptionHtml = $description; // Already contains <br> tags from earlier processing |
212 | 160 | $pathHtml = htmlspecialchars("$compose_root/$project", ENT_QUOTES, 'UTF-8'); |
213 | | - $projectIconUrl = htmlspecialchars($projectIcon, ENT_QUOTES, 'UTF-8'); |
| 161 | + $projectIconUrl = htmlspecialchars($projectIcon ?? '', ENT_QUOTES, 'UTF-8'); |
214 | 162 |
|
215 | 163 | // Status like Docker tab (started/stopped with icon) |
216 | 164 | $status = $isrunning ? ($runningCount == $containerCount ? 'started' : 'partial') : 'stopped'; |
|
231 | 179 | $statusLabel = "partial ($runningCount/$containerCount)"; |
232 | 180 | } |
233 | 181 |
|
234 | | - // Get stack started_at timestamp from file for uptime calculation |
235 | | - $stackStartedAt = ''; |
236 | | - if (is_file("$compose_root/$project/started_at")) { |
237 | | - $stackStartedAt = trim(file_get_contents("$compose_root/$project/started_at")); |
238 | | - } |
| 182 | + // Get stack started_at timestamp via StackInfo |
| 183 | + $stackStartedAt = $stackInfo->getStartedAt(); |
239 | 184 |
|
240 | 185 | // Calculate uptime display from started_at timestamp |
241 | 186 | $stackUptime = ''; |
|
0 commit comments