diff --git a/backend/src/services/DockerController.ts b/backend/src/services/DockerController.ts index 28b6fdd6..74efb711 100644 --- a/backend/src/services/DockerController.ts +++ b/backend/src/services/DockerController.ts @@ -53,6 +53,7 @@ class DockerController { Name?: string; State?: string; Status?: string; + Publishers?: { URL?: string, TargetPort?: number, PublishedPort?: number }[]; } let containers: ComposeContainer[] = []; @@ -81,12 +82,21 @@ class DockerController { // Map to frontend's expected interface // Note: docker compose ps returns Name (singular), but frontend expects Names (array) // Dockerode returns Names with leading slash, so we add it for compatibility - return containers.map((c) => ({ - Id: c.ID || '', - Names: ['/' + (c.Name || '')], // Add leading slash to match Dockerode format - State: c.State || 'unknown', - Status: c.Status || '' - })); + return containers.map((c) => { + let Ports: { PrivatePort: number, PublicPort: number }[] = []; + if (c.Publishers && Array.isArray(c.Publishers)) { + Ports = c.Publishers + .filter(p => typeof p.PublishedPort === 'number' && p.PublishedPort > 0) + .map(p => ({ PrivatePort: (p.TargetPort || 0) as number, PublicPort: p.PublishedPort as number })); + } + return { + Id: c.ID || '', + Names: ['/' + (c.Name || '')], // Add leading slash to match Dockerode format + State: c.State || 'unknown', + Status: c.Status || '', + Ports + }; + }); } // SMART FALLBACK: Trigger when docker compose ps returns empty @@ -163,12 +173,21 @@ class DockerController { }); // 5. Map to the frontend interface - return fallbackContainers.map(c => ({ - Id: c.Id, - Names: c.Names, - State: c.State, - Status: c.Status - })); + return fallbackContainers.map(c => { + let Ports: { PrivatePort: number, PublicPort: number }[] = []; + if (c.Ports && Array.isArray(c.Ports)) { + Ports = c.Ports + .filter((p: any) => typeof p.PublicPort === 'number' && p.PublicPort > 0) + .map((p: any) => ({ PrivatePort: (p.PrivatePort || 0) as number, PublicPort: p.PublicPort as number })); + } + return { + Id: c.Id, + Names: c.Names, + State: c.State, + Status: c.Status, + Ports + }; + }); } catch (fallbackError) { console.error(`Smart Fallback failed for ${stackName}:`, fallbackError); return []; diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index 330d3af0..2289c551 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -12,7 +12,7 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, import { Tabs, TabsList, TabsTrigger } from './ui/tabs'; import { Card, CardContent, CardHeader, CardTitle } from './ui/card'; import { Badge } from './ui/badge'; -import { Plus, Trash2, Play, Square, Save, Terminal, Sun, Moon, RotateCw, CloudDownload, Pencil, X, Home, LogOut, Brush } from 'lucide-react'; +import { Plus, Trash2, Play, Square, Save, Terminal, Sun, Moon, RotateCw, CloudDownload, Pencil, X, Home, LogOut, Brush, ExternalLink } from 'lucide-react'; import { useAuth } from '@/context/AuthContext'; import { apiFetch } from '@/lib/api'; import { toast } from 'sonner'; @@ -26,6 +26,7 @@ interface ContainerInfo { Id: string; Names: string[]; State: string; + Ports?: { PrivatePort: number, PublicPort: number }[]; } interface StackStatus { @@ -675,38 +676,73 @@ export default function EditorLayout() {