feat(sync): show device hostnames in Linked devices table; document sync fully
CI / Build & Test (push) Successful in 4m20s
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
This commit is contained in:
@@ -350,10 +350,13 @@ All state is owned by the native host and persisted to `state.json` next to the
|
||||
],
|
||||
"rdpConnections": [
|
||||
{ "id": "rdp-abc123", "label": "Work PC", "hsUrl": "hs://xyz...", "type": "vnc", "port": 5901, "width": 1280, "height": 720, "username": "", "passwordB64": "cGFzc3dvcmQ=" }
|
||||
]
|
||||
],
|
||||
"deviceNames": { "a1b2c3d4e5f6": "macbook-pro", "b2c3d4e5f678": "work-laptop" }
|
||||
}
|
||||
```
|
||||
|
||||
When sync is used, `deviceNames` (optional) maps each device’s short hex ID to its OS hostname so the Linked devices table can show names instead of “Device 2”, etc.
|
||||
|
||||
Additional settings (e.g. `notifyOnTunnelError`, `backupIntervalHours`, `tunnelAutoReconnect`, `latencyPingEnabled`, `latencyPingIntervalMs`) may be present; see `native-host/holesail-manager/state.js` `SETTINGS_DEFAULTS`.
|
||||
|
||||
## Native host lifecycle
|
||||
|
||||
+4
-3
@@ -525,15 +525,16 @@ Return whether this device is linked for sync, optional invite/last-synced info,
|
||||
"invite": "optional invite string if one was created",
|
||||
"lastSyncedAt": 1710000000000,
|
||||
"deviceId": "a1b2c3d4e5f6",
|
||||
"deviceName": "macbook-pro",
|
||||
"syncGroupId": "f6e5d4c3b2a1",
|
||||
"linkedDevices": [
|
||||
{ "id": "a1b2c3d4e5f6", "isCurrent": true },
|
||||
{ "id": "b2c3d4e5f678", "isCurrent": false }
|
||||
{ "id": "a1b2c3d4e5f6", "isCurrent": true, "name": "macbook-pro" },
|
||||
{ "id": "b2c3d4e5f678", "isCurrent": false, "name": "work-laptop" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
When linked, `deviceId` is a short hex ID for this device’s writer key and `syncGroupId` is a short hex ID for the shared discovery key. `linkedDevices` is populated from the autopass active writers; it may contain only this device until other peers have replicated at least once.
|
||||
When linked, `deviceId` is a short hex ID for this device’s writer key, `deviceName` is the OS hostname of this device, and `syncGroupId` is a short hex ID for the shared discovery key. `linkedDevices` is populated from the autopass active writers; each entry includes `name` (hostname) when known from synced state, so other peers’ hostnames appear after they have replicated at least once. It may contain only this device until other peers have replicated.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@ Holesail Browser can sync its state across two devices using [autopass](https://
|
||||
|
||||
When linked, the Sync page shows a **Linked devices** table:
|
||||
|
||||
- **This device** — This machine’s device ID (a short hex fingerprint of its autopass writer key).
|
||||
- **Device 2**, **Device 3**, … — Other devices in the same sync group, each with their own ID. Other peers appear in the table after they have replicated at least once; if the other device is offline or has not yet synced, you may only see “This device” until replication runs.
|
||||
- **Device name** — Each device is shown by its **hostname** (the OS hostname of the machine, e.g. `macbook-pro`, `work-laptop`). Hostnames are synced in state so once a device has replicated at least once, other peers will see its hostname in the table. If a hostname is not yet known, the table falls back to “This device” for the current machine or “Device 2”, “Device 3”, etc. for others.
|
||||
- **Device ID** — The short hex ID (fingerprint of the device’s autopass writer key) is shown next to each name. Other peers appear in the table after they have replicated at least once; if the other device is offline or has not yet synced, you may only see this device until replication runs.
|
||||
- **Sync group** — A shared ID (fingerprint of the discovery key) that is the same on every device in the group. You can confirm both devices are in the same group by checking that the Sync group ID matches on each.
|
||||
|
||||
## More than two devices
|
||||
|
||||
@@ -51,11 +51,12 @@ function updateSyncStatus() {
|
||||
let rows = '';
|
||||
if (linkedDevices.length > 0) {
|
||||
linkedDevices.forEach((d, i) => {
|
||||
const label = d.isCurrent ? 'This device' : ('Device ' + (i + 1));
|
||||
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 {
|
||||
rows = '<tr><td>This device</td><td class="mono" style="font-size:12px;">' + escapeHtml(response.deviceId || '—') + '</td></tr>';
|
||||
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;
|
||||
|
||||
@@ -105,7 +105,8 @@ function loadState() {
|
||||
sshConnections: Array.isArray(data.sshConnections) ? data.sshConnections : [],
|
||||
rdpConnections: Array.isArray(data.rdpConnections) ? data.rdpConnections : [],
|
||||
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: (data.deviceNames && typeof data.deviceNames === 'object') ? { ...data.deviceNames } : {}
|
||||
};
|
||||
debugLog('state loaded path=', file, 'servers=', out.servers.length, 'vhosts=', out.virtualHosts.length);
|
||||
if (process.stderr && (out.servers.length || out.virtualHosts.length)) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
const path = require('bare-path');
|
||||
const fs = require('bare-fs');
|
||||
const b4a = require('b4a');
|
||||
const os = require('bare-os');
|
||||
|
||||
const STATE_KEY = 'holesail-state';
|
||||
const IDENTITY_FILENAME = 'autopass-identity.json';
|
||||
@@ -201,6 +202,11 @@ async function applySyncedState(snapshot) {
|
||||
if (!statePath) return;
|
||||
applyingSync = true;
|
||||
try {
|
||||
snapshot.deviceNames = snapshot.deviceNames || {};
|
||||
if (pass && pass.writerKey) {
|
||||
const myId = shortId(pass.writerKey);
|
||||
if (myId) snapshot.deviceNames[myId] = typeof os.hostname === 'function' ? os.hostname() : 'device';
|
||||
}
|
||||
const dir = path.dirname(statePath);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
const json = JSON.stringify(snapshot, null, 2);
|
||||
@@ -218,12 +224,25 @@ async function applySyncedState(snapshot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by holesail-manager after each saveState(). Push snapshot to autopass (if linked and not applying).
|
||||
* Called by holesail-manager after each saveState(). Merge this device's hostname into state, write to disk, then push to autopass (if linked and not applying).
|
||||
*/
|
||||
function onStateSaved(snapshot) {
|
||||
if (applyingSync || !pass) return;
|
||||
(async () => {
|
||||
try {
|
||||
const myId = shortId(pass.writerKey);
|
||||
if (myId) {
|
||||
snapshot.deviceNames = snapshot.deviceNames || {};
|
||||
snapshot.deviceNames[myId] = typeof os.hostname === 'function' ? os.hostname() : 'device';
|
||||
}
|
||||
const statePath = getStateFilePath();
|
||||
if (statePath) {
|
||||
try {
|
||||
fs.writeFileSync(statePath, JSON.stringify(snapshot, null, 2), 'utf8');
|
||||
} catch (e) {
|
||||
if (process.stderr) process.stderr.write('[sync-manager] write deviceNames failed: ' + e.message + '\n');
|
||||
}
|
||||
}
|
||||
weJustPushed = true;
|
||||
const value = JSON.stringify(snapshot);
|
||||
await pass.add(STATE_KEY, value);
|
||||
@@ -246,6 +265,18 @@ function shortId(buf) {
|
||||
return hex.slice(0, 12);
|
||||
}
|
||||
|
||||
function readDeviceNamesFromState() {
|
||||
const statePath = getStateFilePath();
|
||||
if (!statePath || !fs.existsSync(statePath)) return {};
|
||||
try {
|
||||
const raw = fs.readFileSync(statePath, 'utf8');
|
||||
const data = JSON.parse(raw);
|
||||
return (data.deviceNames && typeof data.deviceNames === 'object') ? data.deviceNames : {};
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function getSyncStatus() {
|
||||
await ensureInitialized();
|
||||
const out = {
|
||||
@@ -257,25 +288,31 @@ async function getSyncStatus() {
|
||||
try {
|
||||
out.deviceId = shortId(pass.writerKey) || null;
|
||||
out.syncGroupId = shortId(pass.discoveryKey) || null;
|
||||
out.deviceName = typeof os.hostname === 'function' ? os.hostname() : null;
|
||||
out.linkedDevices = [];
|
||||
const myKeyHex = b4a.toString(pass.writerKey, 'hex');
|
||||
const deviceNames = readDeviceNamesFromState();
|
||||
if (pass.base && pass.base.activeWriters) {
|
||||
for (const w of pass.base.activeWriters) {
|
||||
if (!w || !w.core || !w.core.key) continue;
|
||||
const keyHex = b4a.toString(w.core.key, 'hex');
|
||||
const id = shortId(w.core.key);
|
||||
const isCurrent = keyHex === myKeyHex;
|
||||
out.linkedDevices.push({
|
||||
id: shortId(w.core.key),
|
||||
isCurrent: keyHex === myKeyHex
|
||||
id,
|
||||
isCurrent,
|
||||
name: deviceNames[id] || (isCurrent ? out.deviceName : null)
|
||||
});
|
||||
}
|
||||
}
|
||||
if (out.linkedDevices.length === 0 && out.deviceId) {
|
||||
out.linkedDevices = [{ id: out.deviceId, isCurrent: true }];
|
||||
out.linkedDevices = [{ id: out.deviceId, isCurrent: true, name: out.deviceName }];
|
||||
}
|
||||
} catch (e) {
|
||||
out.deviceId = null;
|
||||
out.syncGroupId = null;
|
||||
out.linkedDevices = out.deviceId ? [{ id: out.deviceId, isCurrent: true }] : [];
|
||||
out.deviceName = null;
|
||||
out.linkedDevices = out.deviceId ? [{ id: out.deviceId, isCurrent: true, name: null }] : [];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
|
||||
Reference in New Issue
Block a user