feat(sync): device label, runtime kind, and Sync UI parity (Browser + Pear)
CI / Build & Test (push) Successful in 4m12s
CI / Build & Test (push) Successful in 4m12s
- Persist optional display name locally; publish effective name + runtime to peers - Map deviceRuntimes (browser extension vs desktop) alongside deviceNames - Dashboard: badges, dirty/focus guards against auto-refresh, hero + Change name
This commit is contained in:
@@ -1019,6 +1019,97 @@
|
|||||||
#terminalContainer .xterm-viewport { background: transparent !important; }
|
#terminalContainer .xterm-viewport { background: transparent !important; }
|
||||||
#terminalContainer .xterm-screen { background: transparent !important; }
|
#terminalContainer .xterm-screen { background: transparent !important; }
|
||||||
|
|
||||||
|
.sync-runtime-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 99px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text3);
|
||||||
|
background: var(--surface);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.sync-runtime-badge--browser {
|
||||||
|
background: var(--cyan-dim);
|
||||||
|
border-color: var(--cyan-mid);
|
||||||
|
color: var(--cyan);
|
||||||
|
}
|
||||||
|
.sync-runtime-badge--desktop {
|
||||||
|
background: var(--amber-dim);
|
||||||
|
border-color: color-mix(in srgb, var(--amber) 35%, transparent);
|
||||||
|
color: var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sync: this device (setup vs named hero) ───────────────────────── */
|
||||||
|
.sync-device-intro {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text3);
|
||||||
|
}
|
||||||
|
.sync-device-card.sync-device-card--named:not(.sync-device-card--editing) {
|
||||||
|
border-color: color-mix(in srgb, var(--cyan) 24%, var(--border));
|
||||||
|
background: linear-gradient(
|
||||||
|
145deg,
|
||||||
|
color-mix(in srgb, var(--cyan) 7%, var(--card)) 0%,
|
||||||
|
var(--card) 52%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.sync-device-hero {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--cyan) 18%, var(--border));
|
||||||
|
background: color-mix(in srgb, var(--cyan) 5%, var(--elevated));
|
||||||
|
}
|
||||||
|
.sync-device-hero__main {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.sync-device-hero-name {
|
||||||
|
font-size: 1.35rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: var(--text1);
|
||||||
|
line-height: 1.25;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.sync-device-hero-line {
|
||||||
|
margin: 6px 0 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text4);
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
.sync-device-edit-wrap {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.sync-device-edit-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.sync-device-input {
|
||||||
|
max-width: 340px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
.sync-device-hint {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text4);
|
||||||
|
}
|
||||||
|
.sync-device-change-btn {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Boot splash (initial load / sync warmup) ───────────────────────── */
|
/* ── Boot splash (initial load / sync warmup) ───────────────────────── */
|
||||||
.boot-splash {
|
.boot-splash {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -601,6 +601,28 @@
|
|||||||
|
|
||||||
<!-- ── Sync page ─────────────────────────────── -->
|
<!-- ── Sync page ─────────────────────────────── -->
|
||||||
<div class="page" id="page-sync">
|
<div class="page" id="page-sync">
|
||||||
|
<div class="card sync-device-card" id="syncDeviceCard" style="margin-bottom:16px;">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="section-heading">This device</div>
|
||||||
|
<p id="syncDeviceIntro" class="sync-device-intro">Custom name is shown to other linked devices and replaces your computer host name when set. Stored only on this machine; the chosen name still syncs in the group like before.</p>
|
||||||
|
<div id="syncDeviceHero" class="sync-device-hero" hidden>
|
||||||
|
<div class="sync-device-hero__main">
|
||||||
|
<div id="syncDeviceNameDisplay" class="sync-device-hero-name"></div>
|
||||||
|
<p class="sync-device-hero-line">This is how this device appears in your sync group.</p>
|
||||||
|
</div>
|
||||||
|
<span class="sync-runtime-badge" id="syncRuntimeBadgeHero" title="How this app runs">—</span>
|
||||||
|
</div>
|
||||||
|
<div id="syncDeviceEditWrap" class="sync-device-edit-wrap">
|
||||||
|
<div class="sync-device-edit-row">
|
||||||
|
<input type="text" id="syncDeviceNameInput" class="input sync-device-input" placeholder="Use computer host name if empty" maxlength="64" autocomplete="off" />
|
||||||
|
<button type="button" class="btn btn-primary" id="btnSaveDeviceName">Save name</button>
|
||||||
|
<span class="sync-runtime-badge" id="syncRuntimeBadgeEdit" title="How this app runs">—</span>
|
||||||
|
</div>
|
||||||
|
<p id="syncDeviceNameHint" class="sync-device-hint"></p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-secondary sync-device-change-btn" id="btnChangeDeviceName" hidden>Change name</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card" style="margin-bottom:16px;">
|
<div class="card" style="margin-bottom:16px;">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="section-heading">Device sync</div>
|
<div class="section-heading">Device sync</div>
|
||||||
|
|||||||
@@ -3,6 +3,96 @@
|
|||||||
* Depends: core/utils.js ($), ui/modal.js (openModal, closeModal, showModalError), ui/toast.js (showToast)
|
* Depends: core/utils.js ($), ui/modal.js (openModal, closeModal, showModalError), ui/toast.js (showToast)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** True after the user edits the device name field; blocks refresh from overwriting until save. */
|
||||||
|
let _syncDeviceNameDirty = false;
|
||||||
|
function _markSyncDeviceNameDirty() { _syncDeviceNameDirty = true; }
|
||||||
|
function _clearSyncDeviceNameDirty() { _syncDeviceNameDirty = false; }
|
||||||
|
|
||||||
|
/** When a custom name exists, user can collapse to hero and reopen the editor. */
|
||||||
|
let _syncDeviceNameEditOpen = false;
|
||||||
|
function _openSyncDeviceNameEdit() { _syncDeviceNameEditOpen = true; }
|
||||||
|
function _closeSyncDeviceNameEdit() { _syncDeviceNameEditOpen = false; }
|
||||||
|
|
||||||
|
/** Last successful getSyncStatus payload (for instant layout when opening "Change name"). */
|
||||||
|
let _lastSyncStatusForDevicePanel = null;
|
||||||
|
|
||||||
|
function syncDevicePanelIsNamed(response) {
|
||||||
|
return (response.localDisplayName || '').trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySyncRuntimeBadges(runtimeKind) {
|
||||||
|
const desktop = runtimeKind === 'desktop';
|
||||||
|
const text = desktop ? 'Desktop app' : 'Browser extension';
|
||||||
|
const cls = 'sync-runtime-badge ' + (desktop ? 'sync-runtime-badge--desktop' : 'sync-runtime-badge--browser');
|
||||||
|
const title = desktop ? 'Desktop app (Pear)' : 'Browser extension';
|
||||||
|
for (const id of ['syncRuntimeBadgeHero', 'syncRuntimeBadgeEdit']) {
|
||||||
|
const el = $(id);
|
||||||
|
if (!el) continue;
|
||||||
|
el.textContent = text;
|
||||||
|
el.className = cls;
|
||||||
|
el.title = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncRuntimeBadgeHtml(runtime) {
|
||||||
|
if (runtime === 'desktop') return ' <span class="sync-runtime-badge sync-runtime-badge--desktop">Desktop app</span>';
|
||||||
|
if (runtime === 'browser-extension') return ' <span class="sync-runtime-badge sync-runtime-badge--browser">Browser extension</span>';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySyncDeviceNamePanel(response) {
|
||||||
|
if (!response || response.ok !== true) return;
|
||||||
|
_lastSyncStatusForDevicePanel = response;
|
||||||
|
|
||||||
|
const input = $('syncDeviceNameInput');
|
||||||
|
const hint = $('syncDeviceNameHint');
|
||||||
|
const hero = $('syncDeviceHero');
|
||||||
|
const editWrap = $('syncDeviceEditWrap');
|
||||||
|
const intro = $('syncDeviceIntro');
|
||||||
|
const display = $('syncDeviceNameDisplay');
|
||||||
|
const btnChange = $('btnChangeDeviceName');
|
||||||
|
const card = $('syncDeviceCard');
|
||||||
|
|
||||||
|
const skipDeviceNameApply =
|
||||||
|
_syncDeviceNameDirty || !!(input && document.activeElement === input);
|
||||||
|
|
||||||
|
if (input && response.localDisplayName !== undefined && !skipDeviceNameApply) {
|
||||||
|
input.value = response.localDisplayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.runtimeKind) {
|
||||||
|
applySyncRuntimeBadges(response.runtimeKind);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNamed = syncDevicePanelIsNamed(response);
|
||||||
|
if (!isNamed) {
|
||||||
|
_closeSyncDeviceNameEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
const showEdit = !isNamed || _syncDeviceNameEditOpen;
|
||||||
|
const showHero = isNamed && !_syncDeviceNameEditOpen;
|
||||||
|
|
||||||
|
if (card) {
|
||||||
|
card.classList.toggle('sync-device-card--named', isNamed);
|
||||||
|
card.classList.toggle('sync-device-card--editing', showEdit);
|
||||||
|
}
|
||||||
|
if (intro) intro.hidden = !!isNamed;
|
||||||
|
if (hero) hero.hidden = !showHero;
|
||||||
|
if (editWrap) editWrap.hidden = !showEdit;
|
||||||
|
if (btnChange) btnChange.hidden = !isNamed || _syncDeviceNameEditOpen;
|
||||||
|
|
||||||
|
if (display && typeof response.effectiveDeviceName === 'string' && !skipDeviceNameApply) {
|
||||||
|
display.textContent = response.effectiveDeviceName || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hint && typeof response.effectiveDeviceName === 'string' && !skipDeviceNameApply) {
|
||||||
|
hint.textContent = response.effectiveDeviceName
|
||||||
|
? ('Shown to other devices as: ' + response.effectiveDeviceName)
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
if (hint) hint.hidden = !showEdit;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the sync status UI from the native host.
|
* Update the sync status UI from the native host.
|
||||||
*/
|
*/
|
||||||
@@ -11,6 +101,7 @@ function updateSyncStatus() {
|
|||||||
{ target: 'holesail-native', action: 'send', payload: { type: 'getSyncStatus' } },
|
{ target: 'holesail-native', action: 'send', payload: { type: 'getSyncStatus' } },
|
||||||
(response) => {
|
(response) => {
|
||||||
if (chrome.runtime.lastError) return;
|
if (chrome.runtime.lastError) return;
|
||||||
|
applySyncDeviceNamePanel(response);
|
||||||
const dot = $('syncStatusDot');
|
const dot = $('syncStatusDot');
|
||||||
const text = $('syncStatusText');
|
const text = $('syncStatusText');
|
||||||
const lastSynced = $('syncLastSynced');
|
const lastSynced = $('syncLastSynced');
|
||||||
@@ -18,7 +109,7 @@ function updateSyncStatus() {
|
|||||||
const inviteDisplay = $('syncInviteDisplay');
|
const inviteDisplay = $('syncInviteDisplay');
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
const linkDeviceForm = $('syncLinkDeviceForm');
|
const linkDeviceForm = $('syncLinkDeviceForm');
|
||||||
if (!response || response.error) {
|
if (!response || response.error || response.ok === false) {
|
||||||
if (dot) dot.style.background = 'var(--text4)';
|
if (dot) dot.style.background = 'var(--text4)';
|
||||||
text.textContent = 'Not linked';
|
text.textContent = 'Not linked';
|
||||||
if (lastSynced) lastSynced.textContent = '';
|
if (lastSynced) lastSynced.textContent = '';
|
||||||
@@ -60,12 +151,14 @@ function updateSyncStatus() {
|
|||||||
linkedDevices.forEach((d, i) => {
|
linkedDevices.forEach((d, i) => {
|
||||||
const label = d.name || (d.isCurrent ? (response.deviceName || 'This device') : ('Device ' + (i + 1)));
|
const label = d.name || (d.isCurrent ? (response.deviceName || 'This device') : ('Device ' + (i + 1)));
|
||||||
const masterBadge = d.isMaster ? ' <span class="sync-master-badge">MASTER</span>' : '';
|
const masterBadge = d.isMaster ? ' <span class="sync-master-badge">MASTER</span>' : '';
|
||||||
rows += '<tr><td>' + escapeHtml(label) + masterBadge + '</td><td class="mono" style="font-size:12px;">' + escapeHtml(d.id || '—') + '</td></tr>';
|
const rt = syncRuntimeBadgeHtml(d.runtime);
|
||||||
|
rows += '<tr><td>' + escapeHtml(label) + masterBadge + rt + '</td><td class="mono" style="font-size:12px;">' + escapeHtml(d.id || '—') + '</td></tr>';
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const curName = response.deviceName || 'This device';
|
const curName = response.deviceName || 'This device';
|
||||||
const masterBadge = response.isMaster ? ' <span class="sync-master-badge">MASTER</span>' : '';
|
const masterBadge = response.isMaster ? ' <span class="sync-master-badge">MASTER</span>' : '';
|
||||||
rows = '<tr><td>' + escapeHtml(curName) + masterBadge + '</td><td class="mono" style="font-size:12px;">' + escapeHtml(response.deviceId || '—') + '</td></tr>';
|
const rt = syncRuntimeBadgeHtml(response.runtimeKind);
|
||||||
|
rows = '<tr><td>' + escapeHtml(curName) + masterBadge + rt + '</td><td class="mono" style="font-size:12px;">' + escapeHtml(response.deviceId || '—') + '</td></tr>';
|
||||||
}
|
}
|
||||||
rows += '<tr><td>Sync group</td><td class="mono" style="font-size:12px;">' + escapeHtml(syncGroupId) + '</td></tr>';
|
rows += '<tr><td>Sync group</td><td class="mono" style="font-size:12px;">' + escapeHtml(syncGroupId) + '</td></tr>';
|
||||||
tbody.innerHTML = rows;
|
tbody.innerHTML = rows;
|
||||||
@@ -96,6 +189,38 @@ function setupSyncEvents() {
|
|||||||
const inviteInput = $('syncInviteInput');
|
const inviteInput = $('syncInviteInput');
|
||||||
const inviteDisplay = $('syncInviteDisplay');
|
const inviteDisplay = $('syncInviteDisplay');
|
||||||
|
|
||||||
|
$('syncDeviceNameInput')?.addEventListener('input', _markSyncDeviceNameDirty);
|
||||||
|
|
||||||
|
$('btnChangeDeviceName')?.addEventListener('click', () => {
|
||||||
|
_openSyncDeviceNameEdit();
|
||||||
|
if (_lastSyncStatusForDevicePanel) {
|
||||||
|
applySyncDeviceNamePanel(_lastSyncStatusForDevicePanel);
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => $('syncDeviceNameInput')?.focus());
|
||||||
|
});
|
||||||
|
|
||||||
|
$('btnSaveDeviceName')?.addEventListener('click', () => {
|
||||||
|
const input = $('syncDeviceNameInput');
|
||||||
|
const name = input && typeof input.value === 'string' ? input.value.trim() : '';
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
{ target: 'holesail-native', action: 'send', payload: { type: 'setDeviceDisplayName', payload: { displayName: name } } },
|
||||||
|
(res) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
showToast('Failed: ' + (chrome.runtime.lastError.message || 'unknown'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res && res.ok) {
|
||||||
|
showToast('Device name saved', 'success');
|
||||||
|
_clearSyncDeviceNameDirty();
|
||||||
|
_closeSyncDeviceNameEdit();
|
||||||
|
updateSyncStatus();
|
||||||
|
} else {
|
||||||
|
showToast(res && res.error ? res.error : 'Could not save name', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
btnCreate?.addEventListener('click', () => {
|
btnCreate?.addEventListener('click', () => {
|
||||||
if (btnCreate.disabled) return;
|
if (btnCreate.disabled) return;
|
||||||
btnCreate.disabled = true;
|
btnCreate.disabled = true;
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ function saveState() {
|
|||||||
serviceTunnels: serviceTunnelsList,
|
serviceTunnels: serviceTunnelsList,
|
||||||
sshConnections: connectionsModule.getSshConnections(),
|
sshConnections: connectionsModule.getSshConnections(),
|
||||||
rdpConnections: connectionsModule.getRdpConnections(),
|
rdpConnections: connectionsModule.getRdpConnections(),
|
||||||
deviceNames: stateModule.getDeviceNames()
|
deviceNames: stateModule.getDeviceNames(),
|
||||||
|
deviceRuntimes: stateModule.getDeviceRuntimes()
|
||||||
};
|
};
|
||||||
stateModule.saveStateSync(snapshot);
|
stateModule.saveStateSync(snapshot);
|
||||||
if (onStateSaved) onStateSaved(snapshot);
|
if (onStateSaved) onStateSaved(snapshot);
|
||||||
@@ -96,7 +97,8 @@ function getStateSnapshot() {
|
|||||||
serviceTunnels: serviceTunnelsList,
|
serviceTunnels: serviceTunnelsList,
|
||||||
sshConnections: connectionsModule.getSshConnections(),
|
sshConnections: connectionsModule.getSshConnections(),
|
||||||
rdpConnections: connectionsModule.getRdpConnections(),
|
rdpConnections: connectionsModule.getRdpConnections(),
|
||||||
deviceNames: stateModule.getDeviceNames()
|
deviceNames: stateModule.getDeviceNames(),
|
||||||
|
deviceRuntimes: stateModule.getDeviceRuntimes()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +116,7 @@ function applySnapshotData(snapshot) {
|
|||||||
if (typeof snapshot.nextServerId === 'number') serversModule.applyLoaded(snapshot.nextServerId);
|
if (typeof snapshot.nextServerId === 'number') serversModule.applyLoaded(snapshot.nextServerId);
|
||||||
if (typeof snapshot.nextServiceTunnelId === 'number') svcModule.applyLoaded(snapshot.nextServiceTunnelId);
|
if (typeof snapshot.nextServiceTunnelId === 'number') svcModule.applyLoaded(snapshot.nextServiceTunnelId);
|
||||||
if (snapshot.deviceNames != null && typeof snapshot.deviceNames === 'object') stateModule.setDeviceNames(snapshot.deviceNames);
|
if (snapshot.deviceNames != null && typeof snapshot.deviceNames === 'object') stateModule.setDeviceNames(snapshot.deviceNames);
|
||||||
|
if (snapshot.deviceRuntimes != null && typeof snapshot.deviceRuntimes === 'object') stateModule.setDeviceRuntimes(snapshot.deviceRuntimes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject the shared saveState and emit callbacks into each sub-module
|
// Inject the shared saveState and emit callbacks into each sub-module
|
||||||
@@ -185,6 +188,7 @@ async function resetStateToDefaults() {
|
|||||||
const defaultState = stateModule.buildDefaultState();
|
const defaultState = stateModule.buildDefaultState();
|
||||||
stateModule.saveStateSync(defaultState);
|
stateModule.saveStateSync(defaultState);
|
||||||
stateModule.setDeviceNames({});
|
stateModule.setDeviceNames({});
|
||||||
|
stateModule.setDeviceRuntimes({});
|
||||||
return restorePersistedState();
|
return restorePersistedState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +202,7 @@ module.exports = {
|
|||||||
ensureStorageDir,
|
ensureStorageDir,
|
||||||
setOnStateSaved,
|
setOnStateSaved,
|
||||||
setStateSaveSuppressed,
|
setStateSaveSuppressed,
|
||||||
|
saveState,
|
||||||
getStateSnapshot,
|
getStateSnapshot,
|
||||||
applySnapshotData,
|
applySnapshotData,
|
||||||
resetStateToDefaults,
|
resetStateToDefaults,
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ function debugLog(...args) {
|
|||||||
let stateFilePath = null;
|
let stateFilePath = null;
|
||||||
/** In-memory cache of device names (deviceId -> hostname) so they are never lost when saving. */
|
/** In-memory cache of device names (deviceId -> hostname) so they are never lost when saving. */
|
||||||
let lastLoadedDeviceNames = {};
|
let lastLoadedDeviceNames = {};
|
||||||
|
/** Synced map: deviceId -> 'browser-extension' | 'desktop'. */
|
||||||
|
let lastLoadedDeviceRuntimes = {};
|
||||||
|
|
||||||
function setStoragePath(baseDir) {
|
function setStoragePath(baseDir) {
|
||||||
if (baseDir && typeof baseDir === 'string') {
|
if (baseDir && typeof baseDir === 'string') {
|
||||||
@@ -76,7 +78,8 @@ function buildDefaultState() {
|
|||||||
rdpConnections: [],
|
rdpConnections: [],
|
||||||
nextServerId: 0,
|
nextServerId: 0,
|
||||||
nextServiceTunnelId: 0,
|
nextServiceTunnelId: 0,
|
||||||
deviceNames: {}
|
deviceNames: {},
|
||||||
|
deviceRuntimes: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,11 +93,23 @@ function setDeviceNames(obj) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDeviceRuntimes() {
|
||||||
|
return { ...lastLoadedDeviceRuntimes };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDeviceRuntimes(obj) {
|
||||||
|
if (obj != null && typeof obj === 'object') {
|
||||||
|
lastLoadedDeviceRuntimes = { ...obj };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function saveStateSync(data) {
|
function saveStateSync(data) {
|
||||||
const file = getStatePath();
|
const file = getStatePath();
|
||||||
const deviceNames = (data.deviceNames && typeof data.deviceNames === 'object') ? data.deviceNames : lastLoadedDeviceNames;
|
const deviceNames = (data.deviceNames && typeof data.deviceNames === 'object') ? data.deviceNames : lastLoadedDeviceNames;
|
||||||
lastLoadedDeviceNames = { ...deviceNames };
|
lastLoadedDeviceNames = { ...deviceNames };
|
||||||
const toWrite = { ...data, deviceNames: lastLoadedDeviceNames };
|
const deviceRuntimes = (data.deviceRuntimes && typeof data.deviceRuntimes === 'object') ? data.deviceRuntimes : lastLoadedDeviceRuntimes;
|
||||||
|
lastLoadedDeviceRuntimes = { ...deviceRuntimes };
|
||||||
|
const toWrite = { ...data, deviceNames: lastLoadedDeviceNames, deviceRuntimes: lastLoadedDeviceRuntimes };
|
||||||
const json = JSON.stringify(toWrite, null, 2);
|
const json = JSON.stringify(toWrite, null, 2);
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||||
@@ -115,6 +130,7 @@ function loadState() {
|
|||||||
const data = JSON.parse(raw);
|
const data = JSON.parse(raw);
|
||||||
if (!data || typeof data !== 'object') return buildDefaultState();
|
if (!data || typeof data !== 'object') return buildDefaultState();
|
||||||
lastLoadedDeviceNames = (data.deviceNames && typeof data.deviceNames === 'object') ? { ...data.deviceNames } : {};
|
lastLoadedDeviceNames = (data.deviceNames && typeof data.deviceNames === 'object') ? { ...data.deviceNames } : {};
|
||||||
|
lastLoadedDeviceRuntimes = (data.deviceRuntimes && typeof data.deviceRuntimes === 'object') ? { ...data.deviceRuntimes } : {};
|
||||||
const out = {
|
const out = {
|
||||||
settings: (data.settings && typeof data.settings === 'object') ? { ...SETTINGS_DEFAULTS, ...data.settings } : { ...SETTINGS_DEFAULTS },
|
settings: (data.settings && typeof data.settings === 'object') ? { ...SETTINGS_DEFAULTS, ...data.settings } : { ...SETTINGS_DEFAULTS },
|
||||||
servers: Array.isArray(data.servers) ? data.servers : [],
|
servers: Array.isArray(data.servers) ? data.servers : [],
|
||||||
@@ -124,7 +140,8 @@ function loadState() {
|
|||||||
rdpConnections: Array.isArray(data.rdpConnections) ? data.rdpConnections : [],
|
rdpConnections: Array.isArray(data.rdpConnections) ? data.rdpConnections : [],
|
||||||
nextServerId: typeof data.nextServerId === 'number' ? data.nextServerId : 0,
|
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: { ...lastLoadedDeviceNames }
|
deviceNames: { ...lastLoadedDeviceNames },
|
||||||
|
deviceRuntimes: { ...lastLoadedDeviceRuntimes }
|
||||||
};
|
};
|
||||||
debugLog('state loaded path=', file, 'servers=', out.servers.length, 'vhosts=', out.virtualHosts.length);
|
debugLog('state loaded path=', file, 'servers=', out.servers.length, 'vhosts=', out.virtualHosts.length);
|
||||||
if (process.stderr && (out.servers.length || out.virtualHosts.length)) {
|
if (process.stderr && (out.servers.length || out.virtualHosts.length)) {
|
||||||
@@ -147,6 +164,7 @@ function loadState() {
|
|||||||
const data = JSON.parse(raw);
|
const data = JSON.parse(raw);
|
||||||
if (process.stderr) process.stderr.write('[holesail-manager] migrating from ' + legacyFile + ' to ' + file + '\n');
|
if (process.stderr) process.stderr.write('[holesail-manager] migrating from ' + legacyFile + ' to ' + file + '\n');
|
||||||
lastLoadedDeviceNames = {};
|
lastLoadedDeviceNames = {};
|
||||||
|
lastLoadedDeviceRuntimes = {};
|
||||||
const out = {
|
const out = {
|
||||||
settings: { ...SETTINGS_DEFAULTS },
|
settings: { ...SETTINGS_DEFAULTS },
|
||||||
servers: Array.isArray(data.servers) ? data.servers : [],
|
servers: Array.isArray(data.servers) ? data.servers : [],
|
||||||
@@ -156,7 +174,8 @@ function loadState() {
|
|||||||
rdpConnections: Array.isArray(data.rdpConnections) ? data.rdpConnections : [],
|
rdpConnections: Array.isArray(data.rdpConnections) ? data.rdpConnections : [],
|
||||||
nextServerId: typeof data.nextServerId === 'number' ? data.nextServerId : 0,
|
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: {}
|
deviceNames: {},
|
||||||
|
deviceRuntimes: {}
|
||||||
};
|
};
|
||||||
try { fs.unlinkSync(legacyFile); } catch (_) {}
|
try { fs.unlinkSync(legacyFile); } catch (_) {}
|
||||||
if (process.stderr) process.stderr.write('[holesail-manager] migration complete, legacy file removed\n');
|
if (process.stderr) process.stderr.write('[holesail-manager] migration complete, legacy file removed\n');
|
||||||
@@ -170,4 +189,15 @@ function loadState() {
|
|||||||
return buildDefaultState();
|
return buildDefaultState();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { SETTINGS_DEFAULTS, setStoragePath, loadState, saveStateSync, buildDefaultState, ensureStorageDir, getDeviceNames, setDeviceNames };
|
module.exports = {
|
||||||
|
SETTINGS_DEFAULTS,
|
||||||
|
setStoragePath,
|
||||||
|
loadState,
|
||||||
|
saveStateSync,
|
||||||
|
buildDefaultState,
|
||||||
|
ensureStorageDir,
|
||||||
|
getDeviceNames,
|
||||||
|
setDeviceNames,
|
||||||
|
getDeviceRuntimes,
|
||||||
|
setDeviceRuntimes
|
||||||
|
};
|
||||||
|
|||||||
@@ -61,6 +61,17 @@ function register(deps) {
|
|||||||
reply({ ok: false, error: e.message });
|
reply({ ok: false, error: e.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'setDeviceDisplayName',
|
||||||
|
handle: async (payload, reply) => {
|
||||||
|
try {
|
||||||
|
const result = syncManager.setDeviceDisplayName(payload || {});
|
||||||
|
reply(result);
|
||||||
|
} catch (e) {
|
||||||
|
reply({ ok: false, error: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,50 @@ function getIdentityPath() {
|
|||||||
return path.join(storageDir, IDENTITY_FILENAME);
|
return path.join(storageDir, IDENTITY_FILENAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LOCAL_DEVICE_DISPLAY_FILENAME = 'device-display-name.json';
|
||||||
|
|
||||||
|
function getLocalDeviceDisplayNamePath() {
|
||||||
|
if (!storageDir) return null;
|
||||||
|
return path.join(storageDir, LOCAL_DEVICE_DISPLAY_FILENAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readLocalDeviceDisplayName() {
|
||||||
|
const p = getLocalDeviceDisplayNamePath();
|
||||||
|
if (!p || !fs.existsSync(p)) return '';
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(p, 'utf8');
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
if (data && typeof data.displayName === 'string') {
|
||||||
|
return data.displayName.trim().slice(0, 64);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeLocalDeviceDisplayName(name) {
|
||||||
|
const p = getLocalDeviceDisplayNamePath();
|
||||||
|
if (!p) return { ok: false, error: 'Storage path not set' };
|
||||||
|
const trimmed = typeof name === 'string' ? name.trim().slice(0, 64) : '';
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||||
|
fs.writeFileSync(p, JSON.stringify({ displayName: trimmed }, null, 2), 'utf8');
|
||||||
|
return { ok: true };
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEffectiveDeviceName() {
|
||||||
|
const custom = readLocalDeviceDisplayName();
|
||||||
|
if (custom) return custom;
|
||||||
|
return typeof os.hostname === 'function' ? os.hostname() : 'device';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {'browser-extension'|'desktop'} */
|
||||||
|
function getLocalRuntimeKind() {
|
||||||
|
return process.env.HOLESAIL_EMBEDDED === '1' ? 'desktop' : 'browser-extension';
|
||||||
|
}
|
||||||
|
|
||||||
function loadIdentity() {
|
function loadIdentity() {
|
||||||
const file = getIdentityPath();
|
const file = getIdentityPath();
|
||||||
if (!file || !fs.existsSync(file)) return null;
|
if (!file || !fs.existsSync(file)) return null;
|
||||||
@@ -310,9 +354,13 @@ async function applySyncedStateIncremental(snapshot) {
|
|||||||
setStateSaveSuppressed(true);
|
setStateSaveSuppressed(true);
|
||||||
try {
|
try {
|
||||||
snapshot.deviceNames = snapshot.deviceNames || {};
|
snapshot.deviceNames = snapshot.deviceNames || {};
|
||||||
|
snapshot.deviceRuntimes = snapshot.deviceRuntimes || {};
|
||||||
if (pass && pass.writerKey) {
|
if (pass && pass.writerKey) {
|
||||||
const myId = shortId(pass.writerKey);
|
const myId = shortId(pass.writerKey);
|
||||||
if (myId) snapshot.deviceNames[myId] = typeof os.hostname === 'function' ? os.hostname() : 'device';
|
if (myId) {
|
||||||
|
snapshot.deviceNames[myId] = getEffectiveDeviceName();
|
||||||
|
snapshot.deviceRuntimes[myId] = getLocalRuntimeKind();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const current = getStateSnapshot();
|
const current = getStateSnapshot();
|
||||||
const settings = holesailManager.getSettings ? holesailManager.getSettings() : {};
|
const settings = holesailManager.getSettings ? holesailManager.getSettings() : {};
|
||||||
@@ -399,9 +447,13 @@ async function applySyncedStateFull(snapshot) {
|
|||||||
applyingSync = true;
|
applyingSync = true;
|
||||||
try {
|
try {
|
||||||
snapshot.deviceNames = snapshot.deviceNames || {};
|
snapshot.deviceNames = snapshot.deviceNames || {};
|
||||||
|
snapshot.deviceRuntimes = snapshot.deviceRuntimes || {};
|
||||||
if (pass && pass.writerKey) {
|
if (pass && pass.writerKey) {
|
||||||
const myId = shortId(pass.writerKey);
|
const myId = shortId(pass.writerKey);
|
||||||
if (myId) snapshot.deviceNames[myId] = typeof os.hostname === 'function' ? os.hostname() : 'device';
|
if (myId) {
|
||||||
|
snapshot.deviceNames[myId] = getEffectiveDeviceName();
|
||||||
|
snapshot.deviceRuntimes[myId] = getLocalRuntimeKind();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const settings = holesailManager.getSettings ? holesailManager.getSettings() : {};
|
const settings = holesailManager.getSettings ? holesailManager.getSettings() : {};
|
||||||
if (settings.syncServers !== true) {
|
if (settings.syncServers !== true) {
|
||||||
@@ -446,7 +498,9 @@ function onStateSaved(snapshot) {
|
|||||||
const myId = shortId(pass.writerKey);
|
const myId = shortId(pass.writerKey);
|
||||||
if (myId) {
|
if (myId) {
|
||||||
snapshot.deviceNames = snapshot.deviceNames || {};
|
snapshot.deviceNames = snapshot.deviceNames || {};
|
||||||
snapshot.deviceNames[myId] = typeof os.hostname === 'function' ? os.hostname() : 'device';
|
snapshot.deviceRuntimes = snapshot.deviceRuntimes || {};
|
||||||
|
snapshot.deviceNames[myId] = getEffectiveDeviceName();
|
||||||
|
snapshot.deviceRuntimes[myId] = getLocalRuntimeKind();
|
||||||
}
|
}
|
||||||
const identity = loadIdentity();
|
const identity = loadIdentity();
|
||||||
if (identity && identity.isMaster) {
|
if (identity && identity.isMaster) {
|
||||||
@@ -497,6 +551,18 @@ function readDeviceNamesFromState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readDeviceRuntimesFromState() {
|
||||||
|
const statePath = getStateFilePath();
|
||||||
|
if (!statePath || !fs.existsSync(statePath)) return {};
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(statePath, 'utf8');
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
return (data.deviceRuntimes && typeof data.deviceRuntimes === 'object') ? data.deviceRuntimes : {};
|
||||||
|
} catch (_) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function readSyncMasterDeviceIdFromState() {
|
function readSyncMasterDeviceIdFromState() {
|
||||||
const statePath = getStateFilePath();
|
const statePath = getStateFilePath();
|
||||||
if (!statePath || !fs.existsSync(statePath)) return null;
|
if (!statePath || !fs.existsSync(statePath)) return null;
|
||||||
@@ -512,10 +578,14 @@ function readSyncMasterDeviceIdFromState() {
|
|||||||
|
|
||||||
async function getSyncStatus() {
|
async function getSyncStatus() {
|
||||||
await ensureInitialized();
|
await ensureInitialized();
|
||||||
|
const effName = getEffectiveDeviceName();
|
||||||
const out = {
|
const out = {
|
||||||
linked: !!pass,
|
linked: !!pass,
|
||||||
invite: currentInvite || null,
|
invite: currentInvite || null,
|
||||||
lastSyncedAt: lastSyncedAt || null
|
lastSyncedAt: lastSyncedAt || null,
|
||||||
|
localDisplayName: readLocalDeviceDisplayName(),
|
||||||
|
runtimeKind: getLocalRuntimeKind(),
|
||||||
|
effectiveDeviceName: effName
|
||||||
};
|
};
|
||||||
if (pass) {
|
if (pass) {
|
||||||
try {
|
try {
|
||||||
@@ -523,10 +593,11 @@ async function getSyncStatus() {
|
|||||||
out.isMaster = identity && identity.isMaster === true;
|
out.isMaster = identity && identity.isMaster === true;
|
||||||
out.deviceId = shortId(pass.writerKey) || null;
|
out.deviceId = shortId(pass.writerKey) || null;
|
||||||
out.syncGroupId = shortId(pass.discoveryKey) || null;
|
out.syncGroupId = shortId(pass.discoveryKey) || null;
|
||||||
out.deviceName = typeof os.hostname === 'function' ? os.hostname() : null;
|
out.deviceName = effName;
|
||||||
out.linkedDevices = [];
|
out.linkedDevices = [];
|
||||||
const myKeyHex = b4a.toString(pass.writerKey, 'hex');
|
const myKeyHex = b4a.toString(pass.writerKey, 'hex');
|
||||||
const deviceNames = readDeviceNamesFromState();
|
const deviceNames = readDeviceNamesFromState();
|
||||||
|
const deviceRuntimes = readDeviceRuntimesFromState();
|
||||||
const masterDeviceId = readSyncMasterDeviceIdFromState();
|
const masterDeviceId = readSyncMasterDeviceIdFromState();
|
||||||
if (pass.base && pass.base.activeWriters) {
|
if (pass.base && pass.base.activeWriters) {
|
||||||
for (const w of pass.base.activeWriters) {
|
for (const w of pass.base.activeWriters) {
|
||||||
@@ -538,16 +609,24 @@ async function getSyncStatus() {
|
|||||||
id,
|
id,
|
||||||
isCurrent,
|
isCurrent,
|
||||||
isMaster: id === masterDeviceId,
|
isMaster: id === masterDeviceId,
|
||||||
name: deviceNames[id] || (isCurrent ? out.deviceName : null)
|
name: deviceNames[id] || (isCurrent ? out.deviceName : null),
|
||||||
|
runtime: deviceRuntimes[id] || null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (out.linkedDevices.length === 0 && out.deviceId) {
|
if (out.linkedDevices.length === 0 && out.deviceId) {
|
||||||
out.linkedDevices = [{ id: out.deviceId, isCurrent: true, isMaster: out.deviceId === masterDeviceId || out.isMaster, name: out.deviceName }];
|
out.linkedDevices = [{
|
||||||
|
id: out.deviceId,
|
||||||
|
isCurrent: true,
|
||||||
|
isMaster: out.deviceId === masterDeviceId || out.isMaster,
|
||||||
|
name: out.deviceName,
|
||||||
|
runtime: deviceRuntimes[out.deviceId] || getLocalRuntimeKind()
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
// Ensure our hostname is published so other devices see our name (fire-and-forget)
|
// Ensure our display name and runtime are published so other devices see them (fire-and-forget)
|
||||||
const myId = out.deviceId;
|
const myId = out.deviceId;
|
||||||
const currentName = out.deviceName;
|
const currentName = out.deviceName;
|
||||||
|
const currentRuntime = getLocalRuntimeKind();
|
||||||
if (myId && currentName) {
|
if (myId && currentName) {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -555,10 +634,20 @@ async function getSyncStatus() {
|
|||||||
if (!statePath || !fs.existsSync(statePath) || !pass) return;
|
if (!statePath || !fs.existsSync(statePath) || !pass) return;
|
||||||
const raw = fs.readFileSync(statePath, 'utf8');
|
const raw = fs.readFileSync(statePath, 'utf8');
|
||||||
const data = JSON.parse(raw);
|
const data = JSON.parse(raw);
|
||||||
const deviceNames = (data.deviceNames && typeof data.deviceNames === 'object') ? { ...data.deviceNames } : {};
|
const dn = (data.deviceNames && typeof data.deviceNames === 'object') ? { ...data.deviceNames } : {};
|
||||||
if (deviceNames[myId] === currentName) return;
|
const dr = (data.deviceRuntimes && typeof data.deviceRuntimes === 'object') ? { ...data.deviceRuntimes } : {};
|
||||||
deviceNames[myId] = currentName;
|
let changed = false;
|
||||||
data.deviceNames = deviceNames;
|
if (dn[myId] !== currentName) {
|
||||||
|
dn[myId] = currentName;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (dr[myId] !== currentRuntime) {
|
||||||
|
dr[myId] = currentRuntime;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (!changed) return;
|
||||||
|
data.deviceNames = dn;
|
||||||
|
data.deviceRuntimes = dr;
|
||||||
fs.writeFileSync(statePath, JSON.stringify(data, null, 2), 'utf8');
|
fs.writeFileSync(statePath, JSON.stringify(data, null, 2), 'utf8');
|
||||||
if (!pass) return;
|
if (!pass) return;
|
||||||
const value = JSON.stringify(data);
|
const value = JSON.stringify(data);
|
||||||
@@ -571,7 +660,7 @@ async function getSyncStatus() {
|
|||||||
out.deviceId = null;
|
out.deviceId = null;
|
||||||
out.syncGroupId = null;
|
out.syncGroupId = null;
|
||||||
out.deviceName = null;
|
out.deviceName = null;
|
||||||
out.linkedDevices = out.deviceId ? [{ id: out.deviceId, isCurrent: true, name: null }] : [];
|
out.linkedDevices = out.deviceId ? [{ id: out.deviceId, isCurrent: true, name: null, runtime: null }] : [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
@@ -754,6 +843,8 @@ async function disband() {
|
|||||||
const snapshot = holesailManager.getStateSnapshot();
|
const snapshot = holesailManager.getStateSnapshot();
|
||||||
const deviceNames = readDeviceNamesFromState();
|
const deviceNames = readDeviceNamesFromState();
|
||||||
if (Object.keys(deviceNames).length > 0) snapshot.deviceNames = deviceNames;
|
if (Object.keys(deviceNames).length > 0) snapshot.deviceNames = deviceNames;
|
||||||
|
const deviceRuntimes = readDeviceRuntimesFromState();
|
||||||
|
if (Object.keys(deviceRuntimes).length > 0) snapshot.deviceRuntimes = deviceRuntimes;
|
||||||
delete snapshot.syncGroupDisbanded;
|
delete snapshot.syncGroupDisbanded;
|
||||||
if (statePath) {
|
if (statePath) {
|
||||||
try {
|
try {
|
||||||
@@ -817,6 +908,27 @@ function hasSyncIdentity() {
|
|||||||
return !!loadIdentity();
|
return !!loadIdentity();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist a custom device display name locally (not synced as a file — the effective name syncs via deviceNames).
|
||||||
|
* @param {{ displayName?: string }} payload
|
||||||
|
* @returns {{ ok: boolean, effectiveName?: string, runtimeKind?: string, error?: string }}
|
||||||
|
*/
|
||||||
|
function setDeviceDisplayName(payload) {
|
||||||
|
if (!storageDir || !holesailManager) return { ok: false, error: 'Storage path or holesail manager not set' };
|
||||||
|
const raw = payload && typeof payload.displayName === 'string' ? payload.displayName : '';
|
||||||
|
if (raw.length > 64) return { ok: false, error: 'displayName must be at most 64 characters' };
|
||||||
|
const w = writeLocalDeviceDisplayName(raw);
|
||||||
|
if (!w.ok) return w;
|
||||||
|
if (typeof holesailManager.saveState === 'function') {
|
||||||
|
holesailManager.saveState();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
effectiveName: getEffectiveDeviceName(),
|
||||||
|
runtimeKind: getLocalRuntimeKind()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
setStoragePath,
|
setStoragePath,
|
||||||
setDeps,
|
setDeps,
|
||||||
@@ -830,5 +942,6 @@ module.exports = {
|
|||||||
closeSyncForBackup,
|
closeSyncForBackup,
|
||||||
cleanup,
|
cleanup,
|
||||||
warmupSyncAtStartup,
|
warmupSyncAtStartup,
|
||||||
hasSyncIdentity
|
hasSyncIdentity,
|
||||||
|
setDeviceDisplayName
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ function makeMockDeps(overrides = {}) {
|
|||||||
createSyncInvite: async () => ({}),
|
createSyncInvite: async () => ({}),
|
||||||
pairWithInvite: async () => ({}),
|
pairWithInvite: async () => ({}),
|
||||||
warmupSyncAtStartup: async () => {},
|
warmupSyncAtStartup: async () => {},
|
||||||
hasSyncIdentity: () => false
|
hasSyncIdentity: () => false,
|
||||||
|
setDeviceDisplayName: () => ({ ok: true })
|
||||||
},
|
},
|
||||||
...overrides
|
...overrides
|
||||||
};
|
};
|
||||||
@@ -92,7 +93,7 @@ describe('handler registry', () => {
|
|||||||
'startSshSession', 'stopSshSession', 'resizeSshSession', 'getSshSessions',
|
'startSshSession', 'stopSshSession', 'resizeSshSession', 'getSshSessions',
|
||||||
'startRdpSession', 'stopRdpSession', 'getRdpSessions',
|
'startRdpSession', 'stopRdpSession', 'getRdpSessions',
|
||||||
'createBackup', 'listBackups', 'restoreBackup', 'deleteBackup',
|
'createBackup', 'listBackups', 'restoreBackup', 'deleteBackup',
|
||||||
'getSyncStatus', 'createSyncInvite', 'pairWithInvite'
|
'getSyncStatus', 'createSyncInvite', 'pairWithInvite', 'setDeviceDisplayName'
|
||||||
];
|
];
|
||||||
for (const type of expectedTypes) {
|
for (const type of expectedTypes) {
|
||||||
assert.ok(handlers.has(type), `missing handler: ${type}`);
|
assert.ok(handlers.has(type), `missing handler: ${type}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user