CI / Build & Test (push) Successful in 4m20s
- Sync: use OS hostname for each device in Linked devices table; persist deviceNames in state and merge on save/apply so peers see each other's names - Docs: expand SYNC.md (linked devices table, replace-state modal, multi-device, offline); NATIVE-HOST getSyncStatus (deviceId, syncGroupId, linkedDevices, deviceName/name); ARCHITECTURE sync-manager and autopass paths; README features/dashboard/file locations/doc link; BACKUP and SECURITY cross-refs
164 lines
6.6 KiB
JavaScript
164 lines
6.6 KiB
JavaScript
/**
|
|
* Sync page — device linking via autopass; create invite, pair with invite, show status.
|
|
* Depends: core/utils.js ($), ui/modal.js (openModal, closeModal, showModalError), ui/toast.js (showToast)
|
|
*/
|
|
|
|
/**
|
|
* Update the sync status UI from the native host.
|
|
*/
|
|
function updateSyncStatus() {
|
|
chrome.runtime.sendMessage(
|
|
{ target: 'holesail-native', action: 'send', payload: { type: 'getSyncStatus' } },
|
|
(response) => {
|
|
if (chrome.runtime.lastError) return;
|
|
const dot = $('syncStatusDot');
|
|
const text = $('syncStatusText');
|
|
const lastSynced = $('syncLastSynced');
|
|
const btnCopy = $('btnCopyInvite');
|
|
const inviteDisplay = $('syncInviteDisplay');
|
|
if (!text) return;
|
|
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';
|
|
return;
|
|
}
|
|
if (response.linked) {
|
|
if (dot) dot.style.background = 'var(--green)';
|
|
text.textContent = 'Linked';
|
|
if (response.lastSyncedAt) {
|
|
if (lastSynced) lastSynced.textContent = 'Last synced ' + timeAgo(response.lastSyncedAt);
|
|
} else if (lastSynced) lastSynced.textContent = '';
|
|
if (response.invite) {
|
|
if (btnCopy) btnCopy.style.display = '';
|
|
if (inviteDisplay) {
|
|
inviteDisplay.textContent = response.invite;
|
|
inviteDisplay.style.display = 'block';
|
|
}
|
|
} else {
|
|
if (btnCopy) btnCopy.style.display = 'none';
|
|
if (inviteDisplay) inviteDisplay.style.display = 'none';
|
|
}
|
|
const card = $('syncLinkedDevicesCard');
|
|
const tbody = $('syncLinkedDevicesTable');
|
|
if (card) card.style.display = 'block';
|
|
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)));
|
|
rows += '<tr><td>' + escapeHtml(label) + '</td><td class="mono" style="font-size:12px;">' + escapeHtml(d.id || '—') + '</td></tr>';
|
|
});
|
|
} else {
|
|
const curName = response.deviceName || 'This device';
|
|
rows = '<tr><td>' + escapeHtml(curName) + '</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;
|
|
}
|
|
} else {
|
|
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';
|
|
const card = $('syncLinkedDevicesCard');
|
|
const tbody = $('syncLinkedDevicesTable');
|
|
if (card) card.style.display = 'none';
|
|
if (tbody) tbody.innerHTML = '<tr><td colspan="2" class="empty-cell">Not linked</td></tr>';
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Attach event listeners for the Sync page.
|
|
*/
|
|
function setupSyncEvents() {
|
|
const btnCreate = $('btnCreateSyncInvite');
|
|
const btnCopy = $('btnCopyInvite');
|
|
const btnPair = $('btnPairWithInvite');
|
|
const inviteInput = $('syncInviteInput');
|
|
const inviteDisplay = $('syncInviteDisplay');
|
|
|
|
btnCreate?.addEventListener('click', () => {
|
|
if (btnCreate.disabled) return;
|
|
btnCreate.disabled = true;
|
|
btnCreate.textContent = 'Creating…';
|
|
chrome.runtime.sendMessage(
|
|
{ target: 'holesail-native', action: 'send', payload: { type: 'createSyncInvite' } },
|
|
(response) => {
|
|
btnCreate.disabled = false;
|
|
btnCreate.textContent = 'Create invite';
|
|
if (chrome.runtime.lastError) {
|
|
showToast('Failed: ' + (chrome.runtime.lastError.message || 'unknown'), 'error');
|
|
return;
|
|
}
|
|
if (response && response.ok && response.invite) {
|
|
inviteDisplay.style.display = 'block';
|
|
inviteDisplay.textContent = response.invite;
|
|
btnCopy.style.display = '';
|
|
showToast('Invite created. Share it with the other device.', 'success');
|
|
updateSyncStatus();
|
|
} else {
|
|
showToast(response && response.error ? response.error : 'Failed to create invite', 'error');
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
btnCopy?.addEventListener('click', () => {
|
|
if (!inviteDisplay || !inviteDisplay.textContent) return;
|
|
navigator.clipboard.writeText(inviteDisplay.textContent).then(() => {
|
|
showToast('Invite copied to clipboard', 'success');
|
|
}).catch(() => {
|
|
showToast('Copy failed', 'error');
|
|
});
|
|
});
|
|
|
|
btnPair?.addEventListener('click', () => {
|
|
const invite = inviteInput?.value?.trim();
|
|
if (!invite) {
|
|
showToast('Paste an invite first', 'error');
|
|
return;
|
|
}
|
|
const errEl = $('linkDeviceError');
|
|
if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; }
|
|
openModal('modal-linkDevice');
|
|
});
|
|
|
|
$('linkDeviceConfirm')?.addEventListener('click', () => {
|
|
const invite = inviteInput?.value?.trim();
|
|
const errEl = $('linkDeviceError');
|
|
if (!invite) {
|
|
if (errEl) { errEl.textContent = 'Paste an invite in the field below first.'; errEl.style.display = 'block'; }
|
|
return;
|
|
}
|
|
closeModal('modal-linkDevice');
|
|
if (btnPair) { btnPair.disabled = true; btnPair.textContent = 'Linking…'; }
|
|
chrome.runtime.sendMessage(
|
|
{ target: 'holesail-native', action: 'send', payload: { type: 'pairWithInvite', payload: { invite } } },
|
|
(response) => {
|
|
if (btnPair) { btnPair.disabled = false; btnPair.textContent = 'Link device'; }
|
|
if (chrome.runtime.lastError) {
|
|
showToast('Failed: ' + (chrome.runtime.lastError.message || 'unknown'), 'error');
|
|
return;
|
|
}
|
|
if (response && response.ok) {
|
|
showToast('Device linked. State has been synced.', 'success');
|
|
if (inviteInput) inviteInput.value = '';
|
|
updateSyncStatus();
|
|
if (typeof refresh === 'function') refresh();
|
|
} else {
|
|
showToast(response && response.error ? response.error : 'Linking failed', 'error');
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|