@@ -623,6 +623,10 @@
|
|||||||
<tr><td colspan="2" class="empty-cell">Not linked</td></tr>
|
<tr><td colspan="2" class="empty-cell">Not linked</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div style="padding:12px 16px;border-top:1px solid var(--border);display:flex;gap:10px;flex-wrap:wrap;">
|
||||||
|
<button class="btn btn-secondary" id="btnDelink">Delink</button>
|
||||||
|
<button class="btn btn-secondary" id="btnDisband" style="display:none;">Disband group</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1334,6 +1338,48 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ── Modal: Delink (sync) confirm ────────────────────── -->
|
||||||
|
<div class="modal-backdrop" id="modal-delink">
|
||||||
|
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-delink-title" style="max-width:420px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span class="modal-title" id="modal-delink-title">Delink this device</span>
|
||||||
|
<button class="btn-icon" data-close-modal="modal-delink" aria-label="Close">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="modal-desc">This will remove this device from the sync group. Peers and the master will see you leave. Your local state (virtual hosts, tunnels, SSH/RDP connections, settings) will be <strong>reset to defaults</strong>. This cannot be undone.</p>
|
||||||
|
<p class="modal-desc" style="margin-top:8px;color:var(--amber);">Create a backup first if you want to keep a copy.</p>
|
||||||
|
<div class="modal-error" id="delinkError"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" data-close-modal="modal-delink">Cancel</button>
|
||||||
|
<button class="btn btn-primary" id="delinkConfirm">Delink and reset</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Modal: Disband (sync) confirm ───────────────────── -->
|
||||||
|
<div class="modal-backdrop" id="modal-disband">
|
||||||
|
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-disband-title" style="max-width:420px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span class="modal-title" id="modal-disband-title">Disband sync group</span>
|
||||||
|
<button class="btn-icon" data-close-modal="modal-disband" aria-label="Close">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="modal-desc">As the master, you can disband the sync group. <strong>All other linked devices will be reset to defaults</strong> and removed from the group; they can create a new group or join another. Your own state on this device is kept.</p>
|
||||||
|
<p class="modal-desc" style="margin-top:8px;">This device will no longer be linked after disbanding.</p>
|
||||||
|
<div class="modal-error" id="disbandError"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" data-close-modal="modal-disband">Cancel</button>
|
||||||
|
<button class="btn btn-primary" id="disbandConfirm">Disband group</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ── Modal: Link device (sync) confirm ────────────────── -->
|
<!-- ── Modal: Link device (sync) confirm ────────────────── -->
|
||||||
<div class="modal-backdrop" id="modal-linkDevice">
|
<div class="modal-backdrop" id="modal-linkDevice">
|
||||||
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-linkDevice-title" style="max-width:420px;">
|
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-linkDevice-title" style="max-width:420px;">
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ function updateSyncStatus() {
|
|||||||
}
|
}
|
||||||
const card = $('syncLinkedDevicesCard');
|
const card = $('syncLinkedDevicesCard');
|
||||||
const tbody = $('syncLinkedDevicesTable');
|
const tbody = $('syncLinkedDevicesTable');
|
||||||
|
const btnDisband = $('btnDisband');
|
||||||
if (card) card.style.display = 'block';
|
if (card) card.style.display = 'block';
|
||||||
|
if (btnDisband) btnDisband.style.display = response.isMaster ? '' : 'none';
|
||||||
if (tbody) {
|
if (tbody) {
|
||||||
const syncGroupId = response.syncGroupId || '—';
|
const syncGroupId = response.syncGroupId || '—';
|
||||||
const linkedDevices = Array.isArray(response.linkedDevices) ? response.linkedDevices : [];
|
const linkedDevices = Array.isArray(response.linkedDevices) ? response.linkedDevices : [];
|
||||||
@@ -55,7 +57,7 @@ function updateSyncStatus() {
|
|||||||
if (linkedDevices.length > 0) {
|
if (linkedDevices.length > 0) {
|
||||||
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.isCurrent && response.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>';
|
rows += '<tr><td>' + escapeHtml(label) + masterBadge + '</td><td class="mono" style="font-size:12px;">' + escapeHtml(d.id || '—') + '</td></tr>';
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -166,4 +168,60 @@ function setupSyncEvents() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const btnDelink = $('btnDelink');
|
||||||
|
const btnDisband = $('btnDisband');
|
||||||
|
btnDelink?.addEventListener('click', () => {
|
||||||
|
const errEl = $('delinkError');
|
||||||
|
if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; }
|
||||||
|
openModal('modal-delink');
|
||||||
|
});
|
||||||
|
$('delinkConfirm')?.addEventListener('click', () => {
|
||||||
|
closeModal('modal-delink');
|
||||||
|
if (btnDelink) { btnDelink.disabled = true; btnDelink.textContent = 'Delinking…'; }
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
{ target: 'holesail-native', action: 'send', payload: { type: 'delink' } },
|
||||||
|
(response) => {
|
||||||
|
if (btnDelink) { btnDelink.disabled = false; btnDelink.textContent = 'Delink'; }
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
showToast('Failed: ' + (chrome.runtime.lastError.message || 'unknown'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response && response.ok) {
|
||||||
|
showToast('Device delinked. State reset to defaults.', 'success');
|
||||||
|
updateSyncStatus();
|
||||||
|
if (typeof refresh === 'function') refresh();
|
||||||
|
} else {
|
||||||
|
showToast(response && response.error ? response.error : 'Delink failed', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
btnDisband?.addEventListener('click', () => {
|
||||||
|
const errEl = $('disbandError');
|
||||||
|
if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; }
|
||||||
|
openModal('modal-disband');
|
||||||
|
});
|
||||||
|
$('disbandConfirm')?.addEventListener('click', () => {
|
||||||
|
closeModal('modal-disband');
|
||||||
|
if (btnDisband) { btnDisband.disabled = true; btnDisband.textContent = 'Disbanding…'; }
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
{ target: 'holesail-native', action: 'send', payload: { type: 'disband' } },
|
||||||
|
(response) => {
|
||||||
|
if (btnDisband) { btnDisband.disabled = false; btnDisband.textContent = 'Disband group'; }
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
showToast('Failed: ' + (chrome.runtime.lastError.message || 'unknown'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response && response.ok) {
|
||||||
|
showToast('Sync group disbanded. Other devices have been reset.', 'success');
|
||||||
|
updateSyncStatus();
|
||||||
|
if (typeof refresh === 'function') refresh();
|
||||||
|
} else {
|
||||||
|
showToast(response && response.error ? response.error : 'Disband failed', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,6 +174,20 @@ async function cleanup() {
|
|||||||
await svcModule.cleanupServiceTunnels();
|
await svcModule.cleanupServiceTunnels();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset persistent state to defaults: write default state to disk, clear deviceNames cache,
|
||||||
|
* then return the result of restorePersistedState() so the caller can run restorePersistedTunnels.
|
||||||
|
* Call cleanup() before this. Used by sync-manager when a device delinks or when peers receive a disband.
|
||||||
|
* @returns {{settings: object, servers: Array, virtualHosts: Array, serviceTunnels: Array, sshConnections: Array, rdpConnections: Array}}
|
||||||
|
*/
|
||||||
|
async function resetStateToDefaults() {
|
||||||
|
await cleanup();
|
||||||
|
const defaultState = stateModule.buildDefaultState();
|
||||||
|
stateModule.saveStateSync(defaultState);
|
||||||
|
stateModule.setDeviceNames({});
|
||||||
|
return restorePersistedState();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Re-export full original API ───────────────────────────────────────────────
|
// ── Re-export full original API ───────────────────────────────────────────────
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -186,6 +200,7 @@ module.exports = {
|
|||||||
setStateSaveSuppressed,
|
setStateSaveSuppressed,
|
||||||
getStateSnapshot,
|
getStateSnapshot,
|
||||||
applySnapshotData,
|
applySnapshotData,
|
||||||
|
resetStateToDefaults,
|
||||||
// Settings
|
// Settings
|
||||||
getSettings: settingsModule.getSettings,
|
getSettings: settingsModule.getSettings,
|
||||||
updateSettings: settingsModule.updateSettings,
|
updateSettings: settingsModule.updateSettings,
|
||||||
|
|||||||
@@ -39,6 +39,28 @@ function register(deps) {
|
|||||||
reply({ ok: false, error: e.message });
|
reply({ ok: false, error: e.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'delink',
|
||||||
|
handle: async (payload, reply) => {
|
||||||
|
try {
|
||||||
|
const result = await syncManager.delink();
|
||||||
|
reply(result);
|
||||||
|
} catch (e) {
|
||||||
|
reply({ ok: false, error: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'disband',
|
||||||
|
handle: async (payload, reply) => {
|
||||||
|
try {
|
||||||
|
const result = await syncManager.disband();
|
||||||
|
reply(result);
|
||||||
|
} catch (e) {
|
||||||
|
reply({ ok: false, error: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,6 +212,10 @@ async function applyRemoteUpdate() {
|
|||||||
const str = typeof entry.value === 'string' ? entry.value : (entry.value && entry.value.toString ? entry.value.toString() : '');
|
const str = typeof entry.value === 'string' ? entry.value : (entry.value && entry.value.toString ? entry.value.toString() : '');
|
||||||
if (!str) return;
|
if (!str) return;
|
||||||
const snapshot = JSON.parse(str);
|
const snapshot = JSON.parse(str);
|
||||||
|
if (snapshot.syncGroupDisbanded === true) {
|
||||||
|
await handleDisbandFromMaster();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const statePath = getStateFilePath();
|
const statePath = getStateFilePath();
|
||||||
if (statePath && fs.existsSync(statePath)) {
|
if (statePath && fs.existsSync(statePath)) {
|
||||||
try {
|
try {
|
||||||
@@ -228,6 +232,33 @@ async function applyRemoteUpdate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when this device receives a state update with syncGroupDisbanded: true (master disbanded the group).
|
||||||
|
* Resets local state to defaults and removes sync data so this device can create or join another group.
|
||||||
|
*/
|
||||||
|
async function handleDisbandFromMaster() {
|
||||||
|
if (!holesailManager || !setTunnelsRestoredPromise || !restorePersistedTunnels) return;
|
||||||
|
applyingSync = true;
|
||||||
|
try {
|
||||||
|
if (pass && pass.off) pass.off('update', onRemoteUpdate);
|
||||||
|
await closePass();
|
||||||
|
removeAutopassDir();
|
||||||
|
clearIdentity();
|
||||||
|
initPromise = null;
|
||||||
|
const restored = await holesailManager.resetStateToDefaults();
|
||||||
|
const restorePromise = restorePersistedTunnels(holesailManager, restored).catch((e) =>
|
||||||
|
log('Post-disband tunnel restore failed:', e.message)
|
||||||
|
);
|
||||||
|
setTunnelsRestoredPromise(restorePromise);
|
||||||
|
await restorePromise;
|
||||||
|
lastSyncedAt = null;
|
||||||
|
if (emitEvent) emitEvent('syncApplied', {});
|
||||||
|
log('Sync: disbanded by master; state reset to defaults');
|
||||||
|
} finally {
|
||||||
|
applyingSync = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare two objects by relevant keys (for tunnel configs).
|
* Compare two objects by relevant keys (for tunnel configs).
|
||||||
*/
|
*/
|
||||||
@@ -389,6 +420,13 @@ function onStateSaved(snapshot) {
|
|||||||
snapshot.deviceNames = snapshot.deviceNames || {};
|
snapshot.deviceNames = snapshot.deviceNames || {};
|
||||||
snapshot.deviceNames[myId] = typeof os.hostname === 'function' ? os.hostname() : 'device';
|
snapshot.deviceNames[myId] = typeof os.hostname === 'function' ? os.hostname() : 'device';
|
||||||
}
|
}
|
||||||
|
const identity = loadIdentity();
|
||||||
|
if (identity && identity.isMaster) {
|
||||||
|
snapshot.syncMasterDeviceId = myId;
|
||||||
|
} else {
|
||||||
|
const existing = readSyncMasterDeviceIdFromState();
|
||||||
|
if (existing) snapshot.syncMasterDeviceId = existing;
|
||||||
|
}
|
||||||
const statePath = getStateFilePath();
|
const statePath = getStateFilePath();
|
||||||
if (statePath) {
|
if (statePath) {
|
||||||
try {
|
try {
|
||||||
@@ -431,6 +469,19 @@ function readDeviceNamesFromState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readSyncMasterDeviceIdFromState() {
|
||||||
|
const statePath = getStateFilePath();
|
||||||
|
if (!statePath || !fs.existsSync(statePath)) return null;
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(statePath, 'utf8');
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
const id = data.syncMasterDeviceId;
|
||||||
|
return typeof id === 'string' && id.length > 0 ? id : null;
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getSyncStatus() {
|
async function getSyncStatus() {
|
||||||
await ensureInitialized();
|
await ensureInitialized();
|
||||||
const out = {
|
const out = {
|
||||||
@@ -448,6 +499,7 @@ async function getSyncStatus() {
|
|||||||
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 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) {
|
||||||
if (!w || !w.core || !w.core.key) continue;
|
if (!w || !w.core || !w.core.key) continue;
|
||||||
@@ -457,12 +509,13 @@ async function getSyncStatus() {
|
|||||||
out.linkedDevices.push({
|
out.linkedDevices.push({
|
||||||
id,
|
id,
|
||||||
isCurrent,
|
isCurrent,
|
||||||
|
isMaster: id === masterDeviceId,
|
||||||
name: deviceNames[id] || (isCurrent ? out.deviceName : null)
|
name: deviceNames[id] || (isCurrent ? out.deviceName : null)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (out.linkedDevices.length === 0 && out.deviceId) {
|
if (out.linkedDevices.length === 0 && out.deviceId) {
|
||||||
out.linkedDevices = [{ id: out.deviceId, isCurrent: true, name: out.deviceName }];
|
out.linkedDevices = [{ id: out.deviceId, isCurrent: true, isMaster: out.deviceId === masterDeviceId || out.isMaster, name: out.deviceName }];
|
||||||
}
|
}
|
||||||
// Ensure our hostname is published so other devices see our name (fire-and-forget)
|
// Ensure our hostname is published so other devices see our name (fire-and-forget)
|
||||||
const myId = out.deviceId;
|
const myId = out.deviceId;
|
||||||
@@ -533,7 +586,14 @@ async function createSyncInvite() {
|
|||||||
saveIdentity(pass.key, pass.encryptionKey, true);
|
saveIdentity(pass.key, pass.encryptionKey, true);
|
||||||
currentInvite = await pass.createInvite();
|
currentInvite = await pass.createInvite();
|
||||||
const snapshot = readCurrentStateFromDisk();
|
const snapshot = readCurrentStateFromDisk();
|
||||||
if (snapshot) await pass.add(STATE_KEY, JSON.stringify(snapshot));
|
if (snapshot) {
|
||||||
|
snapshot.syncMasterDeviceId = shortId(pass.writerKey);
|
||||||
|
const statePath = getStateFilePath();
|
||||||
|
if (statePath) {
|
||||||
|
try { fs.writeFileSync(statePath, JSON.stringify(snapshot, null, 2), 'utf8'); } catch (_) {}
|
||||||
|
}
|
||||||
|
await pass.add(STATE_KEY, JSON.stringify(snapshot));
|
||||||
|
}
|
||||||
log('Sync: created new sync group (this device is Master) and invite');
|
log('Sync: created new sync group (this device is Master) and invite');
|
||||||
return { ok: true, invite: currentInvite };
|
return { ok: true, invite: currentInvite };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -595,6 +655,77 @@ function readCurrentStateFromDisk() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delink this device: notify peers (removeWriter), remove sync data, reset state to defaults.
|
||||||
|
* Peers will see this device drop from activeWriters and can remove it from their linked list.
|
||||||
|
*/
|
||||||
|
async function delink() {
|
||||||
|
if (!storageDir || !holesailManager) return { ok: false, error: 'Storage path or holesail manager not set' };
|
||||||
|
await ensureInitialized();
|
||||||
|
if (!pass) return { ok: false, error: 'Not linked' };
|
||||||
|
const setTunnelsRestoredPromiseFn = setTunnelsRestoredPromise;
|
||||||
|
const restorePersistedTunnelsFn = restorePersistedTunnels;
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
await pass.removeWriter(pass.writerKey);
|
||||||
|
if (pass.member && typeof pass.member.flushed === 'function') await pass.member.flushed();
|
||||||
|
} catch (e) {
|
||||||
|
if (process.stderr) process.stderr.write('[sync-manager] delink removeWriter: ' + e.message + '\n');
|
||||||
|
}
|
||||||
|
await closePass();
|
||||||
|
removeAutopassDir();
|
||||||
|
clearIdentity();
|
||||||
|
const restored = await holesailManager.resetStateToDefaults();
|
||||||
|
if (setTunnelsRestoredPromiseFn && restorePersistedTunnelsFn) {
|
||||||
|
const restorePromise = restorePersistedTunnelsFn(holesailManager, restored).catch((e) =>
|
||||||
|
log('Post-delink tunnel restore failed:', e.message)
|
||||||
|
);
|
||||||
|
setTunnelsRestoredPromiseFn(restorePromise);
|
||||||
|
await restorePromise;
|
||||||
|
}
|
||||||
|
lastSyncedAt = null;
|
||||||
|
log('Sync: delinked; state reset to defaults');
|
||||||
|
return { ok: true };
|
||||||
|
} catch (e) {
|
||||||
|
if (process.stderr) process.stderr.write('[sync-manager] delink failed: ' + e.message + '\n');
|
||||||
|
return { ok: false, error: e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disband the sync group (master only). Pushes state with syncGroupDisbanded: true so peers
|
||||||
|
* reset to defaults and remove their sync data. Master keeps local state but removes own sync data.
|
||||||
|
*/
|
||||||
|
async function disband() {
|
||||||
|
if (!storageDir || !holesailManager) return { ok: false, error: 'Storage path or holesail manager not set' };
|
||||||
|
const identity = loadIdentity();
|
||||||
|
if (!identity || !identity.isMaster) return { ok: false, error: 'Only the master can disband the sync group' };
|
||||||
|
await ensureInitialized();
|
||||||
|
if (!pass) return { ok: false, error: 'Not linked' };
|
||||||
|
const setStateSaveSuppressed = holesailManager.setStateSaveSuppressed;
|
||||||
|
try {
|
||||||
|
const snapshot = holesailManager.getStateSnapshot();
|
||||||
|
snapshot.syncGroupDisbanded = true;
|
||||||
|
if (typeof setStateSaveSuppressed === 'function') setStateSaveSuppressed(true);
|
||||||
|
try {
|
||||||
|
await pass.add(STATE_KEY, JSON.stringify(snapshot));
|
||||||
|
if (pass.member && typeof pass.member.flushed === 'function') await pass.member.flushed();
|
||||||
|
} finally {
|
||||||
|
if (typeof setStateSaveSuppressed === 'function') setStateSaveSuppressed(false);
|
||||||
|
}
|
||||||
|
await closePass();
|
||||||
|
removeAutopassDir();
|
||||||
|
clearIdentity();
|
||||||
|
lastSyncedAt = null;
|
||||||
|
log('Sync: group disbanded by master');
|
||||||
|
return { ok: true };
|
||||||
|
} catch (e) {
|
||||||
|
if (typeof setStateSaveSuppressed === 'function') setStateSaveSuppressed(false);
|
||||||
|
if (process.stderr) process.stderr.write('[sync-manager] disband failed: ' + e.message + '\n');
|
||||||
|
return { ok: false, error: e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function cleanup() {
|
async function cleanup() {
|
||||||
initPromise = null;
|
initPromise = null;
|
||||||
weJustPushed = false;
|
weJustPushed = false;
|
||||||
@@ -613,6 +744,8 @@ module.exports = {
|
|||||||
getSyncStatus,
|
getSyncStatus,
|
||||||
createSyncInvite,
|
createSyncInvite,
|
||||||
pairWithInvite,
|
pairWithInvite,
|
||||||
|
delink,
|
||||||
|
disband,
|
||||||
onStateSaved,
|
onStateSaved,
|
||||||
applySyncedState,
|
applySyncedState,
|
||||||
closeSyncForBackup,
|
closeSyncForBackup,
|
||||||
|
|||||||
Reference in New Issue
Block a user