Stabilize multi-peer UI and fix container start API body
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:
Raven Scott
2026-07-11 16:52:57 -04:00
parent 79f57d556b
commit c49d7301d0
10 changed files with 986 additions and 149 deletions
+11 -2
View File
@@ -10,6 +10,7 @@ import {
savePeers,
listSavedPeers,
getPeersCachePath,
setLastActivePeerId,
} from './peerCache.js'
import { normalizeRpcError, isBackgroundMethod } from './errors.js'
@@ -72,10 +73,12 @@ export class ConnectionManager extends EventEmitter {
this._reconnect.set(id, entry)
}
const shouldActivate = meta.setActive !== false
if (this.connections.has(id)) {
const existing = this.connections.get(id)
if (existing.connected) {
this.setActive(id)
if (shouldActivate) this.setActive(id)
return existing
}
await existing.close().catch(() => {})
@@ -109,7 +112,8 @@ export class ConnectionManager extends EventEmitter {
}
this.connections.set(id, conn)
this.persist()
this.setActive(id)
// Boot restore dials all peers with setActive:false so last-known wins later
if (shouldActivate) this.setActive(id)
this._ensureHealthLoop()
this.emit('connect', conn)
return conn
@@ -122,6 +126,11 @@ export class ConnectionManager extends EventEmitter {
const conn = this.connections.get(id)
if (!conn) return
this.active = conn
try {
setLastActivePeerId(id)
} catch {
// ignore persist failures
}
this.emit('active', conn)
}
+90 -8
View File
@@ -15,6 +15,8 @@ import os from 'os'
export const PEERS_CACHE_VERSION = 1
export const LOCALSTORAGE_KEY = 'peardock_connections'
export const LOCALSTORAGE_FLAG = 'peardock_use_localstorage'
/** Last UI-selected active peer id (12-char prefix) */
export const ACTIVE_PEER_KEY = 'peardock_active_peer_id'
/**
* Absolute path to the peers cache file.
@@ -99,7 +101,7 @@ export function parsePeersPayload(raw) {
const out = {}
for (const [key, value] of Object.entries(source)) {
// Skip meta keys if someone stored a flat object with version
if (key === 'version' || key === 'updatedAt' || key === 'peers') continue
if (key === 'version' || key === 'updatedAt' || key === 'peers' || key === 'activePeerId') continue
const entry = normalizePeerEntry(value, key)
if (!entry) continue
out[entry.id] = {
@@ -114,8 +116,9 @@ export function parsePeersPayload(raw) {
/**
* Build on-disk payload.
* @param {Record<string, { publicKeyHex?: string, topicHex?: string, alias?: string|null, inviteToken?: string|null }>} peersMap
* @param {{ activePeerId?: string|null }} [opts]
*/
export function buildPeersPayload(peersMap) {
export function buildPeersPayload(peersMap, opts = {}) {
const peers = {}
for (const [id, value] of Object.entries(peersMap || {})) {
const entry = normalizePeerEntry(value, id)
@@ -126,11 +129,79 @@ export function buildPeersPayload(peersMap) {
inviteToken: entry.inviteToken,
}
}
return {
const payload = {
version: PEERS_CACHE_VERSION,
updatedAt: new Date().toISOString(),
peers,
}
if (Object.prototype.hasOwnProperty.call(opts, 'activePeerId')) {
if (opts.activePeerId != null && opts.activePeerId !== '') {
payload.activePeerId = String(opts.activePeerId).slice(0, 12)
}
// null/'' → omit (clear)
} else {
// preserve existing disk active id when not provided
const existing = readActivePeerIdFromFile()
if (existing) payload.activePeerId = existing
}
return payload
}
/**
* @returns {string|null}
*/
function readActivePeerIdFromFile() {
const file = getPeersCachePath()
try {
if (!fs.existsSync(file)) return null
const raw = JSON.parse(fs.readFileSync(file, 'utf8'))
const id = raw?.activePeerId
return id ? String(id).slice(0, 12) : null
} catch {
return null
}
}
/**
* Last known active peer id (file first, then localStorage).
* @returns {string|null}
*/
export function getLastActivePeerId() {
const fromFile = readActivePeerIdFromFile()
if (fromFile) return fromFile
try {
if (typeof localStorage !== 'undefined') {
const id = localStorage.getItem(ACTIVE_PEER_KEY)
return id ? String(id).slice(0, 12) : null
}
} catch {
// ignore
}
return null
}
/**
* Persist last active peer id (localStorage + peers.json meta when possible).
* @param {string|null} id
*/
export function setLastActivePeerId(id) {
const next = id ? String(id).slice(0, 12) : null
try {
if (typeof localStorage !== 'undefined') {
if (next) localStorage.setItem(ACTIVE_PEER_KEY, next)
else localStorage.removeItem(ACTIVE_PEER_KEY)
}
} catch {
// ignore
}
// Merge into peers.json without rewriting peer entries
try {
const peers = loadPeers()
if (Object.keys(peers).length === 0 && !next) return
writePeersToFile(peers, { activePeerId: next })
} catch {
// ignore
}
}
function ensureCacheDir() {
@@ -158,11 +229,12 @@ export function readPeersFromFile() {
/**
* Write peers to ~/.config/peardock/cache/peers.json (atomic replace).
* @param {Record<string, object>} peersMap
* @param {{ activePeerId?: string|null }} [opts]
* @returns {boolean}
*/
export function writePeersToFile(peersMap) {
export function writePeersToFile(peersMap, opts = {}) {
const file = getPeersCachePath()
const payload = buildPeersPayload(peersMap)
const payload = buildPeersPayload(peersMap, opts)
const json = JSON.stringify(payload, null, 2)
try {
ensureCacheDir()
@@ -231,11 +303,16 @@ export function loadPeers() {
/**
* Persist peers to file (+ mirror localStorage as secondary cache).
* @param {Record<string, object>} peersMap
* @param {{ activePeerId?: string|null }} [opts]
* @returns {{ ok: boolean, path: string, count: number }}
*/
export function savePeers(peersMap) {
const payload = buildPeersPayload(peersMap)
const ok = writePeersToFile(payload.peers)
export function savePeers(peersMap, opts = {}) {
const payload = buildPeersPayload(peersMap, opts)
// Pass through only when caller set activePeerId; otherwise preserve on disk
const writeOpts = Object.prototype.hasOwnProperty.call(opts, 'activePeerId')
? { activePeerId: opts.activePeerId }
: {}
const ok = writePeersToFile(payload.peers, writeOpts)
// Mirror for quick in-session reads / legacy code paths
try {
@@ -243,6 +320,9 @@ export function savePeers(peersMap) {
// Keep flat map for older readers
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(payload.peers))
localStorage.setItem(LOCALSTORAGE_FLAG, '1')
if (payload.activePeerId) {
localStorage.setItem(ACTIVE_PEER_KEY, payload.activePeerId)
}
}
} catch {
// ignore
@@ -296,4 +376,6 @@ export default {
normalizePeerEntry,
readPeersFromFile,
writePeersToFile,
getLastActivePeerId,
setLastActivePeerId,
}