/**
* Add container page — blank create form (separate from Deploy templates).
* Field IDs use the addc- prefix. Submit goes through deployContainerWithSteps.
*/
import {
showStatusIndicator,
hideStatusIndicator,
showAlert,
} from './uiUtils.js'
import { presentError } from '../client/errors.js'
import {
precheckDeployNetworking,
formatNetworkingPrecheckMessage,
} from '../client/deployNetworkPrecheck.js'
let portSeq = 0
let volSeq = 0
let envSeq = 0
let labelSeq = 0
let dnsSeq = 0
let hostSeq = 0
let capSeq = 0
function el(id) {
return document.getElementById(id)
}
function val(id) {
return el(id)?.value?.trim?.() || ''
}
function checked(id) {
return Boolean(el(id)?.checked)
}
function numOrNull(id, parse = parseFloat) {
const raw = el(id)?.value
if (raw === '' || raw == null) return null
const n = parse(raw)
return Number.isFinite(n) && n > 0 ? n : null
}
/**
* Clear dynamic rows and reset primary fields.
*/
export function resetAddContainerForm() {
for (const id of [
'addc-ports',
'addc-volumes',
'addc-env',
'addc-labels',
'addc-dns',
'addc-extra-hosts',
'addc-capabilities',
]) {
const c = el(id)
if (c) c.innerHTML = ''
}
const form = el('add-container-form')
if (form) form.reset()
// Defaults after reset
const always = el('addc-always-pull')
if (always) always.checked = true
const detach = el('addc-detach')
if (detach) detach.checked = true
const net = el('addc-network-mode')
if (net) net.value = 'bridge'
const restart = el('addc-restart-policy')
if (restart) restart.value = 'no'
toggleAddcCustomNetwork()
}
export function addcAddPort(portData = null) {
const container = el('addc-ports')
if (!container) return
const id = `addc-port-${++portSeq}`
let hostPort = ''
let containerPort = ''
let protocol = 'tcp'
if (portData != null) {
const portStr = String(portData).trim()
if (portStr.includes(':')) {
const [h, rest] = portStr.split(':')
hostPort = h.trim()
if (rest.includes('/')) {
const [c, p] = rest.split('/')
containerPort = c.trim()
protocol = p?.toLowerCase() === 'udp' ? 'udp' : 'tcp'
} else containerPort = rest.trim()
} else if (portStr.includes('/')) {
const [c, p] = portStr.split('/')
containerPort = c.trim()
protocol = p?.toLowerCase() === 'udp' ? 'udp' : 'tcp'
} else if (portStr) containerPort = portStr
}
const item = document.createElement('div')
item.className = 'array-item port-mapping-item'
item.dataset.addcRow = id
item.innerHTML = `
`
item.querySelector('[data-addc-remove]')?.addEventListener('click', () => item.remove())
container.appendChild(item)
}
export function addcAddVolume() {
const container = el('addc-volumes')
if (!container) return
const id = `addc-vol-${++volSeq}`
const item = document.createElement('div')
item.className = 'array-item volume-mount-item'
item.dataset.addcRow = id
item.innerHTML = `
`
const typeSel = item.querySelector('.volume-type-input')
const srcLabel = item.querySelector('.volume-src-label')
typeSel?.addEventListener('change', () => {
if (srcLabel) srcLabel.textContent = typeSel.value === 'named' ? 'Volume name' : 'Host path'
})
item.querySelector('[data-addc-remove]')?.addEventListener('click', () => item.remove())
container.appendChild(item)
}
export function addcAddEnv() {
const container = el('addc-env')
if (!container) return
const id = `addc-env-${++envSeq}`
const item = document.createElement('div')
item.className = 'array-item mb-2 d-flex gap-2 align-items-center'
item.dataset.addcRow = id
item.innerHTML = `
`
item.querySelector('[data-addc-remove]')?.addEventListener('click', () => item.remove())
container.appendChild(item)
}
export function addcAddLabel() {
const container = el('addc-labels')
if (!container) return
const id = `addc-label-${++labelSeq}`
const item = document.createElement('div')
item.className = 'array-item d-flex gap-2 align-items-center mb-2'
item.dataset.addcRow = id
item.innerHTML = `
`
item.querySelector('[data-addc-remove]')?.addEventListener('click', () => item.remove())
container.appendChild(item)
}
export function addcAddDns() {
const container = el('addc-dns')
if (!container) return
const id = `addc-dns-${++dnsSeq}`
const item = document.createElement('div')
item.className = 'array-item d-flex gap-2 align-items-center mb-2'
item.dataset.addcRow = id
item.innerHTML = `
`
item.querySelector('[data-addc-remove]')?.addEventListener('click', () => item.remove())
container.appendChild(item)
}
export function addcAddExtraHost() {
const container = el('addc-extra-hosts')
if (!container) return
const id = `addc-host-${++hostSeq}`
const item = document.createElement('div')
item.className = 'array-item d-flex gap-2 align-items-center mb-2'
item.dataset.addcRow = id
item.innerHTML = `
`
item.querySelector('[data-addc-remove]')?.addEventListener('click', () => item.remove())
container.appendChild(item)
}
export function addcAddCapability() {
const container = el('addc-capabilities')
if (!container) return
const id = `addc-cap-${++capSeq}`
const item = document.createElement('div')
item.className = 'array-item d-flex gap-2 align-items-center mb-2'
item.dataset.addcRow = id
item.innerHTML = `
`
item.querySelector('[data-addc-remove]')?.addEventListener('click', () => item.remove())
container.appendChild(item)
}
export function toggleAddcCustomNetwork() {
const mode = val('addc-network-mode') || 'bridge'
const wrap = el('addc-custom-network-wrap')
const label = el('addc-custom-network-label')
const hint = el('addc-custom-network-hint')
const input = el('addc-custom-network')
if (!wrap) return
if (mode === 'host' || mode === 'none') {
wrap.style.display = 'none'
return
}
wrap.style.display = ''
if (mode === 'container') {
if (label) label.textContent = 'Container ID / name'
if (hint) hint.textContent = 'Share the network stack of this container'
if (input) input.placeholder = 'container-name-or-id'
} else {
if (label) label.textContent = 'Attach to network'
if (hint) hint.textContent = 'Named network to join after create (optional)'
if (input) input.placeholder = 'optional network name'
}
}
function collectPorts() {
const container = el('addc-ports')
if (!container) return []
const ports = []
container.querySelectorAll('.port-mapping-item').forEach((item) => {
const host = item.querySelector('.port-host-input')?.value.trim() || ''
const cPort = item.querySelector('.port-container-input')?.value.trim() || ''
const protocol = item.querySelector('.port-protocol-input')?.value || 'tcp'
if (!cPort) return
ports.push(host ? `${host}:${cPort}/${protocol}` : `${cPort}/${protocol}`)
})
return ports
}
function collectVolumes() {
const container = el('addc-volumes')
if (!container) return []
const volumes = []
container.querySelectorAll('.volume-mount-item').forEach((item) => {
const src = item.querySelector('.volume-host-input')?.value.trim() || ''
const dest = item.querySelector('.volume-container-input')?.value.trim() || ''
const mode = item.querySelector('.volume-mode-input')?.value || 'rw'
if (!src || !dest) return
volumes.push(`${src}:${dest}:${mode}`)
})
return volumes
}
function collectEnv() {
const container = el('addc-env')
if (!container) return []
const env = []
container.querySelectorAll('[data-env-key]').forEach((keyInput) => {
const name = keyInput.value.trim()
if (!name) return
const id = keyInput.getAttribute('data-env-key')
const valueInput = container.querySelector(`[data-env-value="${id}"]`)
env.push({ name, value: valueInput?.value ?? '' })
})
return env
}
function collectLabels() {
const container = el('addc-labels')
if (!container) return {}
/** @type {Record} */
const labels = {}
container.querySelectorAll('[data-label-id]').forEach((input) => {
const raw = input.value.trim()
if (!raw || !raw.includes('=')) return
const eq = raw.indexOf('=')
const k = raw.slice(0, eq).trim()
const v = raw.slice(eq + 1)
if (k) labels[k] = v
})
return labels
}
function collectSimpleList(containerId, attr) {
const container = el(containerId)
if (!container) return []
const out = []
container.querySelectorAll(`[${attr}]`).forEach((input) => {
const v = input.value.trim()
if (v) out.push(v)
})
return out
}
/**
* Build deployContainer payload from the add-container form.
* @returns {object}
*/
export function collectAddContainerForm() {
const networkMode = val('addc-network-mode') || 'bridge'
const customNet = val('addc-custom-network')
const labels = collectLabels()
let customNetwork = null
if (customNet && networkMode !== 'host' && networkMode !== 'none') {
customNetwork = customNet
}
const data = {
source: 'add-container',
containerName: val('addc-name'),
image: val('addc-image'),
alwaysPull: checked('addc-always-pull'),
publishAllPorts: checked('addc-publish-all'),
command: val('addc-command') || null,
entrypoint: val('addc-entrypoint') || null,
workingDir: val('addc-workdir') || null,
networkMode,
customNetwork,
hostname: val('addc-hostname') || null,
domainname: val('addc-domainname') || null,
ports: collectPorts(),
dns: collectSimpleList('addc-dns', 'data-dns-id'),
extraHosts: collectSimpleList('addc-extra-hosts', 'data-host-id'),
volumes: collectVolumes(),
env: collectEnv(),
labels: Object.keys(labels).length ? labels : null,
user: val('addc-user') || null,
privileged: checked('addc-privileged'),
readonlyRootfs: checked('addc-readonly-rootfs'),
capabilities: collectSimpleList('addc-capabilities', 'data-cap-id'),
restartPolicy: val('addc-restart-policy') || 'no',
restartMaxRetries: parseInt(el('addc-restart-max-retries')?.value, 10) || null,
autoRemove: checked('addc-auto-remove'),
tty: checked('addc-tty'),
stdinOpen: checked('addc-stdin-open'),
init: checked('addc-init'),
cpuLimit: numOrNull('addc-cpu-limit'),
memoryLimit: numOrNull('addc-memory-limit', (x) => parseInt(x, 10)),
memoryReservation: numOrNull('addc-memory-reservation', (x) => parseInt(x, 10)),
shmSize: numOrNull('addc-shm-size', (x) => parseInt(x, 10)),
healthCmd: val('addc-health-cmd') || null,
logDriver: val('addc-log-driver') || null,
}
Object.keys(data).forEach((key) => {
if (
data[key] === null ||
data[key] === '' ||
(Array.isArray(data[key]) && data[key].length === 0)
) {
delete data[key]
}
})
// Keep boolean alwaysPull even when false
data.alwaysPull = checked('addc-always-pull')
data.publishAllPorts = checked('addc-publish-all')
data.source = 'add-container'
return data
}
/**
* @param {object} payload
*/
async function confirmReplaceIfNeeded(payload) {
if (payload.replace) return payload
if (typeof window.peardockOps?.askUserConfirm !== 'function') return payload
// Server returns conflict; caller may also pre-check. Keep replace off until conflict.
return payload
}
/**
* Submit create form via job stepper.
* @returns {Promise