feat(dashboard): debounced filter for linked devices table on Sync page
CI / Build & Test (push) Successful in 4m25s
CI / Build & Test (push) Successful in 4m25s
Add search input (name, ID, type) with 250ms debounce; mirror in extension and renderer dashboards. Update parity docs note in Browser repo only.
This commit is contained in:
@@ -1042,6 +1042,64 @@
|
||||
border-color: color-mix(in srgb, var(--amber) 35%, transparent);
|
||||
color: var(--amber);
|
||||
}
|
||||
.sync-runtime-badge--neutral {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.sync-you-badge {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
padding: 2px 7px;
|
||||
border-radius: 99px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
background: var(--green-dim);
|
||||
color: var(--green);
|
||||
}
|
||||
.sync-linked-toolbar {
|
||||
padding: 0 16px 12px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.sync-linked-filter-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text3);
|
||||
}
|
||||
.sync-linked-filter-input {
|
||||
max-width: 320px;
|
||||
}
|
||||
.sync-linked-filter-empty {
|
||||
font-style: italic;
|
||||
color: var(--text4);
|
||||
}
|
||||
.sync-linked-table tbody tr.sync-linked-row--current td:first-child {
|
||||
border-left: 3px solid var(--green);
|
||||
padding-left: calc(12px - 3px);
|
||||
}
|
||||
.sync-linked-row--meta td {
|
||||
padding-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--text3);
|
||||
}
|
||||
.sync-table-meta-label {
|
||||
margin-right: 6px;
|
||||
}
|
||||
.sync-table-meta-id {
|
||||
font-size: 12px;
|
||||
}
|
||||
.sync-linked-type-cell {
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sync-device-name-counter {
|
||||
font-size: 11px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--text4);
|
||||
min-width: 3.5em;
|
||||
}
|
||||
|
||||
/* ── Sync: this device (setup vs named hero) ───────────────────────── */
|
||||
.sync-device-intro {
|
||||
|
||||
@@ -602,20 +602,22 @@
|
||||
<!-- ── Sync page ─────────────────────────────── -->
|
||||
<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>
|
||||
<div class="card-body" aria-labelledby="syncDeviceSectionHeading">
|
||||
<div class="section-heading" id="syncDeviceSectionHeading">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>
|
||||
<p id="syncDeviceHeroLine" 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 id="syncDeviceEditWrap" class="sync-device-edit-wrap" aria-label="Edit device display name">
|
||||
<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" />
|
||||
<span id="syncDeviceNameCounter" class="sync-device-name-counter" aria-live="polite">0 / 64</span>
|
||||
<button type="button" class="btn btn-primary" id="btnSaveDeviceName">Save name</button>
|
||||
<button type="button" class="btn btn-secondary" id="btnClearDeviceName" hidden>Clear custom name</button>
|
||||
<span class="sync-runtime-badge" id="syncRuntimeBadgeEdit" title="How this app runs">—</span>
|
||||
</div>
|
||||
<p id="syncDeviceNameHint" class="sync-device-hint"></p>
|
||||
@@ -627,8 +629,8 @@
|
||||
<div class="card-body">
|
||||
<div class="section-heading">Device sync</div>
|
||||
<p style="margin:0 0 14px 0;font-size:13px;color:var(--text3);">Sync your Holesail state (virtual hosts, servers, tunnels, SSH/RDP connections, settings) to another device. CA certificates are not synced; install the root CA on the other device if needed.</p>
|
||||
<div id="syncStatus" style="display:flex;align-items:center;gap:8px;margin-bottom:14px;">
|
||||
<div class="status-dot" id="syncStatusDot"></div>
|
||||
<div id="syncStatus" class="sync-status-live" role="status" aria-live="polite" aria-atomic="true" style="display:flex;align-items:center;gap:8px;margin-bottom:14px;">
|
||||
<div class="status-dot" id="syncStatusDot" aria-hidden="true"></div>
|
||||
<span id="syncStatusText">Checking…</span>
|
||||
<span id="syncLastSynced" style="font-size:11px;color:var(--text4);"></span>
|
||||
</div>
|
||||
@@ -650,15 +652,20 @@
|
||||
<div class="card" id="syncLinkedDevicesCard" style="display:none;margin-bottom:16px;">
|
||||
<div class="card-body flush">
|
||||
<div class="section-heading" style="padding:0 16px 10px 16px;">Linked devices</div>
|
||||
<table class="table">
|
||||
<div class="sync-linked-toolbar">
|
||||
<label class="sync-linked-filter-label" for="syncLinkedDevicesFilter">Filter devices</label>
|
||||
<input type="search" id="syncLinkedDevicesFilter" class="input sync-linked-filter-input" placeholder="Search by name, ID, or type" autocomplete="off" />
|
||||
</div>
|
||||
<table class="table sync-linked-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Device</th>
|
||||
<th>ID</th>
|
||||
<th>Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="syncLinkedDevicesTable">
|
||||
<tr><td colspan="2" class="empty-cell">Not linked</td></tr>
|
||||
<tr><td colspan="3" class="empty-cell">Not linked</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="padding:12px 16px;border-top:1px solid var(--border);display:flex;gap:10px;flex-wrap:wrap;">
|
||||
|
||||
@@ -16,6 +16,11 @@ function _closeSyncDeviceNameEdit() { _syncDeviceNameEditOpen = false; }
|
||||
/** Last successful getSyncStatus payload (for instant layout when opening "Change name"). */
|
||||
let _lastSyncStatusForDevicePanel = null;
|
||||
|
||||
function getDisplayNameMaxLen(response) {
|
||||
const n = response && typeof response.displayNameMaxLength === 'number' ? response.displayNameMaxLength : 64;
|
||||
return n > 0 ? n : 64;
|
||||
}
|
||||
|
||||
function syncDevicePanelIsNamed(response) {
|
||||
return (response.localDisplayName || '').trim().length > 0;
|
||||
}
|
||||
@@ -34,10 +39,115 @@ function applySyncRuntimeBadges(runtimeKind) {
|
||||
}
|
||||
}
|
||||
|
||||
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 syncRuntimeBadgeCellHtml(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 '<span class="sync-runtime-badge sync-runtime-badge--neutral">—</span>';
|
||||
}
|
||||
|
||||
function sortLinkedDevices(devices) {
|
||||
const arr = devices.slice();
|
||||
arr.sort((a, b) => {
|
||||
if (!!a.isCurrent !== !!b.isCurrent) return a.isCurrent ? -1 : 1;
|
||||
if (!!a.isMaster !== !!b.isMaster) return a.isMaster ? -1 : 1;
|
||||
const na = (a.name || a.id || '').toString();
|
||||
const nb = (b.name || b.id || '').toString();
|
||||
const c = na.localeCompare(nb);
|
||||
if (c !== 0) return c;
|
||||
return (a.id || '').localeCompare(b.id || '');
|
||||
});
|
||||
return arr;
|
||||
}
|
||||
|
||||
const LINKED_DEVICES_FILTER_DEBOUNCE_MS = 250;
|
||||
let _linkedDevicesFilterDebounceTimer = null;
|
||||
/** Last successful getSyncStatus payload when linked (for debounced filter re-renders). */
|
||||
let _lastLinkedDevicesContext = null;
|
||||
|
||||
function getLinkedDeviceFilterQuery() {
|
||||
const el = $('syncLinkedDevicesFilter');
|
||||
return el && typeof el.value === 'string' ? el.value : '';
|
||||
}
|
||||
|
||||
function computeLinkedDeviceLabel(d, response, index) {
|
||||
return d.name || (d.isCurrent ? (response.deviceName || 'This device') : ('Device ' + (index + 1)));
|
||||
}
|
||||
|
||||
function linkedDeviceMatchesFilter(d, response, queryLower, index) {
|
||||
if (!queryLower) return true;
|
||||
const id = (d.id || '').toLowerCase();
|
||||
const label = computeLinkedDeviceLabel(d, response, index).toLowerCase();
|
||||
const rt = d.runtime === 'desktop' ? 'desktop app' : d.runtime === 'browser-extension' ? 'browser extension' : '';
|
||||
return label.includes(queryLower) || id.includes(queryLower) || rt.includes(queryLower);
|
||||
}
|
||||
|
||||
function buildLinkedDevicesTableBodyHtml(response) {
|
||||
const q = getLinkedDeviceFilterQuery().trim().toLowerCase();
|
||||
const syncGroupId = response.syncGroupId || '—';
|
||||
const rawDevices = Array.isArray(response.linkedDevices) ? response.linkedDevices : [];
|
||||
let rows = '';
|
||||
if (rawDevices.length > 0) {
|
||||
const sorted = sortLinkedDevices(rawDevices);
|
||||
const withIndex = sorted.map((d, i) => ({ d, i }));
|
||||
const filtered = q ? withIndex.filter(({ d, i }) => linkedDeviceMatchesFilter(d, response, q, i)) : withIndex;
|
||||
if (filtered.length === 0 && q) {
|
||||
rows = '<tr><td colspan="3" class="empty-cell sync-linked-filter-empty">No devices match your search</td></tr>';
|
||||
} else {
|
||||
filtered.forEach(({ d, i }) => {
|
||||
const label = computeLinkedDeviceLabel(d, response, i);
|
||||
const masterBadge = d.isMaster ? ' <span class="sync-master-badge">MASTER</span>' : '';
|
||||
const youBadge = d.isCurrent ? ' <span class="sync-you-badge">This device</span>' : '';
|
||||
const rowClass = d.isCurrent ? ' class="sync-linked-row sync-linked-row--current"' : ' class="sync-linked-row"';
|
||||
const typeCell = syncRuntimeBadgeCellHtml(d.runtime);
|
||||
rows += '<tr' + rowClass + '><td>' + escapeHtml(label) + masterBadge + youBadge + '</td><td class="mono" style="font-size:12px;">' + escapeHtml(d.id || '—') + '</td><td class="sync-linked-type-cell">' + typeCell + '</td></tr>';
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const d = {
|
||||
id: response.deviceId,
|
||||
name: response.deviceName,
|
||||
isCurrent: true,
|
||||
isMaster: response.isMaster,
|
||||
runtime: response.runtimeKind
|
||||
};
|
||||
if (q && !linkedDeviceMatchesFilter(d, response, q, 0)) {
|
||||
rows = '<tr><td colspan="3" class="empty-cell sync-linked-filter-empty">No devices match your search</td></tr>';
|
||||
} else {
|
||||
const label = computeLinkedDeviceLabel(d, response, 0);
|
||||
const masterBadge = d.isMaster ? ' <span class="sync-master-badge">MASTER</span>' : '';
|
||||
const youBadge = ' <span class="sync-you-badge">This device</span>';
|
||||
const rt = syncRuntimeBadgeCellHtml(response.runtimeKind);
|
||||
rows = '<tr class="sync-linked-row sync-linked-row--current"><td>' + escapeHtml(label) + masterBadge + youBadge + '</td><td class="mono" style="font-size:12px;">' + escapeHtml(response.deviceId || '—') + '</td><td class="sync-linked-type-cell">' + rt + '</td></tr>';
|
||||
}
|
||||
}
|
||||
rows += '<tr class="sync-linked-row sync-linked-row--meta"><td colspan="3"><span class="sync-table-meta-label">Sync group</span> <span class="mono sync-table-meta-id">' + escapeHtml(syncGroupId) + '</span></td></tr>';
|
||||
return rows;
|
||||
}
|
||||
|
||||
function renderLinkedDevicesTableBody() {
|
||||
const tbody = $('syncLinkedDevicesTable');
|
||||
if (!tbody || !_lastLinkedDevicesContext) return;
|
||||
tbody.innerHTML = buildLinkedDevicesTableBodyHtml(_lastLinkedDevicesContext);
|
||||
}
|
||||
|
||||
function scheduleLinkedDevicesFilterRender() {
|
||||
if (_linkedDevicesFilterDebounceTimer) clearTimeout(_linkedDevicesFilterDebounceTimer);
|
||||
_linkedDevicesFilterDebounceTimer = setTimeout(() => {
|
||||
_linkedDevicesFilterDebounceTimer = null;
|
||||
renderLinkedDevicesTableBody();
|
||||
}, LINKED_DEVICES_FILTER_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
function updateSyncDeviceNameCounter() {
|
||||
const input = $('syncDeviceNameInput');
|
||||
const el = $('syncDeviceNameCounter');
|
||||
if (!input || !el) return;
|
||||
const max = parseInt(input.getAttribute('maxlength'), 10) || 64;
|
||||
el.textContent = input.value.length + ' / ' + max;
|
||||
}
|
||||
|
||||
function applySyncDeviceNamePanel(response) {
|
||||
@@ -51,14 +161,21 @@ function applySyncDeviceNamePanel(response) {
|
||||
const intro = $('syncDeviceIntro');
|
||||
const display = $('syncDeviceNameDisplay');
|
||||
const btnChange = $('btnChangeDeviceName');
|
||||
const btnClear = $('btnClearDeviceName');
|
||||
const card = $('syncDeviceCard');
|
||||
|
||||
const skipDeviceNameApply =
|
||||
_syncDeviceNameDirty || !!(input && document.activeElement === input);
|
||||
|
||||
const maxLen = getDisplayNameMaxLen(response);
|
||||
if (input) {
|
||||
input.setAttribute('maxlength', String(maxLen));
|
||||
}
|
||||
|
||||
if (input && response.localDisplayName !== undefined && !skipDeviceNameApply) {
|
||||
input.value = response.localDisplayName;
|
||||
}
|
||||
updateSyncDeviceNameCounter();
|
||||
|
||||
if (response.runtimeKind) {
|
||||
applySyncRuntimeBadges(response.runtimeKind);
|
||||
@@ -80,6 +197,7 @@ function applySyncDeviceNamePanel(response) {
|
||||
if (hero) hero.hidden = !showHero;
|
||||
if (editWrap) editWrap.hidden = !showEdit;
|
||||
if (btnChange) btnChange.hidden = !isNamed || _syncDeviceNameEditOpen;
|
||||
if (btnClear) btnClear.hidden = !(showEdit && isNamed);
|
||||
|
||||
if (display && typeof response.effectiveDeviceName === 'string' && !skipDeviceNameApply) {
|
||||
display.textContent = response.effectiveDeviceName || '';
|
||||
@@ -93,6 +211,38 @@ function applySyncDeviceNamePanel(response) {
|
||||
if (hint) hint.hidden = !showEdit;
|
||||
}
|
||||
|
||||
function submitDeviceDisplayName() {
|
||||
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');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function syncDeviceNameEscapeHandler() {
|
||||
if (!_syncDeviceNameEditOpen) return;
|
||||
_closeSyncDeviceNameEdit();
|
||||
if (_lastSyncStatusForDevicePanel) {
|
||||
applySyncDeviceNamePanel(_lastSyncStatusForDevicePanel);
|
||||
} else {
|
||||
updateSyncStatus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the sync status UI from the native host.
|
||||
*/
|
||||
@@ -143,25 +293,8 @@ function updateSyncStatus() {
|
||||
if (btnDelink) btnDelink.style.display = response.isMaster ? 'none' : '';
|
||||
if (btnDisband) btnDisband.style.display = response.isMaster ? '' : 'none';
|
||||
if (tbody) {
|
||||
const syncGroupId = response.syncGroupId || '—';
|
||||
const linkedDevices = Array.isArray(response.linkedDevices) ? response.linkedDevices : [];
|
||||
if (linkedDevices.length > 0) linkedDevices.sort((a, b) => (a.id || '').localeCompare(b.id || ''));
|
||||
let rows = '';
|
||||
if (linkedDevices.length > 0) {
|
||||
linkedDevices.forEach((d, i) => {
|
||||
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 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 {
|
||||
const curName = response.deviceName || 'This device';
|
||||
const masterBadge = response.isMaster ? ' <span class="sync-master-badge">MASTER</span>' : '';
|
||||
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>';
|
||||
tbody.innerHTML = rows;
|
||||
_lastLinkedDevicesContext = response;
|
||||
tbody.innerHTML = buildLinkedDevicesTableBodyHtml(response);
|
||||
}
|
||||
} else {
|
||||
if (dot) dot.style.background = 'var(--text4)';
|
||||
@@ -172,8 +305,9 @@ function updateSyncStatus() {
|
||||
if (linkDeviceForm) linkDeviceForm.style.display = 'flex';
|
||||
const card = $('syncLinkedDevicesCard');
|
||||
const tbody = $('syncLinkedDevicesTable');
|
||||
_lastLinkedDevicesContext = null;
|
||||
if (card) card.style.display = 'none';
|
||||
if (tbody) tbody.innerHTML = '<tr><td colspan="2" class="empty-cell">Not linked</td></tr>';
|
||||
if (tbody) tbody.innerHTML = '<tr><td colspan="3" class="empty-cell">Not linked</td></tr>';
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -189,7 +323,23 @@ function setupSyncEvents() {
|
||||
const inviteInput = $('syncInviteInput');
|
||||
const inviteDisplay = $('syncInviteDisplay');
|
||||
|
||||
$('syncDeviceNameInput')?.addEventListener('input', _markSyncDeviceNameDirty);
|
||||
$('syncLinkedDevicesFilter')?.addEventListener('input', () => {
|
||||
scheduleLinkedDevicesFilterRender();
|
||||
});
|
||||
|
||||
$('syncDeviceNameInput')?.addEventListener('input', () => {
|
||||
_markSyncDeviceNameDirty();
|
||||
updateSyncDeviceNameCounter();
|
||||
});
|
||||
|
||||
$('syncDeviceNameInput')?.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
submitDeviceDisplayName();
|
||||
} else if (e.key === 'Escape') {
|
||||
syncDeviceNameEscapeHandler();
|
||||
}
|
||||
});
|
||||
|
||||
$('btnChangeDeviceName')?.addEventListener('click', () => {
|
||||
_openSyncDeviceNameEdit();
|
||||
@@ -200,25 +350,14 @@ function setupSyncEvents() {
|
||||
});
|
||||
|
||||
$('btnSaveDeviceName')?.addEventListener('click', () => {
|
||||
submitDeviceDisplayName();
|
||||
});
|
||||
|
||||
$('btnClearDeviceName')?.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');
|
||||
}
|
||||
}
|
||||
);
|
||||
if (input) input.value = '';
|
||||
updateSyncDeviceNameCounter();
|
||||
submitDeviceDisplayName();
|
||||
});
|
||||
|
||||
btnCreate?.addEventListener('click', () => {
|
||||
|
||||
@@ -14,6 +14,8 @@ const IDENTITY_FILENAME = 'autopass-identity.json';
|
||||
const STATE_FILENAME = 'state.json';
|
||||
const AUTOPASS_DIRNAME = 'autopass';
|
||||
const REMOTE_UPDATE_DEBOUNCE_MS = 600;
|
||||
/** Max length for custom device display name (local file + synced label). */
|
||||
const DEVICE_DISPLAY_NAME_MAX_LENGTH = 64;
|
||||
|
||||
let storageDir = null;
|
||||
let holesailManager = null;
|
||||
@@ -72,7 +74,7 @@ function readLocalDeviceDisplayName() {
|
||||
const raw = fs.readFileSync(p, 'utf8');
|
||||
const data = JSON.parse(raw);
|
||||
if (data && typeof data.displayName === 'string') {
|
||||
return data.displayName.trim().slice(0, 64);
|
||||
return data.displayName.trim().slice(0, DEVICE_DISPLAY_NAME_MAX_LENGTH);
|
||||
}
|
||||
} catch (_) {}
|
||||
return '';
|
||||
@@ -81,7 +83,7 @@ function readLocalDeviceDisplayName() {
|
||||
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) : '';
|
||||
const trimmed = typeof name === 'string' ? name.trim().slice(0, DEVICE_DISPLAY_NAME_MAX_LENGTH) : '';
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||
fs.writeFileSync(p, JSON.stringify({ displayName: trimmed }, null, 2), 'utf8');
|
||||
@@ -579,13 +581,16 @@ function readSyncMasterDeviceIdFromState() {
|
||||
async function getSyncStatus() {
|
||||
await ensureInitialized();
|
||||
const effName = getEffectiveDeviceName();
|
||||
const localDisp = readLocalDeviceDisplayName();
|
||||
const out = {
|
||||
linked: !!pass,
|
||||
invite: currentInvite || null,
|
||||
lastSyncedAt: lastSyncedAt || null,
|
||||
localDisplayName: readLocalDeviceDisplayName(),
|
||||
localDisplayName: localDisp,
|
||||
runtimeKind: getLocalRuntimeKind(),
|
||||
effectiveDeviceName: effName
|
||||
effectiveDeviceName: effName,
|
||||
displayNameMaxLength: DEVICE_DISPLAY_NAME_MAX_LENGTH,
|
||||
usesCustomDisplayName: localDisp.length > 0
|
||||
};
|
||||
if (pass) {
|
||||
try {
|
||||
@@ -916,7 +921,9 @@ function hasSyncIdentity() {
|
||||
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' };
|
||||
if (raw.length > DEVICE_DISPLAY_NAME_MAX_LENGTH) {
|
||||
return { ok: false, error: 'displayName must be at most ' + DEVICE_DISPLAY_NAME_MAX_LENGTH + ' characters' };
|
||||
}
|
||||
const w = writeLocalDeviceDisplayName(raw);
|
||||
if (!w.ok) return w;
|
||||
if (typeof holesailManager.saveState === 'function') {
|
||||
|
||||
@@ -124,6 +124,35 @@ describe('handler registry', () => {
|
||||
assert.deepStrictEqual(reply.calls[0].settings, settings);
|
||||
});
|
||||
|
||||
test('getSyncStatus handler merges ok with status fields', async () => {
|
||||
const status = {
|
||||
linked: false,
|
||||
displayNameMaxLength: 64,
|
||||
usesCustomDisplayName: false,
|
||||
localDisplayName: '',
|
||||
effectiveDeviceName: 'myhost',
|
||||
runtimeKind: 'browser-extension'
|
||||
};
|
||||
const mockDeps = makeMockDeps({
|
||||
syncManager: {
|
||||
getSyncStatus: async () => status,
|
||||
createSyncInvite: async () => ({}),
|
||||
pairWithInvite: async () => ({}),
|
||||
warmupSyncAtStartup: async () => {},
|
||||
hasSyncIdentity: () => false,
|
||||
setDeviceDisplayName: () => ({ ok: true })
|
||||
}
|
||||
});
|
||||
const handlers = buildHandlers(mockDeps);
|
||||
const reply = mockReply();
|
||||
await handlers.get('getSyncStatus')({}, reply);
|
||||
assert.strictEqual(reply.calls.length, 1);
|
||||
assert.strictEqual(reply.calls[0].ok, true);
|
||||
assert.strictEqual(reply.calls[0].displayNameMaxLength, 64);
|
||||
assert.strictEqual(reply.calls[0].usesCustomDisplayName, false);
|
||||
assert.strictEqual(reply.calls[0].effectiveDeviceName, 'myhost');
|
||||
});
|
||||
|
||||
test('getSshConnections handler calls reply with ok and sshConnections', async () => {
|
||||
const list = [{ id: 'ssh-1', label: 'My Server' }];
|
||||
const mockDeps = makeMockDeps({
|
||||
|
||||
Reference in New Issue
Block a user