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.
252 lines
9.7 KiB
JavaScript
252 lines
9.7 KiB
JavaScript
// 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`);
|
|
}
|