Stabilize multi-peer UI and fix container start API body
Release rolling / release (push) Successful in 8m25s
Release rolling / release (push) Successful in 8m25s
Keep the containers table scoped to the active peer with a merge store, restore the last active server on boot with a restoring screen, start containers without a Docker API body (v1.24+), and stop ⋮ menus from sticking to the viewport bottom after refreshes.
This commit is contained in:
@@ -41,6 +41,71 @@ export const docker = new Docker({
|
||||
|
||||
export { socketPath as dockerSocketPath }
|
||||
|
||||
/**
|
||||
* Promise wrapper around docker-modem dial.
|
||||
* @param {object} optsf
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
function modemDial(optsf) {
|
||||
return new Promise((resolve, reject) => {
|
||||
docker.modem.dial(optsf, (err, data) => {
|
||||
if (err) reject(err)
|
||||
else resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a container with an empty POST body.
|
||||
*
|
||||
* dockerode always passes `options: {}` into modem.dial for `.start()`.
|
||||
* Some Engine/API combinations (and proxies) still treat that as a non-empty
|
||||
* JSON body, which Docker rejects since API v1.24:
|
||||
* "starting container with non-empty request body was deprecated..."
|
||||
*
|
||||
* Dial without an `options` field so no body is serialized.
|
||||
*
|
||||
* @param {string} id container id or name
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function startContainerNoBody(id) {
|
||||
const cid = String(id || '').replace(/^\//, '')
|
||||
if (!cid) throw new Error('Container id required')
|
||||
await modemDial({
|
||||
// No trailing '?' — avoids query/body serialization of empty opts
|
||||
path: `/containers/${encodeURIComponent(cid)}/start`,
|
||||
method: 'POST',
|
||||
statusCodes: {
|
||||
204: true,
|
||||
304: true, // already started
|
||||
404: 'no such container',
|
||||
500: 'server error',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Body-less container lifecycle POSTs (pause / unpause).
|
||||
* @param {string} id
|
||||
* @param {'pause'|'unpause'} action
|
||||
*/
|
||||
export async function containerLifecycleNoBody(id, action) {
|
||||
const cid = String(id || '').replace(/^\//, '')
|
||||
if (!cid) throw new Error('Container id required')
|
||||
if (action !== 'pause' && action !== 'unpause') {
|
||||
throw new Error(`Invalid lifecycle action: ${action}`)
|
||||
}
|
||||
await modemDial({
|
||||
path: `/containers/${encodeURIComponent(cid)}/${action}`,
|
||||
method: 'POST',
|
||||
statusCodes: {
|
||||
204: true,
|
||||
404: 'no such container',
|
||||
500: 'server error',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize docker.listVolumes() response shapes across API versions.
|
||||
* @param {object|Array} volumesResult
|
||||
|
||||
@@ -5,15 +5,44 @@
|
||||
import { docker, extractVolumesList } from './docker.js'
|
||||
import { peers } from '../core/peer-registry.js'
|
||||
import { Pushes } from '../../shared/protocol.js'
|
||||
import { broadcastContainers } from '../handlers/containers.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
let dockerEventStream = null
|
||||
let reconnectTimer = null
|
||||
let stopped = false
|
||||
let reconnectAttempt = 0
|
||||
/** Coalesce noisy container events into one list broadcast */
|
||||
let containerListBroadcastTimer = null
|
||||
|
||||
const BASE_DELAY_MS = 2000
|
||||
const MAX_DELAY_MS = 30_000
|
||||
/** Only these container actions should refresh the fleet list (not exec/health spam) */
|
||||
const CONTAINER_LIST_ACTIONS = new Set([
|
||||
'create',
|
||||
'start',
|
||||
'stop',
|
||||
'die',
|
||||
'kill',
|
||||
'destroy',
|
||||
'remove',
|
||||
'pause',
|
||||
'unpause',
|
||||
'restart',
|
||||
'rename',
|
||||
'update',
|
||||
'oom',
|
||||
])
|
||||
|
||||
function scheduleContainerListBroadcast() {
|
||||
if (containerListBroadcastTimer) return
|
||||
containerListBroadcastTimer = setTimeout(() => {
|
||||
containerListBroadcastTimer = null
|
||||
broadcastContainers().catch((err) => {
|
||||
logger.debug('container list broadcast failed', { error: err.message })
|
||||
})
|
||||
}, 400)
|
||||
}
|
||||
|
||||
export async function startDockerEventStream() {
|
||||
stopped = false
|
||||
@@ -64,9 +93,13 @@ async function openEventStream() {
|
||||
data: event,
|
||||
})
|
||||
|
||||
// Do not rebroadcast the full list on every exec_*/health_status event —
|
||||
// that thrashed clients (rows flicker / disappear on each refresh).
|
||||
if (event.Type === 'container') {
|
||||
const containers = await docker.listContainers({ all: true })
|
||||
peers.broadcast(Pushes.containers, { type: 'containers', data: containers })
|
||||
const action = String(event.Action || event.status || '').split(':')[0]
|
||||
if (CONTAINER_LIST_ACTIONS.has(action)) {
|
||||
scheduleContainerListBroadcast()
|
||||
}
|
||||
}
|
||||
|
||||
if (event.Type === 'image') {
|
||||
|
||||
Reference in New Issue
Block a user