diff --git a/source/compose.manager/include/ComposeList.php b/source/compose.manager/include/ComposeList.php
index a176f5a..72fc3c9 100755
--- a/source/compose.manager/include/ComposeList.php
+++ b/source/compose.manager/include/ComposeList.php
@@ -24,38 +24,23 @@
$composeFile = $stackInfo->composeFilePath ?? ($stackInfo->composeSource . '/' . COMPOSE_FILE_NAMES[0]);
$overridePath = $stackInfo->getOverridePath();
- // Use StackInfo's getDefinedServices for accurate service count
- $definedServicesList = $stackInfo->getDefinedServices();
- $definedServices = count($definedServicesList);
-
- $projectContainers = $stackInfo->getContainerList();
- $runningCount = 0;
- $stoppedCount = 0;
- $pausedCount = 0;
- $restartingCount = 0;
-
- foreach ($projectContainers as $ct) {
- $ctState = $ct['State'] ?? '';
- if ($ctState === 'running') {
- $runningCount++;
- } elseif ($ctState === 'exited') {
- $stoppedCount++;
- } elseif ($ctState === 'paused') {
- $pausedCount++;
- } elseif ($ctState === 'restarting') {
- $restartingCount++;
- }
- }
-
- // Container counts
- $actualContainerCount = count($projectContainers);
- $containerCount = $definedServices > 0 ? $definedServices : $actualContainerCount;
+ // getStackState() internally calls getContainerCounts(), which caches
+ // the result. Calling getContainerCounts() afterwards is free.
+ $stackState = $stackInfo->getStackState();
+ $counts = $stackInfo->getContainerCounts();
+
+ $runningCount = $counts['running'];
+ $stoppedCount = $counts['stopped'];
+ $pausedCount = $counts['paused'];
+ $restartingCount = $counts['restarting'];
+ $actualContainerCount = $counts['total'];
+ $containerCount = $counts['total'];
// Collect container names for the hide-from-docker feature (data attribute)
$containerNamesList = [];
// Collect short container IDs for CPU/MEM load mapping (docker stats uses 12-char short IDs)
$containerIdsList = [];
- foreach ($projectContainers as $ct) {
+ foreach ($stackInfo->getContainerList() as $ct) {
$n = $ct['Names'] ?? '';
if ($n) $containerNamesList[] = $n;
$ctId = $ct['ID'] ?? '';
@@ -124,24 +109,12 @@
$hasInvalidIndirect = ($invalidIndirectPath !== null && trim($invalidIndirectPath) !== '');
$invalidIndirectPathHtml = htmlspecialchars($invalidIndirectPath ?? '', ENT_QUOTES, 'UTF-8');
- // Status like Docker tab (started/stopped with icon)
- $status = $isrunning ? ($runningCount == $containerCount ? 'started' : 'partial') : 'stopped';
- // Use exclamation icon for partial state so it looks like a warning
- if ($status === 'partial') {
- $shape = 'exclamation-circle';
- } elseif ($isrunning) {
- $shape = 'play';
- } else {
- $shape = 'square';
- }
- $color = $status == 'started' ? 'green-text' : ($status == 'partial' ? 'orange-text' : 'grey-text');
- // Use 'partial' outer class for partial state to allow correct styling
- $outerClass = $isrunning ? ($runningCount == $containerCount ? 'started' : 'partial') : 'stopped';
-
- $statusLabel = $status;
- if ($status == 'partial') {
- $statusLabel = "partial ($runningCount/$containerCount)";
- }
+ // Status icon, label, color — derived from centralized getStackState()
+ $status = $stackState['state'];
+ $shape = $stackState['shape'];
+ $color = $stackState['color'];
+ $outerClass = $status;
+ $statusLabel = $stackState['label'];
// Get stack started_at timestamp via StackInfo
$stackStartedAt = $stackInfo->getStartedAt();
diff --git a/source/compose.manager/include/ComposeManager.php b/source/compose.manager/include/ComposeManager.php
index c384f77..51e284f 100755
--- a/source/compose.manager/include/ComposeManager.php
+++ b/source/compose.manager/include/ComposeManager.php
@@ -275,6 +275,7 @@ function compose_manager_cpu_spec_count($cpuSpec)
+