--- /dev/null +++ b/luci-app-dockerman/root/etc/uci-defaults/luci-app-dockerman @@ -0,0 +1,8 @@ +#!/bin/sh + +uci -q batch <<-EOF >/dev/null + set uhttpd.main.script_timeout="360" + commit uhttpd +EOF + +/etc/init.d/dockerd restart --- a/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/configuration.js +++ b/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/configuration.js @@ -66,6 +66,7 @@ return L.view.extend({ o.value('https://docker.io'); o.value('https://ghcr.io'); o.value('https://hub-mirror.c.163.com'); + o.value('https://ustc-edu-cn.mirror.aliyuncs.com'); o = s.taboption('globals', form.ListValue, 'log_level', _('Log Level'), --- a/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js +++ b/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js @@ -73,7 +73,8 @@ return dm2.dv.extend({ const hostConfig = c.HostConfig || {}; const resolvedImage = resolveImageId(c.Image) || resolveImageId(c.Config?.Image) || c.Image || c.Config?.Image || ''; const builtInNetworks = new Set(['none', 'bridge', 'host']); - const [netnames, nets] = Object.entries(c.NetworkSettings?.Networks || {}); + // Object.entries returns [[name, obj], ...] — pick the first network + const [[firstName, firstNet] = []] = Object.entries(c.NetworkSettings?.Networks || {}); containerData.container = { name: c.Name?.substring(1) || '', @@ -82,20 +83,18 @@ return dm2.dv.extend({ image: resolvedImage, privileged: hostConfig.Privileged ? 1 : 0, restart_policy: hostConfig.RestartPolicy?.Name || 'unless-stopped', - network: (() => { - return (netnames && (netnames.length > 0)) ? netnames[0] : ''; - })(), + network: firstName || '', ipv4: (() => { - if (builtInNetworks.has(netnames[0])) return ''; - return (nets && (nets.length > 0)) ? nets[0]?.IPAddress || '' : ''; + if (!firstName || builtInNetworks.has(firstName)) return ''; + return firstNet?.IPAddress || ''; })(), ipv6: (() => { - if (builtInNetworks.has(netnames[0])) return ''; - return (nets && (nets.length > 0)) ? nets[0]?.GlobalIPv6Address || '' : ''; + if (!firstName || builtInNetworks.has(firstName)) return ''; + return firstNet?.GlobalIPv6Address || ''; })(), ipv6_lla: (() => { - if (builtInNetworks.has(netnames[0])) return ''; - return (nets && (nets.length > 0)) ? nets[0]?.LinkLocalIPv6Address || '' : ''; + if (!firstName || builtInNetworks.has(firstName)) return ''; + return firstNet?.LinkLocalIPv6Address || ''; })(), link: hostConfig.Links || [], dns: hostConfig.Dns || [], @@ -135,9 +134,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 || ''; + ports.push((ip ? ip + ':' : '') + b.HostPort + ':' + containerPort); + } } } return ports; @@ -801,9 +803,15 @@ 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] }]]; - return null; + // formats: hostPort:cPort/proto or hostIp:hostPort:cPort/proto + const m = p.match(/^(?:([^:]+):(\d+)|(\d+)):(\d+)\/(tcp|udp)$/); + if (!m) return null; + const hostIp = m[1] || ''; + const hostPort = m[2] || m[3]; + const cPort = m[4]; + const proto = m[5]; + const binding = hostIp ? { HostIp: hostIp, HostPort: hostPort } : { HostPort: hostPort }; + return [`${cPort}/${proto}`, [binding]]; }).filter(Boolean) ) : undefined, Mounts: undefined, --- a/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js +++ b/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js @@ -308,6 +308,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, @@ -342,16 +392,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), }); }