CI / Build & Test (push) Successful in 2m54s
Add docs/CONTRIBUTING.md covering the build system, dev workflow, all npm scripts, how to add new native host message types, code style, and debugging guidance. Add CHANGELOG.md at the project root documenting all features and fixes across the 1.0.0 release. Add JSDoc (@param, @returns) to all previously undocumented exported functions across 35 JS files: - native-host/holesail-manager/ (index, virtual-hosts, service-tunnels, servers, port-allocator) - native-host top-level managers (startup, connect-proxy, https-proxy, certificate-authority, ssh-manager, rdp-manager) - extension/background/ (logs, native-messaging, proxy, message-router) - extension/dashboard/core/ (utils, navigation, init) - extension/dashboard/ui/ (modal, toast, state-tag) - extension/dashboard/pages/ (all 10 page files) - extension/dashboard/refresh.js, events.js - extension/dashboard/data/hostname-validator.js - scripts/ (build-host, run-install)
237 lines
11 KiB
JavaScript
237 lines
11 KiB
JavaScript
/**
|
|
* Global event wiring for the dashboard.
|
|
* Wires toggle switches (theme, notifications, auto-reconnect), settings buttons,
|
|
* export/import handlers, Holesail Lookup, and calls each page module's setup function.
|
|
* Depends on: all page modules
|
|
*/
|
|
|
|
/**
|
|
* Attach all global dashboard event listeners.
|
|
* Guarded by `document._holesailEventsSetup` to prevent duplicate registration.
|
|
*/
|
|
function setupEvents() {
|
|
// Guard against duplicate listener registration if setupEvents() is called
|
|
// more than once (e.g. after a hot-reload). Without this, anonymous listeners
|
|
// accumulate and each toggle fires N times per click after N calls.
|
|
if (document._holesailEventsSetup) return;
|
|
document._holesailEventsSetup = true;
|
|
|
|
// Restore saved theme before any toggle listeners fire
|
|
const savedTheme = localStorage.getItem('holesail-theme');
|
|
if (savedTheme === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
|
|
|
// Toggle switches
|
|
document.querySelectorAll('.toggle').forEach(toggle => {
|
|
toggle.addEventListener('click', () => {
|
|
toggle.classList.toggle('active');
|
|
if (toggle.id === 'toggleTheme') {
|
|
const isLight = toggle.classList.contains('active');
|
|
document.documentElement.setAttribute('data-theme', isLight ? 'light' : 'dark');
|
|
localStorage.setItem('holesail-theme', isLight ? 'light' : 'dark');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Save settings
|
|
$('btnSaveSettings')?.addEventListener('click', saveSettings);
|
|
|
|
// Reset settings to defaults
|
|
$('btnResetSettings')?.addEventListener('click', () => {
|
|
settings = { ...SETTINGS_DEFAULTS };
|
|
chrome.runtime.sendMessage(
|
|
{ target: 'holesail-native', action: 'send', payload: { type: 'updateSettings', payload: { ...settings } } },
|
|
(response) => {
|
|
if (response && response.settings) settings = { ...SETTINGS_DEFAULTS, ...response.settings };
|
|
updateSettingsUI();
|
|
showToast('Settings reset to defaults', 'success');
|
|
}
|
|
);
|
|
});
|
|
|
|
// Export configuration
|
|
$('btnExportConfig')?.addEventListener('click', () => {
|
|
chrome.runtime.sendMessage(
|
|
{ target: 'holesail-native', action: 'send', payload: { type: 'getState', payload: {} } },
|
|
(response) => {
|
|
if (chrome.runtime.lastError || !response || !response.ok) {
|
|
showToast('Export failed: could not read state', 'error');
|
|
return;
|
|
}
|
|
const state = response;
|
|
const exportData = {
|
|
exportedAt: new Date().toISOString(),
|
|
version: 1,
|
|
virtualHosts: (state.virtualHosts || []).map(v => ({ hostname: v.hostname, hsUrl: v.hsUrl })),
|
|
serviceTunnels: (state.serviceTunnels || []).map(t => ({ label: t.label, hsUrl: t.hsUrl, localPort: t.localPort })),
|
|
sshConnections: (state.sshConnections || []).map(s => ({ label: s.label, hsUrl: s.hsUrl, username: s.username })),
|
|
rdpConnections: (state.rdpConnections || []).map(r => ({ label: r.label, hsUrl: r.hsUrl, type: r.type, port: r.port, width: r.width, height: r.height, username: r.username }))
|
|
};
|
|
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'holesail-config-' + new Date().toISOString().slice(0, 10) + '.json';
|
|
a.click();
|
|
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
|
showToast('Configuration exported', 'success');
|
|
}
|
|
);
|
|
});
|
|
|
|
// Import configuration
|
|
$('importConfigFile')?.addEventListener('change', (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
const reader = new FileReader();
|
|
reader.onload = (ev) => {
|
|
let data;
|
|
try { data = JSON.parse(ev.target.result); } catch (_) { showToast('Invalid JSON file', 'error'); return; }
|
|
if (!data || typeof data !== 'object') { showToast('Invalid config file', 'error'); return; }
|
|
|
|
const vhosts = Array.isArray(data.virtualHosts) ? data.virtualHosts : [];
|
|
const svcTunnels = Array.isArray(data.serviceTunnels) ? data.serviceTunnels : [];
|
|
const sshConns = Array.isArray(data.sshConnections) ? data.sshConnections : [];
|
|
const rdpConns = Array.isArray(data.rdpConnections) ? data.rdpConnections : [];
|
|
|
|
let pending = 0;
|
|
let done = 0;
|
|
function onDone() {
|
|
done++;
|
|
if (done >= pending) {
|
|
showToast('Import complete', 'success');
|
|
e.target.value = '';
|
|
if (typeof refreshAll === 'function') refreshAll();
|
|
}
|
|
}
|
|
|
|
for (const v of vhosts) {
|
|
if (!v.hostname || !v.hsUrl) continue;
|
|
pending++;
|
|
chrome.runtime.sendMessage(
|
|
{ target: 'holesail-native', action: 'send', payload: { type: 'setVirtualHost', payload: { hostname: v.hostname, hsUrl: v.hsUrl } } },
|
|
() => onDone()
|
|
);
|
|
}
|
|
for (const t of svcTunnels) {
|
|
if (!t.hsUrl) continue;
|
|
pending++;
|
|
chrome.runtime.sendMessage(
|
|
{ target: 'holesail-native', action: 'send', payload: { type: 'startServiceTunnel', payload: { label: t.label || '', hsUrl: t.hsUrl, localPort: t.localPort } } },
|
|
() => onDone()
|
|
);
|
|
}
|
|
|
|
if (sshConns.length || rdpConns.length) {
|
|
pending++;
|
|
chrome.runtime.sendMessage(
|
|
{ target: 'holesail-native', action: 'send', payload: { type: 'getState', payload: {} } },
|
|
(response) => {
|
|
void chrome.runtime.lastError;
|
|
const existing = response || {};
|
|
const mergedSsh = [...(existing.sshConnections || [])];
|
|
for (const s of sshConns) {
|
|
if (!s.hsUrl) continue;
|
|
if (!mergedSsh.find(x => x.hsUrl === s.hsUrl && x.username === s.username)) {
|
|
mergedSsh.push({ id: 'ssh-' + Date.now() + '-' + Math.random().toString(36).slice(2), label: s.label || '', hsUrl: s.hsUrl, username: s.username || '' });
|
|
}
|
|
}
|
|
const mergedRdp = [...(existing.rdpConnections || [])];
|
|
for (const r of rdpConns) {
|
|
if (!r.hsUrl) continue;
|
|
if (!mergedRdp.find(x => x.hsUrl === r.hsUrl)) {
|
|
mergedRdp.push({ id: 'rdp-' + Date.now() + '-' + Math.random().toString(36).slice(2), label: r.label || '', hsUrl: r.hsUrl, type: r.type || 'vnc', port: r.port || 5900, width: r.width || 1280, height: r.height || 720, username: r.username || '' });
|
|
}
|
|
}
|
|
chrome.runtime.sendMessage(
|
|
{ target: 'holesail-native', action: 'send', payload: { type: 'setSshConnections', payload: { connections: mergedSsh } } },
|
|
() => {
|
|
void chrome.runtime.lastError;
|
|
chrome.runtime.sendMessage(
|
|
{ target: 'holesail-native', action: 'send', payload: { type: 'setRdpConnections', payload: { connections: mergedRdp } } },
|
|
() => { void chrome.runtime.lastError; onDone(); }
|
|
);
|
|
}
|
|
);
|
|
}
|
|
);
|
|
}
|
|
|
|
if (pending === 0) {
|
|
showToast('Nothing to import', 'default');
|
|
e.target.value = '';
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
});
|
|
|
|
// Holesail Lookup
|
|
function runPeerLookup() {
|
|
const input = $('peerLookupInput');
|
|
if (!input) return;
|
|
const key = input.value.trim();
|
|
if (!key) { showToast('Enter an hs:// key', 'default'); return; }
|
|
|
|
const body = $('holesailLookupBody');
|
|
if (body) body.innerHTML = '<div style="text-align:center;padding:12px 0;color:var(--text3);">Looking up…</div>';
|
|
openModal('modal-holesailLookup');
|
|
|
|
chrome.runtime.sendMessage(
|
|
{ target: 'holesail-native', action: 'send', payload: { type: 'lookup', payload: { hsUrl: key } } },
|
|
(response) => {
|
|
void chrome.runtime.lastError;
|
|
if (!body) return;
|
|
if (!response || !response.ok) {
|
|
const errMsg = (response && response.error) || (chrome.runtime.lastError && chrome.runtime.lastError.message) || 'No response';
|
|
body.innerHTML = `
|
|
<div style="display:flex;align-items:center;gap:10px;color:var(--red);">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:20px;height:20px;flex-shrink:0;"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
<span>${escapeHtml(errMsg)}</span>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
const host = response.host || null;
|
|
const port = response.port || null;
|
|
const protocol = response.protocol ? response.protocol.toUpperCase() : null;
|
|
const isPrivate = response.secure === true;
|
|
|
|
if (!host && !port && !protocol) {
|
|
body.innerHTML = `
|
|
<div style="display:flex;align-items:center;gap:10px;color:var(--amber);">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:20px;height:20px;flex-shrink:0;"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
<span>No record found for the provided key.</span>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
function row(label, value, valueColor) {
|
|
return `<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--border);">
|
|
<span style="color:var(--text2);font-size:12.5px;">${label}</span>
|
|
<span style="font-family:'JetBrains Mono',monospace;font-size:12.5px;color:${valueColor || 'var(--text1)'};font-weight:500;">${escapeHtml(String(value))}</span>
|
|
</div>`;
|
|
}
|
|
|
|
body.innerHTML = `
|
|
<div style="margin-bottom:4px;">
|
|
${row('Host', host || 'N/A', host ? 'var(--cyan)' : 'var(--text3)')}
|
|
${row('Port', port || 'N/A', port ? 'var(--cyan)' : 'var(--text3)')}
|
|
${row('Protocol', protocol || 'N/A', protocol ? 'var(--green)' : 'var(--text3)')}
|
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 0;">
|
|
<span style="color:var(--text2);font-size:12.5px;">Private</span>
|
|
<span style="font-size:12.5px;font-weight:500;color:${isPrivate ? 'var(--amber)' : 'var(--green)'};">
|
|
${isPrivate ? '🔒 Yes' : '🌐 No'}
|
|
</span>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
);
|
|
}
|
|
$('btnPeerLookup')?.addEventListener('click', runPeerLookup);
|
|
$('peerLookupInput')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') runPeerLookup(); });
|
|
|
|
setupVirtualHostEvents();
|
|
setupServerEvents();
|
|
setupServiceTunnelEvents();
|
|
setupLogsEvents();
|
|
}
|