diff --git a/CHANGELOG.md b/CHANGELOG.md index 119579cc..a0986cab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Changed:** Standardized manual stack deletion to use the Two-Stage Teardown (Compose Down -> File Wipe) to prevent ghost networks. - **Fixed:** Atomic Rollback failure where non-empty directories caused silent file system errors. - **Added:** Two-Stage Teardown mechanism to ensure `docker compose down` sweeps up ghost networks before deployment files are deleted. +### Deprecated +- **(Planned)** Port 2375 (TCP) fallback support; future releases may require SSH-only for Node config. + +### Fixed +- **Fixed:** Docker API parsing bug where HTML string responses from misconfigured ports were counted as containers. +- **Fixed:** Stack list crashing when SFTP connections fail by gracefully catching SSH errors and returning empty arrays. + +### Changed +- **Changed:** Expanded Node Manager UI width and added horizontal scrollbars for better data visibility. - **Added:** Smart Error Parser with telemetry-ready rule IDs to translate cryptic Docker output. - **Added:** Post-Deploy Health Probe to catch immediate container crashes that slip past Compose. - **Changed:** Rollback engine respects a `canSilentlyRollback` flag to protect user-authored configurations. diff --git a/backend/src/services/DockerController.ts b/backend/src/services/DockerController.ts index dccac8e4..8c3adcb0 100644 --- a/backend/src/services/DockerController.ts +++ b/backend/src/services/DockerController.ts @@ -29,6 +29,14 @@ class DockerController { return this.docker; } + private validateApiData(data: any): T { + // If the daemon port points to a web server (like Sencho UI), Dockerode receives HTML + if (typeof data === 'string') { + throw new Error("Invalid response from Docker API. Did you provide a web port instead of the Docker daemon port?"); + } + return data as T; + } + public async getDiskUsage() { const df = await this.docker.df(); @@ -89,16 +97,19 @@ class DockerController { } public async getImages() { - return await this.docker.listImages({ all: false }); + const data = await this.docker.listImages({ all: false }); + return this.validateApiData(data); } public async getVolumes() { const data = await this.docker.listVolumes(); - return data.Volumes || []; + const validated = this.validateApiData(data); + return validated.Volumes || []; } public async getNetworks() { - return await this.docker.listNetworks(); + const data = await this.docker.listNetworks(); + return this.validateApiData(data); } public async removeImage(id: string) { @@ -118,12 +129,12 @@ class DockerController { public async getRunningContainers() { const containers = await this.docker.listContainers({ all: false }); - return containers; + return this.validateApiData(containers); } public async getAllContainers() { const containers = await this.docker.listContainers({ all: true }); - return containers; + return this.validateApiData(containers); } public async getContainersByStack(stackName: string) { diff --git a/backend/src/services/FileSystemService.ts b/backend/src/services/FileSystemService.ts index 6bfbe781..5733052b 100644 --- a/backend/src/services/FileSystemService.ts +++ b/backend/src/services/FileSystemService.ts @@ -86,8 +86,9 @@ export class FileSystemService { } return stackNames; - } catch (error) { - console.error('Error reading stacks:', error); + } catch (error: any) { + const nodeName = NodeRegistry.getInstance().getNode(this.nodeId)?.name || 'Unknown'; + console.warn(`[SFTP] Failed to fetch stacks for Node ${nodeName}: ${error.message || error}`); return []; } } diff --git a/backend/src/services/NodeRegistry.ts b/backend/src/services/NodeRegistry.ts index 186ad63d..ba828975 100644 --- a/backend/src/services/NodeRegistry.ts +++ b/backend/src/services/NodeRegistry.ts @@ -112,6 +112,12 @@ export class NodeRegistry { try { const docker = this.createDockerClient(node); const info = await docker.info(); + + // Validate the payload contains actual Docker daemon info, not arbitrary HTML + if (!info || !info.OperatingSystem || typeof info.Containers !== 'number') { + throw new Error("Invalid response from Docker API. Did you provide a web port instead of the Docker daemon port?"); + } + db.updateNodeStatus(nodeId, 'online'); return { success: true, diff --git a/frontend/src/components/NodeManager.tsx b/frontend/src/components/NodeManager.tsx index 2cbebb07..302247dc 100644 --- a/frontend/src/components/NodeManager.tsx +++ b/frontend/src/components/NodeManager.tsx @@ -212,9 +212,12 @@ export function NodeManager() { value={formData.compose_dir} onChange={(e) => setFormData({ ...formData, compose_dir: e.target.value })} /> -

- The root directory where compose stack folders live on this node -

+
+

The root directory where compose stack folders live on this node.

+

+ Note: Strategy B requires valid SSH credentials to read and edit compose.yaml files. TCP alone only grants container lifecycle management. +

+
); @@ -238,7 +241,7 @@ export function NodeManager() { Add Node - + Add Remote Node @@ -256,7 +259,7 @@ export function NodeManager() { {/* Nodes Table */} -
+
@@ -380,7 +383,7 @@ export function NodeManager() { {/* Edit Dialog */} - + Edit Node