--- a/luci-app-dockerman/root/usr/share/luci/menu.d/luci-app-dockerman.json +++ b/luci-app-dockerman/root/usr/share/luci/menu.d/luci-app-dockerman.json @@ -1,7 +1,7 @@ { - "admin/services/dockerman": { - "title": "Dockerman JS", - "order": "60", + "admin/dockerman": { + "title": "Docker", + "order": 40, "action": { "type": "firstchild" }, @@ -15,7 +15,7 @@ } }, - "admin/services/dockerman/overview": { + "admin/dockerman/overview": { "title": "Overview", "order": 1, "action": { @@ -24,7 +24,7 @@ } }, - "admin/services/dockerman/configuration": { + "admin/dockerman/configuration": { "title": "Configuration", "order": 2, "action": { @@ -33,14 +33,14 @@ } }, - "admin/services/dockerman/container/archive/*": { + "admin/dockerman/container/archive/*": { "action": { "type": "alias", - "path": "admin/services/dockerman/containers" + "path": "admin/dockerman/containers" } }, - "admin/services/dockerman/container/archive/put/*": { + "admin/dockerman/container/archive/put/*": { "action": { "type": "function", "module": "luci.controller.docker", @@ -52,7 +52,7 @@ } }, - "admin/services/dockerman/container/archive/get/*": { + "admin/dockerman/container/archive/get/*": { "action": { "type": "function", "module": "luci.controller.docker", @@ -64,7 +64,7 @@ } }, - "admin/services/dockerman/container/export/*": { + "admin/dockerman/container/export/*": { "action": { "type": "function", "module": "luci.controller.docker", @@ -76,7 +76,7 @@ } }, - "admin/services/dockerman/container/*": { + "admin/dockerman/container/*": { "title_hide": "Container", "action": { "type": "view", @@ -84,7 +84,7 @@ } }, - "admin/services/dockerman/containers": { + "admin/dockerman/containers": { "title": "Containers", "order": 3, "action": { @@ -93,7 +93,7 @@ } }, - "admin/services/dockerman/containers/prune": { + "admin/dockerman/containers/prune": { "action": { "type": "function", "module": "luci.controller.docker", @@ -106,14 +106,14 @@ } }, - "admin/services/dockerman/container": { + "admin/dockerman/container": { "action": { "type": "alias", - "path": "admin/services/dockerman/containers" + "path": "admin/dockerman/containers" } }, - "admin/services/dockerman/container_new": { + "admin/dockerman/container_new": { "title_hide": "Container", "action": { "type": "view", @@ -121,7 +121,7 @@ } }, - "admin/services/dockerman/images/build": { + "admin/dockerman/images/build": { "action": { "type": "function", "module": "luci.controller.docker", @@ -134,7 +134,7 @@ } }, - "admin/services/dockerman/images/build/prune": { + "admin/dockerman/images/build/prune": { "action": { "type": "function", "module": "luci.controller.docker", @@ -147,7 +147,7 @@ } }, - "admin/services/dockerman/images/get/*": { + "admin/dockerman/images/get/*": { "action": { "type": "function", "module": "luci.controller.docker", @@ -159,7 +159,7 @@ } }, - "admin/services/dockerman/images/load": { + "admin/dockerman/images/load": { "action": { "type": "function", "module": "luci.controller.docker", @@ -172,7 +172,7 @@ } }, - "admin/services/dockerman/images/prune": { + "admin/dockerman/images/prune": { "action": { "type": "function", "module": "luci.controller.docker", @@ -185,7 +185,7 @@ } }, - "admin/services/dockerman/images": { + "admin/dockerman/images": { "title": "Images", "order": 4, "action": { @@ -194,7 +194,7 @@ } }, - "admin/services/dockerman/images/create": { + "admin/dockerman/images/create": { "action": { "type": "function", "module": "luci.controller.docker", @@ -207,7 +207,7 @@ } }, - "admin/services/dockerman/images/push/*": { + "admin/dockerman/images/push/*": { "action": { "type": "function", "module": "luci.controller.docker", @@ -220,14 +220,14 @@ } }, - "admin/services/dockerman/image": { + "admin/dockerman/image": { "action": { "type": "alias", - "path": "admin/services/dockerman/images" + "path": "admin/dockerman/images" } }, - "admin/services/dockerman/network/*": { + "admin/dockerman/network/*": { "title_hide": "Network", "action": { "type": "view", @@ -235,7 +235,7 @@ } }, - "admin/services/dockerman/networks": { + "admin/dockerman/networks": { "title": "Networks", "order": 5, "action": { @@ -244,7 +244,7 @@ } }, - "admin/services/dockerman/networks/prune": { + "admin/dockerman/networks/prune": { "action": { "type": "function", "module": "luci.controller.docker", @@ -257,7 +257,7 @@ } }, - "admin/services/dockerman/network_new": { + "admin/dockerman/network_new": { "title_hide": "Network", "action": { "type": "view", @@ -265,14 +265,14 @@ } }, - "admin/services/dockerman/network": { + "admin/dockerman/network": { "action": { "type": "alias", - "path": "admin/services/dockerman/networks" + "path": "admin/dockerman/networks" } }, - "admin/services/dockerman/volumes": { + "admin/dockerman/volumes": { "title": "Volumes", "order": 6, "action": { @@ -281,7 +281,7 @@ } }, - "admin/services/dockerman/volumes/prune": { + "admin/dockerman/volumes/prune": { "action": { "type": "function", "module": "luci.controller.docker", @@ -294,7 +294,7 @@ } }, - "admin/services/dockerman/docker/events": { + "admin/dockerman/docker/events": { "action": { "type": "function", "module": "luci.controller.docker", @@ -307,7 +307,7 @@ } }, - "admin/services/dockerman/events": { + "admin/dockerman/events": { "title": "Events", "order": 7, "action": { --- a/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js +++ b/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js @@ -437,7 +437,7 @@ const dv = view.extend({ outputText: '', // Initialize output text get dockerman_url() { - return L.url('admin/services/dockerman'); + return L.url('admin/dockerman'); }, parseHeaders(headers, array) { --- /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), }); }