feat(app-store): add port conflict indicator to deploy sheet (#533)

Show a pulsating warning dot next to host ports that are already in
use by a running container. Hovering over the dot reveals which
Sencho-managed stack or external app occupies the port.

Adds GET /api/ports/in-use endpoint that returns a map of bound host
ports with ownership info, and a getPortsInUse method on
DockerController that reuses the existing container-to-stack
resolution logic.
This commit is contained in:
Anso
2026-04-12 19:57:38 -04:00
committed by GitHub
parent 7a2099b56e
commit cd3d7b23be
5 changed files with 98 additions and 3 deletions
+41
View File
@@ -40,6 +40,11 @@ export interface ClassifiedImage {
managedStatus: 'managed' | 'unmanaged' | 'unused';
}
export interface PortInUseInfo {
stack: string | null;
container: string;
}
export interface ClassifiedVolume {
Name: string;
Driver: string;
@@ -662,6 +667,42 @@ class DockerController {
return result;
}
/**
* Returns a map of host ports currently bound by running containers,
* with ownership info (Sencho-managed stack name or external).
*/
public async getPortsInUse(knownStackNames: string[]): Promise<Record<number, PortInUseInfo>> {
const [allContainers, projectToStack] = await Promise.all([
this.docker.listContainers({ all: false }),
DockerController.resolveProjectNameMap(knownStackNames),
]);
const absDirToStack = DockerController.buildAbsDirMap(knownStackNames);
const knownStackSet = new Set(knownStackNames);
const resolvedBase = path.resolve(COMPOSE_DIR);
const result: Record<number, PortInUseInfo> = {};
for (const container of allContainers as Array<{ Names?: string[]; Labels?: Record<string, string>; Ports?: Array<{ PublicPort?: number }> }>) {
const stackDir = DockerController.resolveContainerStack(
container.Labels, projectToStack, knownStackSet, absDirToStack, resolvedBase,
);
const containerName = (container.Names?.[0] || '').replace(/^\//, '');
if (!Array.isArray(container.Ports)) continue;
for (const port of container.Ports) {
if (!port.PublicPort || port.PublicPort <= 0) continue;
// First container to claim a port wins (avoids overwrites)
if (result[port.PublicPort]) continue;
result[port.PublicPort] = { stack: stackDir, container: containerName };
}
}
return result;
}
public async getContainersByStack(stackName: string) {
const stackDir = path.join(COMPOSE_DIR, stackName);