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:
+11
-2
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user