Update Overview
CI / Build & Test (push) Successful in 2m59s

This commit is contained in:
Raven Scott
2026-02-28 17:55:46 -05:00
parent 028f2e22d9
commit b9b4727658
2 changed files with 313 additions and 76 deletions
+186 -43
View File
@@ -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
View File
@@ -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 ──────────────────────────────────────────────────────