diff --git a/applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js b/applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js index 80ce6863a4..66f412a68f 100644 --- a/applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js +++ b/applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js @@ -250,8 +250,12 @@ return dm2.dv.extend({ if (!portBindings || typeof portBindings !== 'object') return []; const ports = []; for (const [containerPort, bindings] of Object.entries(portBindings)) { - if (Array.isArray(bindings) && bindings.length > 0 && bindings[0]?.HostPort) { - ports.push(`${bindings[0].HostPort}:${containerPort}`); + if (Array.isArray(bindings)) { + for (const b of bindings) { + if (!b?.HostPort) continue; + const ip = (b.HostIp && b.HostIp !== '0.0.0.0' && b.HostIp !== '::') ? b.HostIp + ':' : ''; + ports.push(`${ip}${b.HostPort}:${containerPort}`); + } } } return ports; diff --git a/applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js b/applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js index 726240eac1..c493683b8b 100644 --- a/applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js +++ b/applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js @@ -138,9 +138,12 @@ return dm2.dv.extend({ publish: (() => { const ports = []; for (const [containerPort, bindings] of Object.entries(hostConfig.PortBindings || {})) { - if (Array.isArray(bindings) && bindings.length > 0 && bindings[0]?.HostPort) { - const hostPort = bindings[0].HostPort; - ports.push(hostPort + ':' + containerPort); + if (Array.isArray(bindings) && bindings.length > 0) { + for (const b of bindings) { + if (!b?.HostPort) continue; + const ip = (b.HostIp && b.HostIp !== '0.0.0.0' && b.HostIp !== '::') ? b.HostIp : ''; + ports.push((ip ? ip + ':' : '') + b.HostPort + ':' + containerPort); + } } } return ports; @@ -816,8 +819,32 @@ return dm2.dv.extend({ (Array.isArray(publish) ? publish : [publish]) .filter(p => p && typeof p === 'string' && p.trim().length > 0) .map(p => { - const m = p.match(/^(\d+):(\d+)\/(tcp|udp)$/); - if (m) return [`${m[2]}/${m[3]}`, [{ HostPort: m[1] }]]; + // hostIp:hostPort:cPort/proto (e.g. 192.168.1.100:8080:80/tcp) + const m = p.match(/^([^:]+):(\d+):(\d+)\/(tcp|udp)$/); + if (m) { + const hostIp = m[1]; + const hostPort = m[2]; + const cPort = m[3]; + const proto = m[4]; + return [`${cPort}/${proto}`, [{ HostIp: hostIp, HostPort: hostPort }]]; + } + // [ipv6]:hostPort:cPort/proto (e.g. [::1]:8080:80/tcp) + const m6 = p.match(/^\[([^\]]+)\]:(\d+):(\d+)\/(tcp|udp)$/); + if (m6) { + const hostIp = m6[1]; + const hostPort = m6[2]; + const cPort = m6[3]; + const proto = m6[4]; + return [`${cPort}/${proto}`, [{ HostIp: hostIp, HostPort: hostPort }]]; + } + // hostPort:cPort/proto (e.g. 8080:80/tcp) + const m2 = p.match(/^(\d+):(\d+)\/(tcp|udp)$/); + if (m2) { + const hostPort = m2[1]; + const cPort = m2[2]; + const proto = m2[3]; + return [`${cPort}/${proto}`, [{ HostPort: hostPort }]]; + } return null; }).filter(Boolean) ) : undefined, diff --git a/applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js b/applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js index ec0499bb84..cc4980f6e4 100644 --- a/applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js +++ b/applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js @@ -309,6 +309,56 @@ return dm2.dv.extend({ }, E('div', btns)); }, + buildPortLinks(ports) { + // cont.Ports[] from GET /containers/json — flat array. + // Published ports have {IP, PrivatePort, PublicPort, Type}. + // Exposed-only ports omit IP and PublicPort: {PrivatePort, Type}. + if (!Array.isArray(ports) || ports.length === 0) return ''; + + const LOCAL_IPS = new Set(['0.0.0.0', '::']); + + // Sort: published (has PublicPort) before exposed-only, then by PrivatePort + const sorted = [...ports].sort((a, b) => { + const aHasPub = a.PublicPort ? 1 : 0; + const bHasPub = b.PublicPort ? 1 : 0; + if (aHasPub !== bHasPub) return bHasPub - aHasPub; + return (a.PrivatePort || 0) - (b.PrivatePort || 0); + }); + + const lines = sorted.map(p => { + const ip = p.IP || ''; + const pub = p.PublicPort || ''; + const priv = p.PrivatePort || ''; + const type = p.Type || ''; + + const isIPv6 = ip.includes(':'); + const isLocal = LOCAL_IPS.has(ip); + const displayIp = isIPv6 ? `[${ip}]` : ip; + + let label; + if (pub && ip) label = `${displayIp}:${pub}->${priv}/${type}`; + else if (pub) label = `${pub}->${priv}/${type}`; + else label = `${priv}/${type}`; + + // Clickable link for published TCP ports only + if (type === 'tcp' && pub) { + const host = isLocal ? window.location.hostname : displayIp; + return E('div', {}, [ + E('a', { + href: `http://${host}:${pub}`, + target: '_blank', + rel: 'noopener noreferrer', + title: _('Open in browser'), + }, [label]), + ]); + } + + return E('div', {}, [label]); + }); + + return E('div', {}, lines); + }, + handleSave: null, handleSaveApply: null, handleReset: null, @@ -343,16 +393,7 @@ return dm2.dv.extend({ _shortId: (cont?.Id || '').substring(0, 12), Networks: this.parseNetworkLinksForContainer(network_list, cont?.NetworkSettings?.Networks || {}, true), Created: this.buildTimeString(cont?.Created) || '', - Ports: (Array.isArray(cont.Ports) && cont.Ports.length > 0) - ? cont.Ports.map(p => { - // const ip = p.IP || ''; - const pub = p.PublicPort || ''; - const priv = p.PrivatePort || ''; - const type = p.Type || ''; - return `${pub ? pub + ':' : ''}${priv}/${type}`; - // return `${ip ? ip + ':' : ''}${pub} -> ${priv} (${type})`; - }).join('
') - : '', + Ports: this.buildPortLinks(cont.Ports), }); }