fix(sync): persist and cache device names so they are never lost
CI / Build & Test (push) Successful in 4m18s

- state.js: add lastLoadedDeviceNames cache, getDeviceNames(),
  setDeviceNames(); saveStateSync() always merges and writes
  deviceNames so saves never strip them.
- holesail-manager: include deviceNames in saveState() and
  getStateSnapshot(); applySnapshotData() updates cache from
  synced snapshot. buildDefaultState and legacy migration
  include deviceNames: {}.
Prevents hostnames reverting to "Device 1" etc. after saves or sync.
This commit is contained in:
Raven Scott
2026-03-15 05:33:08 -04:00
parent f1fbc6f01f
commit db7bf8c864
4 changed files with 35 additions and 9 deletions
+2 -2
View File
@@ -601,7 +601,7 @@
<button class="btn btn-secondary" id="btnCopyInvite" style="display:none;">
Copy invite
</button>
<div style="display:flex;align-items:center;gap:8px;flex:1;min-width:200px;">
<div id="syncLinkDeviceForm" style="display:flex;align-items:center;gap:8px;flex:1;min-width:200px;">
<input type="text" id="syncInviteInput" class="input" placeholder="Paste invite from other device" style="flex:1;" />
<button class="btn btn-primary" id="btnPairWithInvite">Link device</button>
</div>
@@ -611,7 +611,7 @@
</div>
<div class="card" id="syncLinkedDevicesCard" style="display:none;margin-bottom:16px;">
<div class="card-body flush">
<div class="section-heading" style="padding:0 0 10px 0;">Linked devices</div>
<div class="section-heading" style="padding:0 16px 10px 16px;">Linked devices</div>
<table class="table">
<thead>
<tr>
+4
View File
@@ -17,15 +17,18 @@ function updateSyncStatus() {
const btnCopy = $('btnCopyInvite');
const inviteDisplay = $('syncInviteDisplay');
if (!text) return;
const linkDeviceForm = $('syncLinkDeviceForm');
if (!response || response.error) {
if (dot) dot.style.background = 'var(--text4)';
text.textContent = 'Not linked';
if (lastSynced) lastSynced.textContent = '';
if (btnCopy) btnCopy.style.display = 'none';
if (inviteDisplay) inviteDisplay.style.display = 'none';
if (linkDeviceForm) linkDeviceForm.style.display = 'flex';
return;
}
if (response.linked) {
if (linkDeviceForm) linkDeviceForm.style.display = 'none';
if (dot) dot.style.background = 'var(--green)';
text.textContent = 'Linked';
if (response.lastSyncedAt) {
@@ -69,6 +72,7 @@ function updateSyncStatus() {
if (lastSynced) lastSynced.textContent = '';
if (btnCopy) btnCopy.style.display = 'none';
if (inviteDisplay) inviteDisplay.style.display = 'none';
if (linkDeviceForm) linkDeviceForm.style.display = 'flex';
const card = $('syncLinkedDevicesCard');
const tbody = $('syncLinkedDevicesTable');
if (card) card.style.display = 'none';
+5 -2
View File
@@ -65,7 +65,8 @@ function saveState() {
virtualHosts: virtualHostsList,
serviceTunnels: serviceTunnelsList,
sshConnections: connectionsModule.getSshConnections(),
rdpConnections: connectionsModule.getRdpConnections()
rdpConnections: connectionsModule.getRdpConnections(),
deviceNames: stateModule.getDeviceNames()
};
stateModule.saveStateSync(snapshot);
if (onStateSaved) onStateSaved(snapshot);
@@ -94,7 +95,8 @@ function getStateSnapshot() {
virtualHosts: virtualHostsList,
serviceTunnels: serviceTunnelsList,
sshConnections: connectionsModule.getSshConnections(),
rdpConnections: connectionsModule.getRdpConnections()
rdpConnections: connectionsModule.getRdpConnections(),
deviceNames: stateModule.getDeviceNames()
};
}
@@ -111,6 +113,7 @@ function applySnapshotData(snapshot) {
connectionsModule.applyLoaded(ssh, rdp);
if (typeof snapshot.nextServerId === 'number') serversModule.applyLoaded(snapshot.nextServerId);
if (typeof snapshot.nextServiceTunnelId === 'number') svcModule.applyLoaded(snapshot.nextServiceTunnelId);
if (snapshot.deviceNames != null && typeof snapshot.deviceNames === 'object') stateModule.setDeviceNames(snapshot.deviceNames);
}
// Inject the shared saveState and emit callbacks into each sub-module
+24 -5
View File
@@ -34,6 +34,8 @@ function debugLog(...args) {
}
let stateFilePath = null;
/** In-memory cache of device names (deviceId -> hostname) so they are never lost when saving. */
let lastLoadedDeviceNames = {};
function setStoragePath(baseDir) {
if (baseDir && typeof baseDir === 'string') {
@@ -72,13 +74,27 @@ function buildDefaultState() {
sshConnections: [],
rdpConnections: [],
nextServerId: 0,
nextServiceTunnelId: 0
nextServiceTunnelId: 0,
deviceNames: {}
};
}
function getDeviceNames() {
return { ...lastLoadedDeviceNames };
}
function setDeviceNames(obj) {
if (obj != null && typeof obj === 'object') {
lastLoadedDeviceNames = { ...obj };
}
}
function saveStateSync(data) {
const file = getStatePath();
const json = JSON.stringify(data, null, 2);
const deviceNames = (data.deviceNames && typeof data.deviceNames === 'object') ? data.deviceNames : lastLoadedDeviceNames;
lastLoadedDeviceNames = { ...deviceNames };
const toWrite = { ...data, deviceNames: lastLoadedDeviceNames };
const json = JSON.stringify(toWrite, null, 2);
try {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, json, 'utf8');
@@ -97,6 +113,7 @@ function loadState() {
if (!trimmed) return buildDefaultState();
const data = JSON.parse(raw);
if (!data || typeof data !== 'object') return buildDefaultState();
lastLoadedDeviceNames = (data.deviceNames && typeof data.deviceNames === 'object') ? { ...data.deviceNames } : {};
const out = {
settings: (data.settings && typeof data.settings === 'object') ? { ...SETTINGS_DEFAULTS, ...data.settings } : { ...SETTINGS_DEFAULTS },
servers: Array.isArray(data.servers) ? data.servers : [],
@@ -106,7 +123,7 @@ function loadState() {
rdpConnections: Array.isArray(data.rdpConnections) ? data.rdpConnections : [],
nextServerId: typeof data.nextServerId === 'number' ? data.nextServerId : 0,
nextServiceTunnelId: typeof data.nextServiceTunnelId === 'number' ? data.nextServiceTunnelId : 0,
deviceNames: (data.deviceNames && typeof data.deviceNames === 'object') ? { ...data.deviceNames } : {}
deviceNames: { ...lastLoadedDeviceNames }
};
debugLog('state loaded path=', file, 'servers=', out.servers.length, 'vhosts=', out.virtualHosts.length);
if (process.stderr && (out.servers.length || out.virtualHosts.length)) {
@@ -128,6 +145,7 @@ function loadState() {
if (!trimmed) return buildDefaultState();
const data = JSON.parse(raw);
if (process.stderr) process.stderr.write('[holesail-manager] migrating from ' + legacyFile + ' to ' + file + '\n');
lastLoadedDeviceNames = {};
const out = {
settings: { ...SETTINGS_DEFAULTS },
servers: Array.isArray(data.servers) ? data.servers : [],
@@ -136,7 +154,8 @@ function loadState() {
sshConnections: Array.isArray(data.sshConnections) ? data.sshConnections : [],
rdpConnections: Array.isArray(data.rdpConnections) ? data.rdpConnections : [],
nextServerId: typeof data.nextServerId === 'number' ? data.nextServerId : 0,
nextServiceTunnelId: typeof data.nextServiceTunnelId === 'number' ? data.nextServiceTunnelId : 0
nextServiceTunnelId: typeof data.nextServiceTunnelId === 'number' ? data.nextServiceTunnelId : 0,
deviceNames: {}
};
try { fs.unlinkSync(legacyFile); } catch (_) {}
if (process.stderr) process.stderr.write('[holesail-manager] migration complete, legacy file removed\n');
@@ -150,4 +169,4 @@ function loadState() {
return buildDefaultState();
}
module.exports = { SETTINGS_DEFAULTS, setStoragePath, loadState, saveStateSync, buildDefaultState, ensureStorageDir };
module.exports = { SETTINGS_DEFAULTS, setStoragePath, loadState, saveStateSync, buildDefaultState, ensureStorageDir, getDeviceNames, setDeviceNames };