/** * Overview page — summary stats, quick-action cards, and virtualized connection lists. * Uses the OvList class for infinite-scroll, searchable tables of connections and servers. * 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 /** * Virtualized, searchable, infinite-scroll list for the Overview page. * Renders rows in pages of OV_PAGE, loading more when the sentinel element scrolls into view. */ 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 = `${this.emptyMsg}`; 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; /** * Create and wire up the OvList instances for virtual hosts and server tunnels. * Called once during dashboard initialisation. */ 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 ` ${escapeHtml(h)} ${stateTag(v.state)} Open `; } }); _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) : `${escapeHtml(String(s.port || '—'))}`; return ` ${display} ${stateTag(s.state)} ${escapeHtml(short)} `; } }); _ovSvc = new OvList({ bodyId: 'ovSvcBody', sentinelId: 'ovSvcSentinel', searchId: 'ovSvcSearch', subtitleId: 'ovSvcSubtitle', cols: 3, emptyMsg: 'No service tunnels', rowFn: t => ` ${escapeHtml(t.label || '—')} ${escapeHtml(String(t.localPort || '—'))} ${stateTag(t.state)} ` }); _ovSsh = new OvList({ bodyId: 'ovSshBody', sentinelId: 'ovSshSentinel', searchId: 'ovSshSearch', subtitleId: 'ovSshSubtitle', cols: 2, emptyMsg: 'No SSH connections', rowFn: c => ` ${escapeHtml(c.label || c.hsUrl || '—')} ${escapeHtml(c.username || '—')} ` }); _ovRdp = new OvList({ bodyId: 'ovRdpBody', sentinelId: 'ovRdpSentinel', searchId: 'ovRdpSearch', subtitleId: 'ovRdpSubtitle', cols: 2, emptyMsg: 'No remote desktops', rowFn: c => ` ${escapeHtml(c.label || '—')} ${escapeHtml((c.type || 'vnc').toUpperCase())} ` }); // 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')); } /** * Render the Overview page with the latest extension state. * Updates summary stat cards, quick-action buttons, and the connection/server lists. * @param {object} state - Full extension state from `fetchState()`. */ 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}` : '—'); if (state.trafficStats) { const { bytesIn, bytesOut, requests } = state.trafficStats; const fmt = (n) => n >= 1048576 ? (n / 1048576).toFixed(1) + ' MB' : n >= 1024 ? (n / 1024).toFixed(1) + ' KB' : n + ' B'; setText('ovTrafficLabel', `↑ ${fmt(bytesIn)} ↓ ${fmt(bytesOut)} (${requests} req)`); } 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`); }