efactor(extension): modularize dashboard, background, and docs
CI / Build & Test (push) Failing after 28s
CI / Build & Test (push) Failing after 28s
Split monolithic dashboard.js (2753 lines) into 18 focused modules
under dashboard/{core,data,ui,pages}/ with refresh.js and events.js
as orchestrators. Extracted ~900-line inline <style> into dashboard.css
and moved dashboard.html to dashboard/dashboard.html.
Split background.js (609 lines) into background/{logs,state,proxy,
native-messaging,tab-lifecycle,message-router}.js with a thin entry
point using importScripts().
Deleted dead files: wrong-domain.js (duplicate of inline script).
Updated manifest.json web_accessible_resources for new paths.
Updated docs/ARCHITECTURE.md to reflect the new file structure.
No functionality changed. No build step introduced.
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
// Depends on: core/utils.js ($, escapeHtml), ui/state-tag.js (stateTag),
|
||||
// ui/modal.js (openModal), pages/ssh.js (openAddSshModal),
|
||||
// pages/rdp.js (openAddRdpModal), core/state.js (sshConnections, rdpConnections)
|
||||
|
||||
const OV_PAGE = 20; // rows per page
|
||||
|
||||
class OvList {
|
||||
constructor({ bodyId, sentinelId, searchId, subtitleId, rowFn, emptyMsg, cols }) {
|
||||
this.body = $(bodyId);
|
||||
this.sentinel = $(sentinelId);
|
||||
this.searchEl = $(searchId);
|
||||
this.subtitleEl = $(subtitleId);
|
||||
this.rowFn = rowFn;
|
||||
this.emptyMsg = emptyMsg;
|
||||
this.cols = cols;
|
||||
this.data = [];
|
||||
this.filtered = [];
|
||||
this.rendered = 0;
|
||||
this._debounce = null;
|
||||
this._observer = null;
|
||||
this._initObserver();
|
||||
this._initSearch();
|
||||
}
|
||||
|
||||
_initObserver() {
|
||||
if (!this.sentinel) return;
|
||||
this._observer = new IntersectionObserver(entries => {
|
||||
if (entries[0].isIntersecting) this._appendPage();
|
||||
}, { threshold: 0 });
|
||||
this._observer.observe(this.sentinel);
|
||||
}
|
||||
|
||||
_initSearch() {
|
||||
if (!this.searchEl) return;
|
||||
this.searchEl.addEventListener('input', () => {
|
||||
clearTimeout(this._debounce);
|
||||
this._debounce = setTimeout(() => this._applyFilter(), 180);
|
||||
});
|
||||
}
|
||||
|
||||
setData(data, subtitleText) {
|
||||
this.data = data;
|
||||
if (this.subtitleEl) this.subtitleEl.textContent = subtitleText;
|
||||
this._applyFilter();
|
||||
}
|
||||
|
||||
_applyFilter() {
|
||||
const q = (this.searchEl?.value || '').trim().toLowerCase();
|
||||
this.filtered = q
|
||||
? this.data.filter(item => JSON.stringify(item).toLowerCase().includes(q))
|
||||
: this.data.slice();
|
||||
this._reset();
|
||||
}
|
||||
|
||||
_reset() {
|
||||
if (!this.body) return;
|
||||
this.rendered = 0;
|
||||
this.body.innerHTML = '';
|
||||
if (this.filtered.length === 0) {
|
||||
this.body.innerHTML = `<tr><td colspan="${this.cols}" class="empty">${this.emptyMsg}</td></tr>`;
|
||||
return;
|
||||
}
|
||||
this._appendPage();
|
||||
}
|
||||
|
||||
_appendPage() {
|
||||
if (!this.body || this.rendered >= this.filtered.length) return;
|
||||
const next = this.filtered.slice(this.rendered, this.rendered + OV_PAGE);
|
||||
this.body.insertAdjacentHTML('beforeend', next.map(this.rowFn).join(''));
|
||||
this.rendered += next.length;
|
||||
}
|
||||
}
|
||||
|
||||
// Instances — created once, reused on every updateDashboard call
|
||||
let _ovVhost = null;
|
||||
let _ovServers = null;
|
||||
let _ovSvc = null;
|
||||
let _ovSsh = null;
|
||||
let _ovRdp = null;
|
||||
|
||||
function initOvLists() {
|
||||
_ovVhost = new OvList({
|
||||
bodyId: 'recentConnections', sentinelId: 'ovVhostSentinel',
|
||||
searchId: 'ovVhostSearch', subtitleId: 'ovVhostSubtitle',
|
||||
cols: 3, emptyMsg: 'No virtual hosts',
|
||||
rowFn: v => {
|
||||
const h = v.hostname || v.id || '';
|
||||
return `<tr>
|
||||
<td style="font-size:12px;"><span class="mono-chip">${escapeHtml(h)}</span></td>
|
||||
<td>${stateTag(v.state)}</td>
|
||||
<td><a href="https://${escapeHtml(h)}" target="_blank" rel="noopener"
|
||||
class="btn btn-ghost btn-sm" style="padding:3px 8px;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
style="width:11px;height:11px;">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15,3 21,3 21,9"/><line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>Open</a></td>
|
||||
</tr>`;
|
||||
}
|
||||
});
|
||||
|
||||
_ovServers = new OvList({
|
||||
bodyId: 'ovServersBody', sentinelId: 'ovServersSentinel',
|
||||
searchId: 'ovServersSearch', subtitleId: 'ovServersSubtitle',
|
||||
cols: 3, emptyMsg: 'No server tunnels',
|
||||
rowFn: s => {
|
||||
const key = s.url || s.hsUrl || '';
|
||||
const short = key.length > 22 ? key.slice(0, 9) + '…' + key.slice(-7) : key;
|
||||
const display = s.label ? escapeHtml(s.label) : `<span class="mono" style="font-size:11px;color:var(--text3);">${escapeHtml(String(s.port || '—'))}</span>`;
|
||||
return `<tr>
|
||||
<td style="font-size:12px;">${display}</td>
|
||||
<td>${stateTag(s.state)}</td>
|
||||
<td class="mono" style="font-size:11px;color:var(--text3);"
|
||||
title="${escapeHtml(key)}">${escapeHtml(short)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
});
|
||||
|
||||
_ovSvc = new OvList({
|
||||
bodyId: 'ovSvcBody', sentinelId: 'ovSvcSentinel',
|
||||
searchId: 'ovSvcSearch', subtitleId: 'ovSvcSubtitle',
|
||||
cols: 3, emptyMsg: 'No service tunnels',
|
||||
rowFn: t => `<tr>
|
||||
<td style="font-size:12px;">${escapeHtml(t.label || '—')}</td>
|
||||
<td class="mono" style="font-size:12px;">${escapeHtml(String(t.localPort || '—'))}</td>
|
||||
<td>${stateTag(t.state)}</td>
|
||||
</tr>`
|
||||
});
|
||||
|
||||
_ovSsh = new OvList({
|
||||
bodyId: 'ovSshBody', sentinelId: 'ovSshSentinel',
|
||||
searchId: 'ovSshSearch', subtitleId: 'ovSshSubtitle',
|
||||
cols: 2, emptyMsg: 'No SSH connections',
|
||||
rowFn: c => `<tr>
|
||||
<td style="font-size:12px;">${escapeHtml(c.label || c.hsUrl || '—')}</td>
|
||||
<td class="mono" style="font-size:12px;color:var(--text3);">${escapeHtml(c.username || '—')}</td>
|
||||
</tr>`
|
||||
});
|
||||
|
||||
_ovRdp = new OvList({
|
||||
bodyId: 'ovRdpBody', sentinelId: 'ovRdpSentinel',
|
||||
searchId: 'ovRdpSearch', subtitleId: 'ovRdpSubtitle',
|
||||
cols: 2, emptyMsg: 'No remote desktops',
|
||||
rowFn: c => `<tr>
|
||||
<td style="font-size:12px;">${escapeHtml(c.label || '—')}</td>
|
||||
<td><span class="badge badge-neutral" style="font-size:10px;padding:2px 6px;">
|
||||
${escapeHtml((c.type || 'vnc').toUpperCase())}</span></td>
|
||||
</tr>`
|
||||
});
|
||||
|
||||
// Quick action buttons
|
||||
$('qaAddVhost') ?.addEventListener('click', () => openModal('modal-addVhost'));
|
||||
$('qaAddServer')?.addEventListener('click', () => {
|
||||
const editIdEl = $('serverEditId');
|
||||
if (editIdEl) editIdEl.value = '';
|
||||
const labelEl = $('startServerLabel');
|
||||
if (labelEl) labelEl.value = '';
|
||||
const portEl = $('startServerPort');
|
||||
if (portEl) portEl.value = '3000';
|
||||
const hostEl = $('startServerHost');
|
||||
if (hostEl) hostEl.value = '127.0.0.1';
|
||||
const secureEl = $('startServerSecure');
|
||||
if (secureEl) secureEl.checked = true;
|
||||
const tcpEl = $('startServerProtocolTcp');
|
||||
if (tcpEl) tcpEl.checked = true;
|
||||
const titleEl = $('modal-startServer-title');
|
||||
if (titleEl) titleEl.textContent = 'Start Server Tunnel';
|
||||
const submitEl = $('startServerSubmit');
|
||||
if (submitEl) submitEl.textContent = 'Start Server';
|
||||
openModal('modal-startServer');
|
||||
});
|
||||
$('qaAddSvc') ?.addEventListener('click', () => openModal('modal-addServiceTunnel'));
|
||||
$('qaAddSsh') ?.addEventListener('click', () => openAddSshModal(null));
|
||||
$('qaAddRdp') ?.addEventListener('click', () => openAddRdpModal(null));
|
||||
$('qaInstallCA') ?.addEventListener('click', () => openModal('modal-installCA'));
|
||||
}
|
||||
|
||||
function updateDashboard(state) {
|
||||
currentState = state;
|
||||
const servers = state.servers || [];
|
||||
const virtualHosts = state.virtualHosts || [];
|
||||
const serviceTunnels = state.serviceTunnels || [];
|
||||
|
||||
const dot = $('sidebarDot');
|
||||
const statusText = $('sidebarStatus');
|
||||
if (state.hostConnected) {
|
||||
dot?.classList.add('connected');
|
||||
dot?.classList.remove('disconnected');
|
||||
if (statusText) statusText.textContent = 'Connected';
|
||||
} else {
|
||||
dot?.classList.remove('connected');
|
||||
dot?.classList.add('disconnected');
|
||||
if (statusText) statusText.textContent = 'Disconnected';
|
||||
}
|
||||
|
||||
const setText = (id, val) => { const el = $(id); if (el) el.textContent = val; };
|
||||
|
||||
setText('dashConnections', virtualHosts.length);
|
||||
setText('dashSwarms', servers.length);
|
||||
setText('dashServiceTunnels', serviceTunnels.length);
|
||||
setText('dashSsh', sshConnections.length);
|
||||
setText('dashRdp', rdpConnections.length);
|
||||
setText('dashUptime', state.caInstalled ? 'Trusted' : 'Not trusted');
|
||||
|
||||
const caIcon = $('caStatIcon');
|
||||
if (caIcon) {
|
||||
caIcon.style.background = state.caInstalled ? 'var(--green-dim)' : 'var(--red-dim)';
|
||||
caIcon.style.color = state.caInstalled ? 'var(--green)' : 'var(--red)';
|
||||
}
|
||||
|
||||
const ovHostDot = $('ovHostDot');
|
||||
if (ovHostDot) {
|
||||
ovHostDot.classList.toggle('connected', !!state.hostConnected);
|
||||
ovHostDot.classList.toggle('disconnected', !state.hostConnected);
|
||||
}
|
||||
setText('ovHostLabel', state.hostConnected ? 'Connected' : 'Disconnected');
|
||||
const ovCaDot = $('ovCaDot');
|
||||
if (ovCaDot) {
|
||||
ovCaDot.style.background = state.caInstalled ? 'var(--green)' : 'var(--red)';
|
||||
ovCaDot.style.boxShadow = state.caInstalled ? '0 0 5px var(--green)' : 'none';
|
||||
}
|
||||
setText('ovCaLabel', state.caInstalled ? 'Installed & trusted' : 'Not installed');
|
||||
setText('ovProxyLabel', state.proxyPort != null ? `port ${state.proxyPort}` : '—');
|
||||
setText('ovConnectLabel', state.connectProxyPort != null ? `port ${state.connectProxyPort}` : '—');
|
||||
|
||||
const lu = $('overviewLastUpdated');
|
||||
if (lu) lu.textContent = 'Updated ' + new Date().toLocaleTimeString();
|
||||
|
||||
setText('swarmCount', servers.length);
|
||||
setText('connCount', virtualHosts.length);
|
||||
setText('serviceTunnelCount', serviceTunnels.length);
|
||||
setText('sshCount', sshConnections.length);
|
||||
setText('rdpCount', rdpConnections.length);
|
||||
setText('tabCount', state.caInstalled ? 'CA ✓' : 'CA');
|
||||
|
||||
if (!_ovVhost) initOvLists();
|
||||
|
||||
const sub = (n, unit, readyArr) => {
|
||||
if (n === 0) return `No ${unit}s`;
|
||||
const r = readyArr.filter(x => x.state === 'ready').length;
|
||||
return `${n} ${unit}${n !== 1 ? 's' : ''} — ${r} ready`;
|
||||
};
|
||||
|
||||
_ovVhost .setData(virtualHosts, sub(virtualHosts.length, 'host', virtualHosts));
|
||||
_ovServers.setData(servers, sub(servers.length, 'tunnel', servers));
|
||||
_ovSvc .setData(serviceTunnels, sub(serviceTunnels.length, 'tunnel', serviceTunnels));
|
||||
_ovSsh .setData(sshConnections, sshConnections.length === 0
|
||||
? 'No SSH connections' : `${sshConnections.length} saved`);
|
||||
_ovRdp .setData(rdpConnections, rdpConnections.length === 0
|
||||
? 'No remote desktops' : `${rdpConnections.length} saved`);
|
||||
}
|
||||
Reference in New Issue
Block a user