feat(settings): add Sync Servers toggle (off by default)
CI / Build & Test (push) Successful in 4m29s

- Add syncServers setting (default false) to extension and native host state
- Add "Sync Servers" toggle under Settings → Sync; when disabled, server
  tunnels stay local and are not synced across linked devices
- On push: omit servers from snapshot when syncServers is false
- On apply: keep local servers and nextServerId when syncServers is false
  (incremental and full restore)
This commit is contained in:
Raven Scott
2026-03-16 17:25:58 -04:00
parent 6984ff70ac
commit 9eeb387941
6 changed files with 35 additions and 5 deletions
+2 -1
View File
@@ -14,7 +14,8 @@ const SETTINGS_DEFAULTS = {
tunnelAutoReconnect: true,
latencyPingEnabled: false,
latencyPingIntervalMs: 5000,
dashboardRefreshIntervalMs: 2000
dashboardRefreshIntervalMs: 2000,
syncServers: false
};
let currentState = null;
+12
View File
@@ -688,6 +688,18 @@
<div class="toggle" id="toggleTunnelAutoReconnect" data-setting="tunnelAutoReconnect"></div>
</div>
</div>
<div class="section-heading">Sync</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Sync Servers</div>
<div class="setting-desc">When enabled, server tunnels are synced across linked devices. When disabled (default), server tunnels stay local to this device.</div>
</div>
<div class="setting-control">
<div class="toggle" id="toggleSyncServers" data-setting="syncServers"></div>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Debug Mode</div>
+2
View File
@@ -26,6 +26,7 @@ function updateSettingsUI() {
$('toggleNotify')?.classList.toggle('active', settings.notifyOnDisconnect === true);
$('toggleNotifyTunnelError')?.classList.toggle('active', settings.notifyOnTunnelError !== false);
$('toggleTunnelAutoReconnect')?.classList.toggle('active', settings.tunnelAutoReconnect === true);
$('toggleSyncServers')?.classList.toggle('active', settings.syncServers === true);
$('toggleDebug')?.classList.toggle('active', settings.debug === true);
$('toggleDisableFileUrls')?.classList.toggle('active', settings.disableOnFileUrls === true);
$('toggleTheme')?.classList.toggle('active', document.documentElement.getAttribute('data-theme') === 'light');
@@ -48,6 +49,7 @@ function saveSettings() {
settings.notifyOnDisconnect = $('toggleNotify')?.classList.contains('active') ?? SETTINGS_DEFAULTS.notifyOnDisconnect;
settings.notifyOnTunnelError = $('toggleNotifyTunnelError')?.classList.contains('active') ?? SETTINGS_DEFAULTS.notifyOnTunnelError;
settings.tunnelAutoReconnect = $('toggleTunnelAutoReconnect')?.classList.contains('active') ?? SETTINGS_DEFAULTS.tunnelAutoReconnect;
settings.syncServers = $('toggleSyncServers')?.classList.contains('active') ?? SETTINGS_DEFAULTS.syncServers;
settings.debug = $('toggleDebug')?.classList.contains('active') ?? SETTINGS_DEFAULTS.debug;
settings.disableOnFileUrls = $('toggleDisableFileUrls')?.classList.contains('active') ?? SETTINGS_DEFAULTS.disableOnFileUrls;
settings.latencyPingEnabled = $('toggleLatencyPing')?.classList.contains('active') ?? SETTINGS_DEFAULTS.latencyPingEnabled;
+1
View File
@@ -47,6 +47,7 @@ function updateSettings(patch) {
if (typeof patch.dashboardRefreshIntervalMs === 'number' && patch.dashboardRefreshIntervalMs >= 0 && [0, 2000, 5000, 10000].includes(patch.dashboardRefreshIntervalMs)) {
currentSettings.dashboardRefreshIntervalMs = patch.dashboardRefreshIntervalMs;
}
if (typeof patch.syncServers === 'boolean') currentSettings.syncServers = patch.syncServers;
if (_saveState) _saveState();
return { requiresRestart };
}
+2 -1
View File
@@ -23,7 +23,8 @@ const SETTINGS_DEFAULTS = {
tunnelAutoReconnect: true,
latencyPingEnabled: false,
latencyPingIntervalMs: 5000,
dashboardRefreshIntervalMs: 2000
dashboardRefreshIntervalMs: 2000,
syncServers: false
};
const DEBUG = process.env.HOLESAIL_DEBUG === '1' || process.env.HOLESAIL_DEBUG === 'true';
+16 -3
View File
@@ -315,7 +315,9 @@ async function applySyncedStateIncremental(snapshot) {
if (myId) snapshot.deviceNames[myId] = typeof os.hostname === 'function' ? os.hostname() : 'device';
}
const current = getStateSnapshot();
const snapServers = Array.isArray(snapshot.servers) ? snapshot.servers : [];
const settings = holesailManager.getSettings ? holesailManager.getSettings() : {};
const syncServers = settings.syncServers === true;
const snapServers = syncServers && Array.isArray(snapshot.servers) ? snapshot.servers : [];
const snapVhosts = Array.isArray(snapshot.virtualHosts) ? snapshot.virtualHosts : [];
const snapSvc = Array.isArray(snapshot.serviceTunnels) ? snapshot.serviceTunnels : [];
const curVhosts = new Map((current.virtualHosts || []).map(v => [String(v.hostname || '').toLowerCase(), v]));
@@ -325,7 +327,8 @@ async function applySyncedStateIncremental(snapshot) {
const snapServersById = new Map(snapServers.map(s => [s.id, s]));
const snapSvcById = new Map(snapSvc.map(t => [t.id, t]));
applySnapshotData(snapshot);
const snapshotToApply = syncServers ? snapshot : { ...snapshot, servers: [], nextServerId: current.nextServerId };
applySnapshotData(snapshotToApply);
for (const [hostname] of curVhosts) {
if (!snapVhostsByHost.has(hostname)) {
@@ -378,7 +381,8 @@ async function applySyncedStateIncremental(snapshot) {
const dir = path.dirname(statePath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(statePath, JSON.stringify(snapshot, null, 2), 'utf8');
const toWrite = syncServers ? snapshot : { ...snapshot, servers: current.servers || [], nextServerId: current.nextServerId };
fs.writeFileSync(statePath, JSON.stringify(toWrite, null, 2), 'utf8');
} finally {
setStateSaveSuppressed(false);
applyingSync = false;
@@ -399,6 +403,12 @@ async function applySyncedStateFull(snapshot) {
const myId = shortId(pass.writerKey);
if (myId) snapshot.deviceNames[myId] = typeof os.hostname === 'function' ? os.hostname() : 'device';
}
const settings = holesailManager.getSettings ? holesailManager.getSettings() : {};
if (settings.syncServers !== true) {
const current = holesailManager.getStateSnapshot ? holesailManager.getStateSnapshot() : {};
snapshot.servers = Array.isArray(current.servers) ? current.servers : [];
snapshot.nextServerId = typeof current.nextServerId === 'number' ? current.nextServerId : 0;
}
const dir = path.dirname(statePath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const json = JSON.stringify(snapshot, null, 2);
@@ -430,6 +440,9 @@ function onStateSaved(snapshot) {
if (applyingSync || !pass) return;
(async () => {
try {
if (holesailManager && typeof holesailManager.getSettings === 'function' && !holesailManager.getSettings().syncServers) {
snapshot.servers = [];
}
const myId = shortId(pass.writerKey);
if (myId) {
snapshot.deviceNames = snapshot.deviceNames || {};