mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-20 15:22:59 +00:00
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:
@@ -3167,6 +3167,19 @@ app.get('/api/containers', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/ports/in-use', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const fsService = FileSystemService.getInstance(req.nodeId);
|
||||
const stacks = await fsService.getStacks();
|
||||
const dockerController = DockerController.getInstance(req.nodeId);
|
||||
const portsInUse = await dockerController.getPortsInUse(stacks);
|
||||
res.json(portsInUse);
|
||||
} catch (error) {
|
||||
console.error('[Ports] Failed to fetch ports in use:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch ports in use' });
|
||||
}
|
||||
});
|
||||
|
||||
// --- Label Routes (Skipper+) ---
|
||||
|
||||
app.get('/api/labels', authMiddleware, async (req: Request, res: Response): Promise<void> => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user