+186
-43
@@ -215,7 +215,8 @@
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@media (max-width: 900px) { .stats-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||
@media (max-width: 1100px) { .stats-grid { grid-template-columns: repeat(3, 1fr) !important; } }
|
||||
@media (max-width: 700px) { .stats-grid { grid-template-columns: repeat(2, 1fr) !important; } }
|
||||
|
||||
.stat-card {
|
||||
background: var(--card);
|
||||
@@ -986,73 +987,215 @@
|
||||
<div class="page active" id="page-dashboard">
|
||||
<div class="page-header" style="margin-bottom:16px;">
|
||||
<h2>Overview</h2>
|
||||
<div id="overviewLastUpdated" style="font-size:11px;color:var(--text4);align-self:center;"></div>
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
|
||||
<!-- System status bar -->
|
||||
<div id="overviewStatusBar" style="display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);margin-bottom:14px;flex-wrap:wrap;">
|
||||
<div style="display:flex;align-items:center;gap:7px;flex:1;min-width:160px;">
|
||||
<div class="status-dot" id="ovHostDot"></div>
|
||||
<span style="font-size:12.5px;font-weight:500;color:var(--text2);">Native Host</span>
|
||||
<span id="ovHostLabel" style="font-size:11.5px;color:var(--text3);">—</span>
|
||||
</div>
|
||||
<div style="width:1px;height:18px;background:var(--border);flex-shrink:0;"></div>
|
||||
<div style="display:flex;align-items:center;gap:7px;flex:1;min-width:140px;">
|
||||
<div id="ovCaDot" style="width:7px;height:7px;border-radius:50%;background:var(--text4);flex-shrink:0;"></div>
|
||||
<span style="font-size:12.5px;font-weight:500;color:var(--text2);">Root CA</span>
|
||||
<span id="ovCaLabel" style="font-size:11.5px;color:var(--text3);">—</span>
|
||||
</div>
|
||||
<div style="width:1px;height:18px;background:var(--border);flex-shrink:0;"></div>
|
||||
<div style="display:flex;align-items:center;gap:7px;flex:1;min-width:160px;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px;color:var(--text4);flex-shrink:0;"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
<span style="font-size:12.5px;font-weight:500;color:var(--text2);">HTTPS Proxy</span>
|
||||
<span id="ovProxyLabel" style="font-size:11.5px;color:var(--text3);">—</span>
|
||||
</div>
|
||||
<div style="width:1px;height:18px;background:var(--border);flex-shrink:0;"></div>
|
||||
<div style="display:flex;align-items:center;gap:7px;flex:1;min-width:160px;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px;color:var(--text4);flex-shrink:0;"><polyline points="22,12 18,12 15,21 9,3 6,12 2,12"/></svg>
|
||||
<span style="font-size:12.5px;font-weight:500;color:var(--text2);">CONNECT Proxy</span>
|
||||
<span id="ovConnectLabel" style="font-size:11.5px;color:var(--text3);">—</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stat cards — 6 across -->
|
||||
<div class="stats-grid" style="grid-template-columns:repeat(6,1fr);margin-bottom:14px;">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon cyan">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="2"/>
|
||||
<circle cx="4" cy="6" r="2"/><circle cx="20" cy="6" r="2"/>
|
||||
<line x1="6" y1="6" x2="10" y2="11"/><line x1="18" y1="6" x2="14" y2="11"/>
|
||||
</svg>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><circle cx="4" cy="6" r="2"/><circle cx="20" cy="6" r="2"/><line x1="6" y1="6" x2="10" y2="11"/><line x1="18" y1="6" x2="14" y2="11"/></svg>
|
||||
</div>
|
||||
<div class="stat-value" id="dashConnections">0</div>
|
||||
<div class="stat-label">Virtual Hosts</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon amber">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="3" width="6" height="6" rx="1"/>
|
||||
<rect x="16" y="3" width="6" height="6" rx="1"/>
|
||||
<rect x="9" y="15" width="6" height="6" rx="1"/>
|
||||
<line x1="5" y1="9" x2="12" y2="15"/><line x1="19" y1="9" x2="12" y2="15"/>
|
||||
</svg>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="6" height="6" rx="1"/><rect x="16" y="3" width="6" height="6" rx="1"/><rect x="9" y="15" width="6" height="6" rx="1"/><line x1="5" y1="9" x2="12" y2="15"/><line x1="19" y1="9" x2="12" y2="15"/></svg>
|
||||
</div>
|
||||
<div class="stat-value" id="dashSwarms">0</div>
|
||||
<div class="stat-label">Server Tunnels</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon green">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2"/>
|
||||
<line x1="8" y1="21" x2="16" y2="21"/>
|
||||
<line x1="12" y1="17" x2="12" y2="21"/>
|
||||
</svg>
|
||||
<div class="stat-icon" style="background:rgba(139,92,246,.12);color:#a78bfa;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
||||
</div>
|
||||
<div class="stat-value" id="dashTabs">—</div>
|
||||
<div class="stat-label">Proxy Port</div>
|
||||
<div class="stat-value" id="dashServiceTunnels">0</div>
|
||||
<div class="stat-label">Service Tunnels</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" id="caStatIcon" style="background:var(--red-dim);color:var(--red)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||
</svg>
|
||||
<div class="stat-icon green">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><polyline points="8,10 12,14 16,10"/></svg>
|
||||
</div>
|
||||
<div class="stat-value" id="dashUptime">—</div>
|
||||
<div class="stat-value" id="dashSsh">0</div>
|
||||
<div class="stat-label">SSH Connections</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background:rgba(251,113,133,.12);color:#fb7185;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
||||
</div>
|
||||
<div class="stat-value" id="dashRdp">0</div>
|
||||
<div class="stat-label">Remote Desktops</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" id="caStatIcon" style="background:var(--red-dim);color:var(--red);">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
</div>
|
||||
<div class="stat-value" id="dashUptime" style="font-size:16px;">—</div>
|
||||
<div class="stat-label">CA Status</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Virtual hosts preview -->
|
||||
<!-- Two-column layout: Virtual Hosts + right column -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px;">
|
||||
|
||||
<!-- Virtual Hosts preview -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<div class="card-title">Virtual Hosts</div>
|
||||
<div class="card-subtitle" id="ovVhostSubtitle">No virtual hosts configured</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm" data-page-link="connections">View all →</button>
|
||||
</div>
|
||||
<div class="card-body flush">
|
||||
<table class="table">
|
||||
<thead><tr><th>Hostname</th><th>State</th><th>Open</th></tr></thead>
|
||||
<tbody id="recentConnections">
|
||||
<tr><td colspan="3" class="empty">No virtual hosts yet</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server Tunnels preview -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<div class="card-title">Server Tunnels</div>
|
||||
<div class="card-subtitle" id="ovServersSubtitle">No server tunnels running</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm" data-page-link="swarms">View all →</button>
|
||||
</div>
|
||||
<div class="card-body flush">
|
||||
<table class="table">
|
||||
<thead><tr><th>Port</th><th>State</th><th>hs:// Key</th></tr></thead>
|
||||
<tbody id="ovServersBody">
|
||||
<tr><td colspan="3" class="empty">No server tunnels yet</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Second row: Service Tunnels + SSH + Remote Desktop -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:12px;">
|
||||
|
||||
<!-- Service Tunnels preview -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<div class="card-title">Service Tunnels</div>
|
||||
<div class="card-subtitle" id="ovSvcSubtitle">No service tunnels</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm" data-page-link="service-tunnels">View all →</button>
|
||||
</div>
|
||||
<div class="card-body flush">
|
||||
<table class="table">
|
||||
<thead><tr><th>Label</th><th>Port</th><th>State</th></tr></thead>
|
||||
<tbody id="ovSvcBody">
|
||||
<tr><td colspan="3" class="empty">None</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SSH preview -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<div class="card-title">SSH Connections</div>
|
||||
<div class="card-subtitle" id="ovSshSubtitle">No SSH connections saved</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm" data-page-link="ssh">View all →</button>
|
||||
</div>
|
||||
<div class="card-body flush">
|
||||
<table class="table">
|
||||
<thead><tr><th>Label</th><th>User</th></tr></thead>
|
||||
<tbody id="ovSshBody">
|
||||
<tr><td colspan="2" class="empty">None</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Remote Desktop preview -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<div class="card-title">Remote Desktops</div>
|
||||
<div class="card-subtitle" id="ovRdpSubtitle">No remote desktops saved</div>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm" data-page-link="rdp">View all →</button>
|
||||
</div>
|
||||
<div class="card-body flush">
|
||||
<table class="table">
|
||||
<thead><tr><th>Label</th><th>Type</th></tr></thead>
|
||||
<tbody id="ovRdpBody">
|
||||
<tr><td colspan="2" class="empty">None</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick actions -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">Virtual hosts</div>
|
||||
<button class="btn btn-ghost btn-sm" data-page-link="connections">View all →</button>
|
||||
<div class="card-title">Quick Actions</div>
|
||||
</div>
|
||||
<div class="card-body flush">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>State</th>
|
||||
<th>Backend</th>
|
||||
<th>Open</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="recentConnections">
|
||||
<tr><td colspan="4" class="empty">No virtual hosts yet</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="card-body" style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||
<button class="btn btn-secondary btn-sm" data-page-link="connections" id="qaAddVhost">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Add Virtual Host
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" data-page-link="swarms" id="qaAddServer">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
New Server Tunnel
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" data-page-link="service-tunnels" id="qaAddSvc">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Add Service Tunnel
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" data-page-link="ssh" id="qaAddSsh">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Add SSH Connection
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" data-page-link="backups" id="qaBackup">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17,8 12,3 7,8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
||||
Create Backup
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" data-page-link="logs" id="qaLogs">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22,12 18,12 15,21 9,3 6,12 2,12"/></svg>
|
||||
View Logs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+127
-33
@@ -888,55 +888,149 @@ function updateDashboard(state) {
|
||||
if (statusText) statusText.textContent = 'Disconnected';
|
||||
}
|
||||
|
||||
// Stats
|
||||
const dashConnections = $('dashConnections');
|
||||
const dashSwarms = $('dashSwarms');
|
||||
const dashTabs = $('dashTabs');
|
||||
const dashUptime = $('dashUptime');
|
||||
if (dashConnections) dashConnections.textContent = virtualHosts.length;
|
||||
if (dashSwarms) dashSwarms.textContent = servers.length;
|
||||
if (dashTabs) dashTabs.textContent = state.proxyPort != null ? state.proxyPort : '—';
|
||||
if (dashUptime) dashUptime.textContent = state.caInstalled ? 'Trusted' : 'Not trusted';
|
||||
const serviceTunnels = state.serviceTunnels || [];
|
||||
|
||||
// ── Stat cards ────────────────────────────────────────────────────────────
|
||||
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');
|
||||
// Keep legacy dashTabs in sync (used by proxy & CA page)
|
||||
setText('dashTabs', state.proxyPort != null ? state.proxyPort : '—');
|
||||
|
||||
// CA stat icon color
|
||||
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)';
|
||||
caIcon.style.color = state.caInstalled ? 'var(--green)' : 'var(--red)';
|
||||
}
|
||||
|
||||
// Nav badges
|
||||
const swarmCount = $('swarmCount');
|
||||
const connCount = $('connCount');
|
||||
const tabCount = $('tabCount');
|
||||
if (swarmCount) swarmCount.textContent = servers.length;
|
||||
if (connCount) connCount.textContent = virtualHosts.length;
|
||||
if (tabCount) tabCount.textContent = state.caInstalled ? 'CA ✓' : 'CA';
|
||||
// ── System status bar ─────────────────────────────────────────────────────
|
||||
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}` : '—');
|
||||
|
||||
// Virtual hosts preview on overview page
|
||||
// Last updated timestamp
|
||||
const lu = $('overviewLastUpdated');
|
||||
if (lu) lu.textContent = 'Updated ' + new Date().toLocaleTimeString();
|
||||
|
||||
// ── Nav badges ────────────────────────────────────────────────────────────
|
||||
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');
|
||||
|
||||
// ── Virtual Hosts preview ─────────────────────────────────────────────────
|
||||
const recentConnBody = $('recentConnections');
|
||||
if (recentConnBody) {
|
||||
const ovVhostSub = $('ovVhostSubtitle');
|
||||
if (virtualHosts.length === 0) {
|
||||
recentConnBody.innerHTML = `<tr><td colspan="4" class="empty">No virtual hosts yet — <button class="btn btn-ghost btn-sm" id="addVhostFromEmpty" style="display:inline-flex;">Add one</button></td></tr>`;
|
||||
if (ovVhostSub) ovVhostSub.textContent = 'No virtual hosts configured';
|
||||
recentConnBody.innerHTML = `<tr><td colspan="3" class="empty">No virtual hosts yet — <button class="btn btn-ghost btn-sm" id="addVhostFromEmpty" style="display:inline-flex;">Add one</button></td></tr>`;
|
||||
$('addVhostFromEmpty')?.addEventListener('click', () => openModal('modal-addVhost'));
|
||||
} else {
|
||||
const proxyPort = state.proxyPort || 8443;
|
||||
recentConnBody.innerHTML = virtualHosts.slice(0, 8).map(v => {
|
||||
const ready = virtualHosts.filter(v => v.state === 'ready').length;
|
||||
if (ovVhostSub) ovVhostSub.textContent = `${virtualHosts.length} host${virtualHosts.length !== 1 ? 's' : ''} — ${ready} ready`;
|
||||
recentConnBody.innerHTML = virtualHosts.slice(0, 6).map(v => {
|
||||
const hostname = v.hostname || v.id || '';
|
||||
const openUrl = `https://${hostname}`;
|
||||
return `
|
||||
<tr>
|
||||
<td><span class="mono-chip">${escapeHtml(hostname)}</span></td>
|
||||
<td>${stateTag(v.state)}</td>
|
||||
<td class="mono" style="font-size:11px;color:var(--text3);">${v.localHost ? v.localHost + ':' + v.localPort : '—'}</td>
|
||||
<td><a href="${openUrl}" target="_blank" rel="noopener" class="btn btn-ghost btn-sm">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:12px;height:12px;"><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>`;
|
||||
return `<tr>
|
||||
<td><span class="mono-chip">${escapeHtml(hostname)}</span></td>
|
||||
<td>${stateTag(v.state)}</td>
|
||||
<td><a href="https://${escapeHtml(hostname)}" 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>`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Server Tunnels preview ────────────────────────────────────────────────
|
||||
const ovServersBody = $('ovServersBody');
|
||||
if (ovServersBody) {
|
||||
const ovSrvSub = $('ovServersSubtitle');
|
||||
if (servers.length === 0) {
|
||||
if (ovSrvSub) ovSrvSub.textContent = 'No server tunnels running';
|
||||
ovServersBody.innerHTML = `<tr><td colspan="3" class="empty">No server tunnels yet</td></tr>`;
|
||||
} else {
|
||||
const ready = servers.filter(s => s.state === 'ready').length;
|
||||
if (ovSrvSub) ovSrvSub.textContent = `${servers.length} tunnel${servers.length !== 1 ? 's' : ''} — ${ready} ready`;
|
||||
ovServersBody.innerHTML = servers.slice(0, 6).map(s => {
|
||||
const key = s.hsUrl || '';
|
||||
const short = key.length > 24 ? key.slice(0, 10) + '…' + key.slice(-8) : key;
|
||||
return `<tr>
|
||||
<td class="mono" style="font-size:12px;">${escapeHtml(String(s.port || '—'))}</td>
|
||||
<td>${stateTag(s.state)}</td>
|
||||
<td class="mono" style="font-size:11px;color:var(--text3);" title="${escapeHtml(key)}">${escapeHtml(short)}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Service Tunnels preview ───────────────────────────────────────────────
|
||||
const ovSvcBody = $('ovSvcBody');
|
||||
if (ovSvcBody) {
|
||||
const ovSvcSub = $('ovSvcSubtitle');
|
||||
if (serviceTunnels.length === 0) {
|
||||
if (ovSvcSub) ovSvcSub.textContent = 'No service tunnels';
|
||||
ovSvcBody.innerHTML = `<tr><td colspan="3" class="empty">None</td></tr>`;
|
||||
} else {
|
||||
const ready = serviceTunnels.filter(t => t.state === 'ready').length;
|
||||
if (ovSvcSub) ovSvcSub.textContent = `${serviceTunnels.length} tunnel${serviceTunnels.length !== 1 ? 's' : ''} — ${ready} ready`;
|
||||
ovSvcBody.innerHTML = serviceTunnels.slice(0, 5).map(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>`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// ── SSH preview ───────────────────────────────────────────────────────────
|
||||
const ovSshBody = $('ovSshBody');
|
||||
if (ovSshBody) {
|
||||
const ovSshSub = $('ovSshSubtitle');
|
||||
if (sshConnections.length === 0) {
|
||||
if (ovSshSub) ovSshSub.textContent = 'No SSH connections saved';
|
||||
ovSshBody.innerHTML = `<tr><td colspan="2" class="empty">None</td></tr>`;
|
||||
} else {
|
||||
if (ovSshSub) ovSshSub.textContent = `${sshConnections.length} saved connection${sshConnections.length !== 1 ? 's' : ''}`;
|
||||
ovSshBody.innerHTML = sshConnections.slice(0, 5).map(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>`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Remote Desktop preview ────────────────────────────────────────────────
|
||||
const ovRdpBody = $('ovRdpBody');
|
||||
if (ovRdpBody) {
|
||||
const ovRdpSub = $('ovRdpSubtitle');
|
||||
if (rdpConnections.length === 0) {
|
||||
if (ovRdpSub) ovRdpSub.textContent = 'No remote desktops saved';
|
||||
ovRdpBody.innerHTML = `<tr><td colspan="2" class="empty">None</td></tr>`;
|
||||
} else {
|
||||
if (ovRdpSub) ovRdpSub.textContent = `${rdpConnections.length} saved connection${rdpConnections.length !== 1 ? 's' : ''}`;
|
||||
ovRdpBody.innerHTML = rdpConnections.slice(0, 5).map(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>`).join('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Virtual Hosts table ──────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user