- 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)
- state.js: add lastLoadedDeviceNames cache, getDeviceNames(),
setDeviceNames(); saveStateSync() always merges and writes
deviceNames so saves never strip them.
- holesail-manager: include deviceNames in saveState() and
getStateSnapshot(); applySnapshotData() updates cache from
synced snapshot. buildDefaultState and legacy migration
include deviceNames: {}.
Prevents hostnames reverting to "Device 1" etc. after saves or sync.
- Device that creates sync group is the Master; MASTER label in Linked devices table
- Create invite when already linked generates invite for same sync group (no wipe)
- Any peer can create invites so devices can be added when Master is offline
- Identity file stores isMaster; same sync group reused across native host restarts
- Update SYNC.md, NATIVE-HOST.md, ARCHITECTURE.md, CHANGELOG.md
- 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
- getSyncStatus() now iterates pass.base.activeWriters and returns
linkedDevices: [{ id, isCurrent }] for each writer in the sync group
- Dashboard Linked devices table shows every device: "This device" plus
"Device 2", "Device 3", etc. with their IDs; rows sorted by id
- Fallback to single "This device" row when activeWriters is missing
- Await restorePersistedTunnels so applyingSync stays true for full restore,
preventing saveState() during tunnel startup from pushing back and looping
- Debounce onRemoteUpdate (600ms) and skip apply when state unchanged
(canonical fingerprint) to avoid redundant restarts
- Add confirm modal before "Link device": warns that current state will be
replaced and suggests creating a backup first
- Add Linked devices table when linked: show This device ID (writerKey) and
Sync group ID (discoveryKey); getSyncStatus returns deviceId and syncGroupId
- Add install-command helper (getInstallCommand + platform detection)
with OS-specific one-liners and configurable base URL
- Show onboarding card on Overview when !hostConnected: copy button,
platform switcher (macOS/Linux / Windows), and "Check again" button
- "Check again" triggers refresh and shows success/disconnected toast
- refresh() returns state so callers can react to hostConnected
- Add unit tests for install-commands (all platforms, base URL)
- Document "Native host not found" flow in README and INSTALLATION
- Add unit tests (hostname-validator, TLDs, payload-schemas) and integration tests for message handler registry
- Refactor native host message router into handler registry (handlers/state, tunnels, ssh, rdp, backup, ca, connections)
- Add ESLint config and npm test + lint steps in CI
- Dashboard: visibility-based refresh pause, configurable refresh interval (2s/5s/10s/paused)
- Accessibility: ARIA on nav and modals, focus trap and restore, prefers-reduced-motion
- Empty states: primary action buttons for virtual hosts, servers, service tunnels
- Native host rate limiting for backup and CA operations; update SECURITY.md
- CONTRIBUTING: "Adding a new dashboard page", dev workflow; add npm run dev script
- Trigger getState + applyPAC after successful restoreBackup (same as setVirtualHost/removeVirtualHost)
- Always re-apply PAC when getState returns state so restored/different vhost TLDs are reflected
- Add "Use TLS (secure connection)" checkbox in Add Virtual Host modal
- Persist and restore useTls in state; show TLS badge in table
- When enabled, HTTPS proxy connects to tunnel backend over TLS (SNI =
hostname) for HTTP and WebSocket; supports services on port 443
Firefox's default MV3 CSP includes upgrade-insecure-requests, which upgrades
ws://127.0.0.1 to wss:// and breaks SSH/RDP WebSocket connections to the
local native host. Add explicit content_security_policy to manifest_firefox.json
allowing ws://127.0.0.1:* and http://127.0.0.1:* on extension pages.
- Firefox rejects native messaging manifest if allowed_origins is present.
Install script now writes Chrome-only manifest (allowed_origins) and
Firefox-only manifest (allowed_extensions) to their respective locations.
- Fix local Firefox manifest by removing allowed_origins.
- Log disconnect reason from port.error (Firefox) in addition to
runtime.lastError (Chrome) so native messaging failures are visible.
- Log getState failures on disconnect for easier debugging.
- Treat PAC as active when mode === 'pac_script' (Chrome) or proxyType ===
'autoConfig' (Firefox) so we don't mis-detect "not active" in Firefox.
- In proxy.settings.onChange, only call applyPAC() when levelOfControl !==
'controlled_by_this_extension'. When we still have control, return without
re-applying to stop the set -> onChange -> applyPAC loop.
Firefox's match pattern validator does not allow the moz-extension:// scheme
in matches. Use extension_ids: ["*"] so extension pages can load the
resources without invalid manifest error.
Firefox MV3 does not support background.service_worker. Split background
into background-boot.js (globals) and background-main.js (startup logic);
Chrome keeps using importScripts() in background.js, Firefox manifest uses
background.scripts with the same file list so the extension loads in both.
- Add extension/manifest_firefox.json (no key, moz-extension://*/* for
web_accessible_resources) for correct Firefox MV3 behavior
- Update pack-extension.js: zip excludes manifest_firefox.json; XPI
built separately with manifest_firefox.json as manifest.json
- Update generate-extension-id.js to set gecko.id in manifest_firefox.json
when present (no key in Firefox manifest)
- Document dual-manifest layout and pack/generate-id behavior in CONTRIBUTING.md
Use var(--bg) for btn-primary text color and color-mix for hover to
correctly adapt across themes. Add --terminal-bg CSS variable and
_getXtermTheme() helper so the SSH terminal picks up the correct
palette at connection time.
Normalize loadAvg to [0,0,0] fallback in both the native host handler
and the dashboard renderer to prevent TypeError when bare-node-os returns
a non-array value for loadavg().
- Add getProcessStats message handler to native host returning process
memory (rss, heap used/total, external), PID, uptime, OS memory,
CPU load averages, core count, platform/arch, and tunnel state counts
- Add stats-tracker.js to background SW tracking cumulative connections,
reconnect count, last disconnect time, and per-minute connection rate
ring buffer (30 buckets) for sparkline display
- Include statsHistory and activeConnections array in all getState responses
- Add Statistics dashboard page with six sections: Native Host Process,
System Resources, Tunnel Health, Proxy Traffic, Peer Connections
(with SVG sparkline), and Extension & Proxy Info
- Preserve checkbox selection across table re-renders on all bulk-action
tables (virtual hosts, servers, service tunnels)
- Add theme-aware custom checkbox styling for bulk selection rows using
design system tokens, with white checkmark override for light theme
Replace native browser checkbox appearance on table row and select-all
checkboxes with fully custom styled controls using design system tokens,
with a white checkmark override for the light theme's darker cyan.
Snapshot checked row IDs before each innerHTML swap in the virtual hosts,
service tunnels, and server tunnels tables so that periodic refresh cycles
no longer reset multi-selection state.
chrome.permissions.request() must be called synchronously within a user
gesture. Moving it from the background message-router (where the gesture
context has expired) into the addVhostSubmit click handler in virtual-hosts.js
fixes the "Unchecked runtime.lastError: This function must be called during
a user gesture" console error.
Reverts the experimental feature that attempted to serve the dashboard
via a local HTTP server registered as a hardcoded `my.dash.board` virtual
host, intended to satisfy Chrome's PWA "secure origin" installability
requirement. The approach caused proxy connection errors and file-not-found
issues that were not worth resolving.
Removed: native-host/dashboard-server.js, native-host/test-chain.mjs
Reverted: startup.js, message-router.js, virtual-hosts.js, index.js,
build-distributable.js, extension/pages/virtual-hosts.js,
CI workflows (ci.yml, release.yml)
Fix "Proxy error: connection reset by peer" caused by dashboard-server.js
looking for files in native-host/ instead of extension/dashboard/. Remove
the devBasePath parameter and resolve DEV_DASHBOARD_DIR directly from
__dirname so dev and distribution mode both find the correct files.
Fix all manifest warnings (start_url/id/scope/protocol_handlers ignored)
by reverting manifest.webmanifest to relative/extension-origin URLs for
the chrome-extension:// context, and having dashboard-server.js dynamically
generate a manifest at /manifest.webmanifest with absolute
https://my.dash.board/ URLs — satisfying Chrome's same-origin requirement
for PWA installability.
Also serve extension icons at https://my.dash.board/icons/<file> with
correct bundle key mapping, and embed them in the distributable binary
via build-distributable.js.
Serve the dashboard over a local HTTPS virtual host (my.dash.board) so
Chrome treats it as a secure origin and shows the PWA install prompt.
- Add native-host/dashboard-server.js: bare-http1 static file server
backed by bare-bundle assets in distribution mode, disk fallback in dev
- Add setLocalVirtualHost() to virtual-hosts.js; type:local entries are
protected from removal and filtered out of saveState persistence
- Export setLocalVirtualHost from holesail-manager/index.js
- Start dashboard server in startup.js after proxies are ready and
register my.dash.board as a local virtual host
- Stop dashboard server in message-router.js cleanup()
- Update manifest.webmanifest: start_url, id, scope → https://my.dash.board/
- Embed all extension/dashboard/ files as bare-bundle assets in
build-distributable.js patchBundle() — no installer copy step needed
- Hide checkbox, hs:// URL, Reconnect, and Remove controls for built-in
my.dash.board row in the virtual hosts table UI
Read runtime.lastError in the onDisconnect callback so Chrome does not
log "Unchecked runtime.lastError: Native host has exited" to the
extensions console. The disconnect reason is forwarded to debugLog so
it remains visible when Debug Mode is enabled.
Chrome warns that an empty fetch listener adds navigation overhead.
Removed the handler entirely — a registered SW without a fetch handler
still satisfies Chrome's PWA installability requirement since v112.
The inline <script> for service worker registration was blocked by the
extension's script-src 'self' Content Security Policy. Extracted it to
register-sw.js and replaced the inline block with a <script src>.
Added register-sw.js to web_accessible_resources.
Add ov-empty CSS class to .ov-scroll-area when an overview list has no
items — disables overflow scrolling and uses flexbox to vertically and
horizontally center the empty message. Class is removed automatically
when data is present. Keeps the normal scrollable layout intact for
populated lists.
The Root CA card and Install CA modal incorrectly stated the CA only
covers *.hole.sail domains. Updated both to accurately describe that
the CA covers all virtual host domains using any private TLD
(e.g. myapp.hole.sail, api.my.internal).
Add a minimal sw.js with an empty fetch handler — required by Chrome
for the PWA install prompt to appear. Register it at the bottom of
dashboard.html with a silent catch. Add "scope": "." to the manifest.
Register sw.js in web_accessible_resources so Chrome can fetch it from
the extension origin.
- Fix icon purpose from "any maskable" to "any" to avoid incorrect
padding on some platforms
- Add explicit id field matching start_url to stabilise the App ID
- Add display_override with window-controls-overlay for native title bar
- Register web+holesail:// protocol handler so installed app can be
launched directly from custom protocol links
Add wide (1024x515) and narrow (390x844) screenshots to the web app
manifest so Chrome displays the richer install UI with a preview on
both desktop and mobile. Register screenshots directory in
web_accessible_resources so Chrome can fetch the images from the
extension origin.
Replace all extension icons (16, 32, 48, 128, 192, 512) with versions
derived from the new 512x512 brand icon for consistency. Add 192x192
and 512x512 entries to manifest.webmanifest — Chrome requires both
sizes to show the "Install page as app" option in the browser menu.
Mark the 512px icon as maskable for OS launcher compatibility.
Add manifest.webmanifest to the dashboard so Chrome can install it as
a standalone app via the browser menu. Register the manifest in
web_accessible_resources and link it from dashboard.html. Add a
dedicated PWA install section to docs/INSTALLATION.md covering install
steps, uninstall, and the requirement that the extension remains
installed.
"Sending to native" and "Received from native" are emitted on every
poll cycle and clutter the logs panel. Moved both to debugLog so they
only appear when Debug Mode is enabled in Settings.
- Default latencyPingEnabled to false so badges are hidden until opted in
- Fix toggleLatencyPing sync condition from !== false to === true to match
the new default correctly
- Default tunnelAutoReconnect to true so tunnels reconnect automatically
out of the box
- Hide latency badges in virtual hosts and service tunnels tables when
latency ping is disabled
Track a _settingsDirty flag that is set whenever the user interacts with
a settings toggle or number input. updateSettingsUI() skips the sync while
dirty so the 2-second refresh cycle cannot revert in-progress edits.
The flag is cleared after a successful save or reset.
- Snapshot and restore latency badge values across innerHTML re-renders
so badges never flash back to "…ms" on the 2-second state refresh cycle
- Add CSS transition (color 0.4s ease) and in-flight opacity dim (.pinging)
to .latency-badge for smooth color changes
- Decouple latency pinging from the state refresh cycle — pings now run on
their own independent interval instead of being triggered by refresh()
- Add two new settings under a "Latency Ping" section in Settings:
- Enable Latency Ping toggle (latencyPingEnabled, default: true)
- Ping Interval in seconds (latencyPingIntervalMs, default: 5s)
- Restart ping interval immediately on settings save so changes take effect
without a page reload