Merge pull request #61 from AnsoCode/fix/managed-container-classification-by-working-dir

fix(stats): classify managed containers by working_dir instead of project name
This commit is contained in:
Anso
2026-03-22 19:42:05 -04:00
committed by GitHub
2 changed files with 16 additions and 14 deletions
+1
View File
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- **Managed container count wrong when stacks launched from COMPOSE_DIR root:** The `GET /api/stats` endpoint classified containers as managed by matching `com.docker.compose.project` against known stack directory names. When stacks are launched from the COMPOSE_DIR root (rather than each subdirectory), Docker assigns the root folder name as the project name for all of them — causing every such container to appear as "external". Fixed by classifying via `com.docker.compose.project.working_dir`: a container is managed if its working directory falls within COMPOSE_DIR, regardless of the project name.
- **Blank page on HTTP deployments (root cause — Helmet 8 default CSP):** Helmet 8 merges custom `directives` with its built-in defaults, which include `upgrade-insecure-requests`. The previous fix (PR #59) omitted the directive from the custom object, but Helmet silently re-added it from defaults. The correct fix is `upgradeInsecureRequests: null`, which is the Helmet 8 API for explicitly removing a default directive.
- **Login loop caused by remote node auth failure:** When a configured remote node had an expired or invalid API token, every proxied request to that node returned 401. Both `apiFetch` and `fetchForNode` treated any 401 as a user session failure and dispatched `sencho-unauthorized`, immediately logging the user out and sending them back to the login page — even after a successful login. Fixed by adding a `proxyRes` handler that stamps all remote-proxied responses with `x-sencho-proxy: 1`; the frontend now only fires `sencho-unauthorized` when this header is absent (i.e., it's a genuine local session failure).
- **Missing `authMiddleware` on notifications endpoints:** `GET /api/notifications`, `POST /api/notifications/read`, `DELETE /api/notifications/:id`, `DELETE /api/notifications`, and `POST /api/notifications/test` were all missing `authMiddleware`, violating the default-deny policy. Added to all five.
+15 -14
View File
@@ -1156,24 +1156,25 @@ app.post('/api/convert', async (req: Request, res: Response) => {
// Get all containers stats for dashboard
app.get('/api/stats', async (req: Request, res: Response) => {
try {
const [dockerController, knownStacks] = [
DockerController.getInstance(req.nodeId),
await FileSystemService.getInstance(req.nodeId).getStacks(),
];
const allContainers = await dockerController.getAllContainers();
const knownSet = new Set(knownStacks);
const composeDir = path.resolve(NodeRegistry.getInstance().getComposeDir(req.nodeId));
const allContainers = await DockerController.getInstance(req.nodeId).getAllContainers();
// A container is "managed" if Docker started it from within COMPOSE_DIR.
// We use com.docker.compose.project.working_dir rather than project name because
// stacks launched from the COMPOSE_DIR root (not a subdirectory) all share the
// project name of the root folder — causing false "external" classification.
const isManagedByComposeDir = (c: any): boolean => {
const workingDir: string | undefined = c.Labels?.['com.docker.compose.project.working_dir'];
if (!workingDir) return false;
const resolved = path.resolve(workingDir);
return resolved === composeDir || resolved.startsWith(composeDir + path.sep);
};
const active = allContainers.filter((c: any) => c.State === 'running').length;
const exited = allContainers.filter((c: any) => c.State === 'exited').length;
const total = allContainers.length;
const managed = allContainers.filter((c: any) => {
const project: string | undefined = c.Labels?.['com.docker.compose.project'];
return project && knownSet.has(project) && c.State === 'running';
}).length;
const unmanaged = allContainers.filter((c: any) => {
const project: string | undefined = c.Labels?.['com.docker.compose.project'];
return project && !knownSet.has(project) && c.State === 'running';
}).length;
const managed = allContainers.filter((c: any) => c.State === 'running' && isManagedByComposeDir(c)).length;
const unmanaged = allContainers.filter((c: any) => c.State === 'running' && !isManagedByComposeDir(c)).length;
res.json({ active, managed, unmanaged, exited, total });
} catch (error) {