Files
holesail-browser/extension/dashboard.html
T
Raven Scott 8303c72140
CI / Build & Test (push) Successful in 3m44s
further tests
2026-02-28 21:07:39 -05:00

2003 lines
88 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Holesail Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="vendor/xterm.css">
<script src="vendor/xterm.js"></script>
<script src="vendor/xterm-addon-fit.js"></script>
<script type="module" src="vendor/novnc.js"></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #09090b;
--surface: #111113;
--card: #18181b;
--elevated: #1f1f23;
--border: #27272a;
--border2: #3f3f46;
--text: #fafafa;
--text2: #a1a1aa;
--text3: #71717a;
--text4: #52525b;
--cyan: #22d3ee;
--cyan-dim: rgba(34,211,238,.12);
--cyan-mid: rgba(34,211,238,.25);
--green: #4ade80;
--green-dim: rgba(74,222,128,.12);
--amber: #fbbf24;
--amber-dim: rgba(251,191,36,.12);
--red: #f43f5e;
--red-dim: rgba(244,63,94,.12);
--red-mid: rgba(244,63,94,.25);
--radius-sm: 6px;
--radius: 10px;
--radius-lg: 14px;
--sidebar-w: 224px;
--transition: 0.15s ease;
}
html, body {
height: 100%;
background: var(--bg);
color: var(--text);
font-family: 'Inter', system-ui, sans-serif;
font-size: 14px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/* ── Scrollbar ─────────────────────────────────── */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 99px; }
::-webkit-scrollbar-thumb:hover { background: var(--text4); }
/* ── Layout ────────────────────────────────────── */
.layout { display: flex; height: 100vh; overflow: hidden; }
/* ── Sidebar ───────────────────────────────────── */
.sidebar {
width: var(--sidebar-w);
flex-shrink: 0;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-logo {
padding: 20px 18px 16px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 10px;
}
.sidebar-logo-icon {
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
overflow: hidden;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.sidebar-logo-icon img {
width: 32px;
height: 32px;
display: block;
}
.sidebar-logo-text { line-height: 1.2; }
.sidebar-logo-name { font-size: 15px; font-weight: 700; color: var(--text); letter-spacing: -0.02em; }
.sidebar-logo-version { font-size: 11px; color: var(--text4); margin-top: 1px; }
.sidebar-nav { flex: 1; padding: 8px 0; overflow-y: auto; }
.nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 14px;
margin: 1px 8px;
border-radius: var(--radius-sm);
color: var(--text3);
font-size: 13.5px;
font-weight: 500;
cursor: pointer;
transition: background var(--transition), color var(--transition);
user-select: none;
}
.nav-item:hover { background: var(--border); color: var(--text2); }
.nav-item.active { background: var(--cyan-dim); color: var(--cyan); }
.nav-item svg { width: 16px; height: 16px; flex-shrink: 0; }
.nav-badge {
margin-left: auto;
font-size: 11px;
font-weight: 600;
padding: 1px 7px;
border-radius: 99px;
background: var(--elevated);
color: var(--text3);
min-width: 20px;
text-align: center;
}
.nav-item.active .nav-badge { background: var(--cyan-mid); color: var(--cyan); }
.sidebar-footer {
padding: 12px 16px;
border-top: 1px solid var(--border);
}
.status-pill {
display: flex;
align-items: center;
gap: 7px;
font-size: 12px;
color: var(--text3);
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--text4);
flex-shrink: 0;
}
.status-dot.connected { background: var(--green); box-shadow: 0 0 6px var(--green); animation: pulse 2.5s infinite; }
.status-dot.disconnected { background: var(--red); }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.45; }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* ── Main ──────────────────────────────────────── */
.main {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
}
/* Top bar */
.topbar {
position: sticky;
top: 0;
z-index: 10;
background: var(--bg);
border-bottom: 1px solid var(--border);
padding: 0 28px;
height: 52px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.topbar-title { font-size: 15px; font-weight: 600; color: var(--text); }
.topbar-actions { display: flex; align-items: center; gap: 8px; }
.content { padding: 24px 28px; flex: 1; }
/* ── Page visibility ───────────────────────────── */
.page { display: none; }
.page.active { display: block; }
/* ── Cards ─────────────────────────────────────── */
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
margin-bottom: 16px;
}
.card-header {
padding: 14px 18px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.card-title { font-size: 13.5px; font-weight: 600; color: var(--text); }
.card-subtitle { font-size: 12px; color: var(--text3); margin-top: 2px; }
.card-body { padding: 16px 18px; }
.card-body.flush { padding: 0; }
/* ── Stats grid ────────────────────────────────── */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 16px;
}
@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);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 16px 18px;
display: flex;
flex-direction: column;
gap: 8px;
}
.stat-icon {
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
}
.stat-icon svg { width: 16px; height: 16px; }
.stat-icon.cyan { background: var(--cyan-dim); color: var(--cyan); }
.stat-icon.green { background: var(--green-dim); color: var(--green); }
.stat-icon.amber { background: var(--amber-dim); color: var(--amber); }
.stat-icon.red { background: var(--red-dim); color: var(--red); }
.stat-value { font-size: 26px; font-weight: 700; color: var(--text); letter-spacing: -0.03em; line-height: 1; }
.stat-label { font-size: 11.5px; font-weight: 500; color: var(--text3); text-transform: uppercase; letter-spacing: 0.06em; }
/* ── Buttons ───────────────────────────────────── */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 7px 14px;
border-radius: var(--radius-sm);
font-size: 13px;
font-weight: 500;
font-family: inherit;
cursor: pointer;
border: none;
transition: all var(--transition);
white-space: nowrap;
text-decoration: none;
}
.btn svg { width: 14px; height: 14px; flex-shrink: 0; }
.btn:disabled { opacity: 0.45; cursor: not-allowed; pointer-events: none; }
.btn-primary { background: var(--cyan); color: #09090b; }
.btn-primary:hover { background: #06b6d4; }
.btn-secondary {
background: var(--elevated);
color: var(--text2);
border: 1px solid var(--border);
}
.btn-secondary:hover { background: var(--border); color: var(--text); }
.btn-ghost { background: transparent; color: var(--text3); }
.btn-ghost:hover { background: var(--border); color: var(--text2); }
.btn-danger { background: var(--red-dim); color: var(--red); border: 1px solid var(--red-mid); }
.btn-danger:hover { background: var(--red-mid); }
.btn-icon {
padding: 6px;
border-radius: var(--radius-sm);
background: transparent;
color: var(--text3);
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: all var(--transition);
}
.btn-icon:hover { background: var(--border); color: var(--text2); }
.btn-icon svg { width: 14px; height: 14px; }
.btn-sm { padding: 4px 10px; font-size: 12px; }
.btn-sm svg { width: 12px; height: 12px; }
.btn-xs { padding: 3px 8px; font-size: 11px; }
/* ── Badges / Tags ─────────────────────────────── */
.badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 99px;
font-size: 11.5px;
font-weight: 500;
}
.badge-cyan { background: var(--cyan-dim); color: var(--cyan); }
.badge-green { background: var(--green-dim); color: var(--green); }
.badge-amber { background: var(--amber-dim); color: var(--amber); }
.badge-red { background: var(--red-dim); color: var(--red); }
.badge-neutral { background: var(--elevated); color: var(--text3); }
/* ── Overview list cards ────────────────────────── */
#page-dashboard.active {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.ov-list-card {
display: flex;
flex-direction: column;
min-height: 0;
/* Each card takes equal share of remaining vertical space */
}
.ov-scroll-area {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
min-height: 80px;
}
.ov-table thead th {
position: sticky;
top: 0;
background: var(--card);
z-index: 1;
}
.ov-sentinel { height: 1px; }
/* Make the two grid rows share available height evenly */
#page-dashboard.active > div[style*="grid-template-columns:1fr 1fr"] {
flex: 1;
min-height: 0;
}
#page-dashboard.active > div[style*="grid-template-columns:1fr 1fr 1fr"] {
flex: 1;
min-height: 0;
}
/* ── Tables ────────────────────────────────────── */
.table { width: 100%; border-collapse: collapse; }
.table th {
padding: 10px 16px;
text-align: left;
font-size: 11px;
font-weight: 600;
color: var(--text4);
text-transform: uppercase;
letter-spacing: 0.07em;
background: var(--elevated);
border-bottom: 1px solid var(--border);
}
.table td {
padding: 11px 16px;
font-size: 13px;
color: var(--text2);
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr:hover td { background: rgba(255,255,255,.025); }
.table .mono {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: var(--cyan);
}
.table .empty {
text-align: center;
color: var(--text4);
padding: 36px 16px;
font-size: 13px;
}
/* ── Mono chip ─────────────────────────────────── */
.mono-chip {
display: inline-flex;
align-items: center;
gap: 6px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: var(--cyan);
background: var(--cyan-dim);
border: 1px solid rgba(34,211,238,.15);
padding: 3px 8px;
border-radius: var(--radius-sm);
}
/* ── Form inputs ───────────────────────────────── */
.input {
background: var(--elevated);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text);
font-family: inherit;
font-size: 13px;
padding: 8px 11px;
outline: none;
transition: border-color var(--transition);
width: 100%;
}
.input:focus { border-color: var(--cyan); }
.input::placeholder { color: var(--text4); }
.input.mono { font-family: 'JetBrains Mono', monospace; font-size: 12px; }
.form-group { display: flex; flex-direction: column; gap: 5px; }
.form-label { font-size: 12px; font-weight: 500; color: var(--text3); }
.form-error { font-size: 12px; color: var(--red); margin-top: 4px; display: none; }
/* ── Toggle ────────────────────────────────────── */
.toggle {
position: relative;
width: 40px;
height: 22px;
background: var(--border2);
border-radius: 99px;
cursor: pointer;
transition: background var(--transition);
flex-shrink: 0;
}
.toggle.active { background: var(--cyan); }
.toggle::after {
content: '';
position: absolute;
top: 3px;
left: 3px;
width: 16px;
height: 16px;
background: #fff;
border-radius: 50%;
transition: transform var(--transition);
box-shadow: 0 1px 3px rgba(0,0,0,.3);
}
.toggle.active::after { transform: translateX(18px); }
/* ── Settings rows ─────────────────────────────── */
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 13px 0;
border-bottom: 1px solid var(--border);
}
.setting-row:last-child { border-bottom: none; }
.setting-info { flex: 1; min-width: 0; }
.setting-label { font-size: 13.5px; color: var(--text); font-weight: 500; }
.setting-desc { font-size: 12px; color: var(--text3); margin-top: 2px; }
.setting-control { flex-shrink: 0; }
/* ── Section heading ───────────────────────────── */
.section-heading {
font-size: 11px;
font-weight: 600;
color: var(--text4);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 10px;
margin-top: 20px;
}
.section-heading:first-child { margin-top: 0; }
/* ── Logs ──────────────────────────────────────── */
.logs-toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.logs-filter {
flex: 1;
min-width: 180px;
max-width: 280px;
}
.logs-container {
background: #0a0a0c;
border: 1px solid var(--border);
border-radius: var(--radius);
height: calc(100vh - 220px);
overflow-y: auto;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
}
.log-entry {
display: flex;
gap: 12px;
padding: 5px 14px;
border-bottom: 1px solid rgba(255,255,255,.03);
}
.log-entry:hover { background: rgba(255,255,255,.025); }
.log-time { color: var(--text4); flex-shrink: 0; font-size: 11px; padding-top: 1px; }
.log-msg { color: var(--text2); word-break: break-all; line-height: 1.5; }
.log-msg.is-error { color: var(--red); }
.log-msg.is-warn { color: var(--amber); }
.log-msg.is-info { color: var(--cyan); }
/* ── Proxy/CA page ─────────────────────────────── */
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.info-item {
background: var(--elevated);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px;
}
.info-item-label { font-size: 11px; font-weight: 600; color: var(--text4); text-transform: uppercase; letter-spacing: 0.07em; margin-bottom: 6px; }
.info-item-value { font-size: 20px; font-weight: 700; color: var(--text); font-family: 'JetBrains Mono', monospace; }
/* ── Empty state ───────────────────────────────── */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
padding: 48px 24px;
color: var(--text4);
text-align: center;
}
.empty-state svg { width: 36px; height: 36px; opacity: 0.4; }
.empty-state-title { font-size: 14px; font-weight: 600; color: var(--text3); }
.empty-state-desc { font-size: 13px; }
/* ── Modals ────────────────────────────────────── */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0,0,0,.7);
backdrop-filter: blur(4px);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
.modal-backdrop.open {
opacity: 1;
pointer-events: all;
}
.modal {
background: var(--card);
border: 1px solid var(--border2);
border-radius: var(--radius-lg);
width: 100%;
max-width: 440px;
margin: 16px;
box-shadow: 0 24px 64px rgba(0,0,0,.6);
transform: translateY(12px) scale(0.98);
transition: transform 0.2s ease;
}
.modal-backdrop.open .modal { transform: translateY(0) scale(1); }
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--border);
}
.modal-title { font-size: 15px; font-weight: 600; color: var(--text); }
.modal-body { padding: 20px; display: flex; flex-direction: column; gap: 14px; }
.modal-desc { font-size: 13px; color: var(--text2); line-height: 1.6; }
.modal-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding: 14px 20px;
border-top: 1px solid var(--border);
}
.modal-error { font-size: 12px; color: var(--red); display: none; }
/* ── Confirm inline ────────────────────────────── */
.confirm-inline {
display: none;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text2);
}
.confirm-inline.show { display: inline-flex; }
/* ── Toast ─────────────────────────────────────── */
.toast {
position: fixed;
bottom: 24px;
right: 24px;
background: var(--elevated);
border: 1px solid var(--border2);
border-radius: var(--radius);
padding: 10px 16px;
font-size: 13px;
color: var(--text);
box-shadow: 0 8px 24px rgba(0,0,0,.4);
z-index: 200;
transform: translateY(8px);
opacity: 0;
pointer-events: none;
transition: all 0.2s ease;
max-width: 320px;
}
.toast.show { opacity: 1; transform: translateY(0); }
.toast.success { border-color: rgba(74,222,128,.3); color: var(--green); }
.toast.error { border-color: var(--red-mid); color: var(--red); }
/* ── Divider ───────────────────────────────────── */
.divider { height: 1px; background: var(--border); margin: 16px 0; }
/* ── Copy feedback ─────────────────────────────── */
.copy-btn { position: relative; }
.copy-tooltip {
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
background: var(--elevated);
border: 1px solid var(--border2);
border-radius: 4px;
padding: 3px 8px;
font-size: 11px;
color: var(--green);
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
}
.copy-btn.copied .copy-tooltip { opacity: 1; }
/* ── Page headers ──────────────────────────────── */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
gap: 12px;
}
.page-header h2 { font-size: 18px; font-weight: 700; color: var(--text); letter-spacing: -0.02em; }
/* ── Inline row layout ─────────────────────────── */
.row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }
.row .form-group { flex: 1; min-width: 120px; }
/* ── Checkbox ──────────────────────────────────── */
.checkbox-row {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
user-select: none;
}
.checkbox-row input[type="checkbox"] {
width: 15px;
height: 15px;
accent-color: var(--cyan);
cursor: pointer;
}
.checkbox-row label { font-size: 13px; color: var(--text2); cursor: pointer; }
/* ── Radio ──────────────────────────────────────── */
.radio-row {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
user-select: none;
padding: 6px 12px;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 13px;
color: var(--text2);
transition: border-color 0.15s, background 0.15s;
}
.radio-row:has(input:checked) {
border-color: var(--cyan);
background: color-mix(in srgb, var(--cyan) 10%, transparent);
color: var(--cyan);
}
.radio-row input[type="radio"] {
accent-color: var(--cyan);
cursor: pointer;
}
/* ── Remote Desktop page ────────────────────────── */
.rdp-conn-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 10px;
transition: border-color var(--transition), background var(--transition);
cursor: default;
}
.rdp-conn-card:hover { border-color: var(--border2); background: var(--elevated); }
.rdp-conn-card-top {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.rdp-conn-icon {
width: 38px;
height: 38px;
border-radius: var(--radius-sm);
background: var(--cyan-dim);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--cyan);
}
.rdp-conn-icon svg { width: 18px; height: 18px; }
.rdp-conn-info { flex: 1; min-width: 0; }
.rdp-conn-label { font-size: 14px; font-weight: 600; color: var(--text); word-break: break-word; }
.rdp-conn-actions { display: flex; gap: 6px; flex-shrink: 0; margin-left: auto; }
.rdp-conn-meta {
font-size: 11px;
color: var(--text3);
font-family: 'JetBrains Mono', monospace;
word-break: break-all;
line-height: 1.5;
padding: 6px 8px;
background: var(--base);
border-radius: var(--radius-sm);
border: 1px solid var(--border);
}
.rdp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 12px; }
/* ── RDP Viewer modal ───────────────────────────── */
.modal-rdp-viewer {
max-width: 95vw;
width: 95vw;
height: 90vh;
padding: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.rdp-viewer-toolbar {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background: var(--elevated);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.rdp-viewer-statusbar {
display: flex;
align-items: center;
gap: 12px;
padding: 6px 14px;
background: var(--elevated);
border-top: 1px solid var(--border);
flex-shrink: 0;
font-size: 11px;
color: var(--text4);
}
.rdp-viewer-statusbar span { color: var(--text3); }
#rdpViewerContainer {
flex: 1;
background: #000;
overflow: hidden;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
#rdpViewerContainer canvas { max-width: 100%; max-height: 100%; display: block; }
/* noVNC injects its own canvas — make it fill the container */
#rdpViewerContainer > :first-child { width: 100% !important; height: 100% !important; }
/* ── SSH page ───────────────────────────────────── */
.ssh-conn-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px 18px;
display: flex;
flex-direction: column;
gap: 10px;
transition: border-color var(--transition), background var(--transition);
cursor: default;
}
.ssh-conn-card:hover { border-color: var(--border2); background: var(--elevated); }
.ssh-conn-card-top {
display: flex;
align-items: center;
gap: 14px;
min-width: 0;
}
.ssh-conn-icon {
width: 38px;
height: 38px;
border-radius: var(--radius-sm);
background: var(--cyan-dim);
border: 1px solid var(--cyan-mid);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--cyan);
}
.ssh-conn-icon svg { width: 18px; height: 18px; }
.ssh-conn-info { flex: 1; min-width: 0; }
.ssh-conn-label { font-size: 14px; font-weight: 600; color: var(--text); word-break: break-word; }
.ssh-conn-actions { display: flex; gap: 6px; flex-shrink: 0; margin-left: auto; }
.ssh-conn-meta {
font-size: 11px;
color: var(--text3);
font-family: 'JetBrains Mono', monospace;
word-break: break-all;
line-height: 1.5;
padding: 6px 8px;
background: var(--base);
border-radius: var(--radius-sm);
border: 1px solid var(--border);
}
.ssh-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 12px; }
/* ── SSH Terminal modal ──────────────────────────── */
.modal-terminal {
max-width: 95vw;
width: 95vw;
height: 90vh;
margin: 0;
display: flex;
flex-direction: column;
}
.terminal-toolbar {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
background: var(--surface);
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.terminal-conn-label {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: var(--cyan);
background: var(--cyan-dim);
border: 1px solid var(--cyan-mid);
border-radius: var(--radius-sm);
padding: 3px 10px;
flex-shrink: 0;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.terminal-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--green);
box-shadow: 0 0 6px var(--green);
flex-shrink: 0;
animation: pulse 2.5s infinite;
}
.terminal-status-dot.disconnected { background: var(--red); box-shadow: 0 0 6px var(--red); animation: none; }
.terminal-spacer { flex: 1; }
.terminal-statusbar {
display: flex;
align-items: center;
gap: 12px;
padding: 6px 16px;
border-top: 1px solid var(--border);
background: var(--surface);
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
flex-shrink: 0;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text4);
}
.terminal-statusbar span { color: var(--text3); }
#terminalContainer {
flex: 1;
background: #0d0d0f;
overflow: hidden;
padding: 8px;
}
/* Override xterm defaults to match our palette */
#terminalContainer .xterm { height: 100%; }
#terminalContainer .xterm-viewport { background: transparent !important; }
#terminalContainer .xterm-screen { background: transparent !important; }
</style>
</head>
<body>
<div class="layout">
<!-- ── Sidebar ──────────────────────────────────── -->
<nav class="sidebar">
<div class="sidebar-logo">
<div class="sidebar-logo-icon"><img src="icons/32.png" alt="Holesail"></div>
<div class="sidebar-logo-text">
<div class="sidebar-logo-name">Holesail</div>
<div class="sidebar-logo-version" id="sidebarVersion">v1.0.0 · hole.sail</div>
</div>
</div>
<div class="sidebar-nav">
<div class="nav-item active" data-page="dashboard">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7" rx="1.5"/>
<rect x="14" y="3" width="7" height="7" rx="1.5"/>
<rect x="3" y="14" width="7" height="7" rx="1.5"/>
<rect x="14" y="14" width="7" height="7" rx="1.5"/>
</svg>
Overview
</div>
<div class="nav-item" data-page="connections">
<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"/>
<circle cx="4" cy="18" r="2"/><circle cx="20" cy="18" r="2"/>
<line x1="6" y1="6" x2="10" y2="11"/><line x1="18" y1="6" x2="14" y2="11"/>
<line x1="6" y1="18" x2="10" y2="13"/><line x1="18" y1="18" x2="14" y2="13"/>
</svg>
Virtual Hosts
<span class="nav-badge" id="connCount">0</span>
</div>
<div class="nav-item" data-page="service-tunnels">
<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>
Service Tunnels
<span class="nav-badge" id="serviceTunnelCount">0</span>
</div>
<div class="nav-item" data-page="ssh">
<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>
SSH
<span class="nav-badge" id="sshCount">0</span>
</div>
<div class="nav-item" data-page="swarms">
<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>
Servers
<span class="nav-badge" id="swarmCount">0</span>
</div>
<div class="nav-item" data-page="tabs">
<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>
Proxy &amp; CA
<span class="nav-badge" id="tabCount"></span>
</div>
<div class="nav-item" data-page="rdp">
<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>
Remote Desktop
<span class="nav-badge" id="rdpCount">0</span>
</div>
<div class="nav-item" data-page="backups">
<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>
Backups
<span class="nav-badge" id="backupCount">0</span>
</div>
<div class="nav-item" data-page="logs">
<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>
Logs
</div>
<div class="nav-item" data-page="settings">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
Settings
</div>
</div>
<div class="sidebar-footer">
<div class="status-pill">
<div class="status-dot" id="sidebarDot"></div>
<span id="sidebarStatus">Connecting…</span>
</div>
</div>
</nav>
<!-- ── Main ─────────────────────────────────────── -->
<main class="main">
<div class="topbar">
<span class="topbar-title" id="topbarTitle">Overview</span>
<div class="topbar-actions" id="topbarActions"></div>
</div>
<div class="content">
<!-- ── Overview page ──────────────────────────── -->
<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>
<!-- 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>
</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>
</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" 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="dashServiceTunnels">0</div>
<div class="stat-label">Service 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="4" width="20" height="16" rx="2"/><polyline points="8,10 12,14 16,10"/></svg>
</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>
<!-- Lists: 2-col top row, 3-col bottom row — each card has fixed height + scroll -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px;">
<!-- Virtual Hosts -->
<div class="card ov-list-card">
<div class="card-header" style="padding-bottom:8px;">
<div style="min-width:0;">
<div class="card-title">Virtual Hosts</div>
<div class="card-subtitle" id="ovVhostSubtitle"></div>
</div>
</div>
<div style="padding:0 12px 8px;">
<input class="input ov-search" id="ovVhostSearch" placeholder="Filter…" autocomplete="off" style="height:28px;font-size:12px;">
</div>
<div class="ov-scroll-area">
<table class="table ov-table">
<thead><tr><th>Hostname</th><th>State</th><th></th></tr></thead>
<tbody id="recentConnections"></tbody>
</table>
<div id="ovVhostSentinel" class="ov-sentinel"></div>
</div>
</div>
<!-- Server Tunnels -->
<div class="card ov-list-card">
<div class="card-header" style="padding-bottom:8px;">
<div style="min-width:0;">
<div class="card-title">Server Tunnels</div>
<div class="card-subtitle" id="ovServersSubtitle"></div>
</div>
</div>
<div style="padding:0 12px 8px;">
<input class="input ov-search" id="ovServersSearch" placeholder="Filter…" autocomplete="off" style="height:28px;font-size:12px;">
</div>
<div class="ov-scroll-area">
<table class="table ov-table">
<thead><tr><th>Port</th><th>State</th><th>hs:// Key</th></tr></thead>
<tbody id="ovServersBody"></tbody>
</table>
<div id="ovServersSentinel" class="ov-sentinel"></div>
</div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:10px;">
<!-- Service Tunnels -->
<div class="card ov-list-card">
<div class="card-header" style="padding-bottom:8px;">
<div style="min-width:0;">
<div class="card-title">Service Tunnels</div>
<div class="card-subtitle" id="ovSvcSubtitle"></div>
</div>
</div>
<div style="padding:0 12px 8px;">
<input class="input ov-search" id="ovSvcSearch" placeholder="Filter…" autocomplete="off" style="height:28px;font-size:12px;">
</div>
<div class="ov-scroll-area">
<table class="table ov-table">
<thead><tr><th>Label</th><th>Port</th><th>State</th></tr></thead>
<tbody id="ovSvcBody"></tbody>
</table>
<div id="ovSvcSentinel" class="ov-sentinel"></div>
</div>
</div>
<!-- SSH Connections -->
<div class="card ov-list-card">
<div class="card-header" style="padding-bottom:8px;">
<div style="min-width:0;">
<div class="card-title">SSH Connections</div>
<div class="card-subtitle" id="ovSshSubtitle"></div>
</div>
</div>
<div style="padding:0 12px 8px;">
<input class="input ov-search" id="ovSshSearch" placeholder="Filter…" autocomplete="off" style="height:28px;font-size:12px;">
</div>
<div class="ov-scroll-area">
<table class="table ov-table">
<thead><tr><th>Label</th><th>User</th></tr></thead>
<tbody id="ovSshBody"></tbody>
</table>
<div id="ovSshSentinel" class="ov-sentinel"></div>
</div>
</div>
<!-- Remote Desktops -->
<div class="card ov-list-card">
<div class="card-header" style="padding-bottom:8px;">
<div style="min-width:0;">
<div class="card-title">Remote Desktops</div>
<div class="card-subtitle" id="ovRdpSubtitle"></div>
</div>
</div>
<div style="padding:0 12px 8px;">
<input class="input ov-search" id="ovRdpSearch" placeholder="Filter…" autocomplete="off" style="height:28px;font-size:12px;">
</div>
<div class="ov-scroll-area">
<table class="table ov-table">
<thead><tr><th>Label</th><th>Type</th></tr></thead>
<tbody id="ovRdpBody"></tbody>
</table>
<div id="ovRdpSentinel" class="ov-sentinel"></div>
</div>
</div>
</div>
<!-- Quick actions -->
<div class="card" style="flex-shrink:0;">
<div class="card-header"><div class="card-title">Quick Actions</div></div>
<div class="card-body" style="display:flex;gap:8px;flex-wrap:wrap;padding-top:10px;padding-bottom:10px;">
<button class="btn btn-secondary btn-sm" 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" 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" 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" 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" id="qaAddRdp">
<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 Remote Desktop
</button>
<button class="btn btn-secondary btn-sm" id="qaInstallCA">
<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>
Install Root CA
</button>
</div>
</div>
</div>
<!-- ── Virtual Hosts page ──────────────────────── -->
<div class="page" id="page-connections">
<div class="page-header">
<h2>Virtual Hosts</h2>
<button class="btn btn-primary" id="addVhostBtn">
<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 Host
</button>
</div>
<div class="card">
<div class="card-body flush">
<table class="table">
<thead>
<tr>
<th>Hostname</th>
<th>hs:// URL</th>
<th>Backend</th>
<th>State</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="connectionsTable">
<tr><td colspan="5" class="empty">No virtual hosts. Click "Add Host" to get started.</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ── Servers page ────────────────────────────── -->
<div class="page" id="page-swarms">
<div class="page-header">
<h2>Server Tunnels</h2>
<button class="btn btn-primary" id="startServerBtn">
<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
</button>
</div>
<div class="card">
<div class="card-body flush">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Port</th>
<th>hs:// URL</th>
<th>Secure</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="swarmsTable">
<tr><td colspan="5" class="empty">No server tunnels. Click "New Server" to start one.</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ── Proxy & CA page ────────────────────────── -->
<div class="page" id="page-tabs">
<div class="page-header"><h2>Proxy &amp; CA</h2></div>
<div class="info-grid" id="proxyInfoGrid" style="margin-bottom:16px;">
<div class="info-item">
<div class="info-item-label">Proxy Port</div>
<div class="info-item-value" id="proxyInfoPort"></div>
</div>
<div class="info-item">
<div class="info-item-label">CA Status</div>
<div class="info-item-value" id="proxyInfoCA"></div>
</div>
</div>
<div class="card">
<div class="card-header">
<div>
<div class="card-title">Root Certificate Authority</div>
<div class="card-subtitle">Required for HTTPS on *.hole.sail domains</div>
</div>
</div>
<div class="card-body" style="display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;">
<p style="font-size:13px;color:var(--text2);max-width:380px;line-height:1.6;">
Install the Holesail root CA into your system keychain so Chrome trusts HTTPS certificates for <code style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--cyan);">*.hole.sail</code> virtual hosts.
</p>
<button class="btn btn-primary" id="installCaBtn">
<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>
Install Root CA
</button>
</div>
</div>
<!-- Certificate Validator — shown only when CA is installed -->
<div class="card" id="certValidatorCard" style="display:none;">
<div class="card-header">
<div>
<div class="card-title">Certificate Validator</div>
<div class="card-subtitle">Tests HTTPS reachability and TLS trust for each virtual host</div>
</div>
<button class="btn btn-primary btn-sm" id="runAllValidationsBtn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5,3 19,12 5,21"/></svg>
Test All
</button>
</div>
<div class="card-body flush">
<table class="table" id="certValidatorTable">
<thead>
<tr>
<th>Hostname</th>
<th>TLS Trust</th>
<th>HTTP Status</th>
<th>Response Time</th>
<th>Action</th>
</tr>
</thead>
<tbody id="certValidatorBody">
<tr><td colspan="5" class="empty">No virtual hosts to test</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ── Remote Desktop page ───────────────────── -->
<div class="page" id="page-rdp">
<div class="page-header">
<h2>Remote Desktop</h2>
<button class="btn btn-primary" id="addRdpBtn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Add Connection
</button>
</div>
<div class="rdp-grid" id="rdpGrid">
<div class="empty-state" style="grid-column:1/-1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><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="empty-state-title">No remote desktop connections</div>
<div class="empty-state-desc">Add a VNC or RDP connection. You'll need an hs:// key for the remote peer.</div>
</div>
</div>
</div>
<!-- ── Backups page ───────────────────────────── -->
<div class="page" id="page-backups">
<div class="page-header">
<h2>Backups</h2>
<button class="btn btn-primary" id="btnTakeBackup">
<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>
Take Backup
</button>
</div>
<div class="card">
<div class="card-body flush">
<table class="table">
<thead>
<tr>
<th>Filename</th>
<th>Created</th>
<th>Size</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="backupsTable">
<tr><td colspan="4"><div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="width:32px;height:32px;color:var(--text4)"><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>
<div class="empty-state-title">No backups yet</div>
<div class="empty-state-desc">Click "Take Backup" to create your first backup</div>
</div></td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ── Settings page ──────────────────────────── -->
<div class="page" id="page-settings">
<div class="page-header"><h2>Settings</h2></div>
<div class="card">
<div class="card-body">
<div class="section-heading">Notifications</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Host Disconnect Alert</div>
<div class="setting-desc">Show a notification when the native host disconnects</div>
</div>
<div class="setting-control">
<div class="toggle" id="toggleNotify" data-setting="notifyOnDisconnect"></div>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Debug Mode</div>
<div class="setting-desc">Enable verbose logging for troubleshooting</div>
</div>
<div class="setting-control">
<div class="toggle" id="toggleDebug" data-setting="debug"></div>
</div>
</div>
<div class="section-heading">Proxy &amp; API</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Proxy Port</div>
<div class="setting-desc">HTTPS proxy port for virtual hosts (default 8443)</div>
</div>
<div class="setting-control">
<input type="number" id="proxyPort" min="1" max="65535" class="input" style="width:90px;" placeholder="8443">
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Ready Timeout (ms)</div>
<div class="setting-desc">Timeout for Holesail tunnel ready state</div>
</div>
<div class="setting-control">
<input type="number" id="readyTimeoutMs" class="input" style="width:90px;" placeholder="0">
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Disable on File URLs</div>
<div class="setting-desc">Do not inject extension on file:// pages</div>
</div>
<div class="setting-control">
<div class="toggle" id="toggleDisableFileUrls"></div>
</div>
</div>
<div class="section-heading">Backups</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Backup Retention</div>
<div class="setting-desc">Number of backups to keep (oldest are pruned automatically)</div>
</div>
<div class="setting-control">
<input type="number" id="backupRetention" min="1" max="100" class="input" style="width:90px;" placeholder="5">
</div>
</div>
<div style="margin-top:20px;display:flex;gap:8px;">
<button class="btn btn-primary" id="btnSaveSettings">Save Settings</button>
<button class="btn btn-secondary" id="btnResetSettings">Reset to Defaults</button>
</div>
</div>
</div>
</div>
<!-- ── Service Tunnels page ──────────────────────── -->
<div class="page" id="page-service-tunnels">
<div class="page-header">
<h2>Service Tunnels</h2>
<button class="btn btn-primary" id="addServiceTunnelBtn">
<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 Tunnel
</button>
</div>
<div class="card">
<div class="card-body flush">
<table class="table">
<thead>
<tr>
<th>Label</th>
<th>hs:// Key</th>
<th>Local Port</th>
<th>State</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="serviceTunnelsTable">
<tr><td colspan="5" class="empty">No service tunnels. Click "Add Tunnel" to connect a remote service.</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ── SSH page ────────────────────────────────── -->
<div class="page" id="page-ssh">
<div class="page-header">
<h2>SSH Connections</h2>
<button class="btn btn-primary" id="addSshBtn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Add Connection
</button>
</div>
<div class="ssh-grid" id="sshGrid">
<div class="empty-state" style="grid-column:1/-1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="4" width="20" height="16" rx="2"/><polyline points="8,10 12,14 16,10"/></svg>
<div class="empty-state-title">No SSH connections</div>
<div class="empty-state-desc">Add a connection to get started. You'll need an hs:// key for the remote peer.</div>
</div>
</div>
</div>
<!-- ── Logs page ───────────────────────────────── -->
<div class="page" id="page-logs">
<div class="page-header"><h2>System Logs</h2></div>
<div class="logs-toolbar">
<input type="text" class="input logs-filter" id="logsFilter" placeholder="Filter logs…">
<button class="btn btn-secondary" id="btnAutoScroll">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="7,13 12,18 17,13"/><polyline points="7,6 12,11 17,6"/></svg>
Auto-scroll: ON
</button>
<button class="btn btn-ghost" id="btnClearLogs">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3,6 5,6 21,6"/><path d="M19,6l-1,14a2,2,0,0,1-2,2H8a2,2,0,0,1-2-2L5,6"/><path d="M10,11v6"/><path d="M14,11v6"/></svg>
Clear
</button>
</div>
<div class="logs-container" id="logsContainer">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polyline points="22,12 18,12 15,21 9,3 6,12 2,12"/></svg>
<div class="empty-state-title">No logs yet</div>
<div class="empty-state-desc">Logs will appear here as the extension runs</div>
</div>
</div>
</div>
</div><!-- /content -->
</main>
</div><!-- /layout -->
<!-- ── Toast ─────────────────────────────────────────── -->
<div class="toast" id="toast"></div>
<!-- ── Modal: Add Virtual Host ───────────────────────── -->
<div class="modal-backdrop" id="modal-addVhost">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-addVhost-title">
<div class="modal-header">
<span class="modal-title" id="modal-addVhost-title">Add Virtual Host</span>
<button class="btn-icon" data-close-modal="modal-addVhost" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body">
<p class="modal-desc">Assign a hostname to an hs:// tunnel URL. The host will be accessible at <code style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--cyan);">https://&lt;hostname&gt;</code> via the proxy.</p>
<div class="form-group">
<label class="form-label" for="addVhostHostname">Hostname <span class="form-hint">(e.g. myapp.hole.sail or i.love.hole.sail — no real TLDs)</span></label>
<input type="text" id="addVhostHostname" class="input mono" placeholder="myapp.hole.sail" autocomplete="off" spellcheck="false">
<div style="font-size:11px;color:var(--text4);margin-top:4px;line-height:1.5;">Use any depth: <code style="font-size:10px;">app.hole.sail</code>, <code style="font-size:10px;">i.love.hole.sail</code>, <code style="font-size:10px;">api.v2.my.internal</code> — private TLDs only, real TLDs like <code style="font-size:10px;">.com</code> are not allowed.</div>
</div>
<div class="form-group">
<label class="form-label" for="addVhostHsUrl">hs:// URL</label>
<input type="text" id="addVhostHsUrl" class="input mono" placeholder="hs://s000..." autocomplete="off" spellcheck="false">
</div>
<div class="modal-error" id="addVhostError"></div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-close-modal="modal-addVhost">Cancel</button>
<button class="btn btn-primary" id="addVhostSubmit">Add Host</button>
</div>
</div>
</div>
<!-- ── Modal: Start Server ───────────────────────────── -->
<div class="modal-backdrop" id="modal-startServer">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-startServer-title">
<div class="modal-header">
<span class="modal-title" id="modal-startServer-title">Start Server Tunnel</span>
<button class="btn-icon" data-close-modal="modal-startServer" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body">
<p class="modal-desc">Expose a local port as an hs:// tunnel. The generated URL can be shared with others.</p>
<div class="row">
<div class="form-group" style="max-width:120px;">
<label class="form-label" for="startServerPort">Port</label>
<input type="number" id="startServerPort" class="input" value="3000" min="1" max="65535">
</div>
<div class="form-group">
<label class="form-label" for="startServerHost">Host</label>
<input type="text" id="startServerHost" class="input mono" value="127.0.0.1">
</div>
</div>
<div class="form-group">
<label class="form-label">Protocol</label>
<div style="display:flex;gap:8px;">
<label class="radio-row">
<input type="radio" name="startServerProtocol" id="startServerProtocolTcp" value="tcp" checked>
<span>TCP</span>
</label>
<label class="radio-row">
<input type="radio" name="startServerProtocol" id="startServerProtocolUdp" value="udp">
<span>UDP</span>
</label>
</div>
</div>
<label class="checkbox-row">
<input type="checkbox" id="startServerSecure" checked>
<label for="startServerSecure">Secure mode (hs://s000…)</label>
</label>
<input type="hidden" id="serverEditId">
<div class="modal-error" id="startServerError"></div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-close-modal="modal-startServer">Cancel</button>
<button class="btn btn-primary" id="startServerSubmit">Start Server</button>
</div>
</div>
</div>
<!-- ── Modal: Install CA ─────────────────────────────── -->
<div class="modal-backdrop" id="modal-installCA">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-installCA-title">
<div class="modal-header">
<span class="modal-title" id="modal-installCA-title">Install Root CA</span>
<button class="btn-icon" data-close-modal="modal-installCA" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body">
<p class="modal-desc">
This installs the Holesail root certificate authority into your system keychain, so Chrome trusts HTTPS for all <code style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--cyan);">*.hole.sail</code> virtual host domains.
</p>
<p class="modal-desc" style="color:var(--fg-muted);font-size:11px;">
After installing, fully quit and reopen Chrome (Cmd+Q) for the trust to take effect.
</p>
<div class="modal-error" id="installCaError"></div>
<div id="installCaSuccess" style="font-size:12px;color:var(--green);display:none;"></div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-close-modal="modal-installCA">Cancel</button>
<button class="btn btn-primary" id="installCaSubmit">Install CA</button>
</div>
</div>
</div>
<!-- ── Modal: Remove Virtual Host confirm ────────────── -->
<div class="modal-backdrop" id="modal-removeVhost">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-removeVhost-title">
<div class="modal-header">
<span class="modal-title" id="modal-removeVhost-title">Remove Virtual Host</span>
<button class="btn-icon" data-close-modal="modal-removeVhost" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body">
<p class="modal-desc">Remove <strong id="removeVhostName" style="color:var(--cyan);font-family:'JetBrains Mono',monospace;font-size:12px;"></strong>? The tunnel will be closed and the hostname will no longer be accessible.</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-close-modal="modal-removeVhost">Cancel</button>
<button class="btn btn-danger" id="removeVhostConfirm">Remove</button>
</div>
</div>
</div>
<!-- ── Modal: Stop Server confirm ────────────────────── -->
<div class="modal-backdrop" id="modal-stopServer">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-stopServer-title">
<div class="modal-header">
<span class="modal-title" id="modal-stopServer-title">Stop Server</span>
<button class="btn-icon" data-close-modal="modal-stopServer" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body">
<p class="modal-desc">Stop server <strong id="stopServerName" style="color:var(--cyan);font-family:'JetBrains Mono',monospace;font-size:12px;"></strong>? The tunnel will be closed and the hs:// URL will no longer be active.</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-close-modal="modal-stopServer">Cancel</button>
<button class="btn btn-danger" id="stopServerConfirm">Stop Server</button>
</div>
</div>
</div>
<!-- ── Modal: Add Service Tunnel ─────────────────────── -->
<div class="modal-backdrop" id="modal-addServiceTunnel">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-addServiceTunnel-title">
<div class="modal-header">
<span class="modal-title" id="modal-addServiceTunnel-title">Add Service Tunnel</span>
<button class="btn-icon" data-close-modal="modal-addServiceTunnel" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body">
<p class="modal-desc">Connect to a remote service (game server, database, TCP/UDP service, etc.) via a Holesail tunnel. The service will be available on a local port you choose.</p>
<div class="form-group">
<label class="form-label" for="serviceTunnelLabel">Label</label>
<input type="text" id="serviceTunnelLabel" class="input" placeholder="My Game Server" autocomplete="off">
</div>
<div class="form-group">
<label class="form-label" for="serviceTunnelHsUrl">Holesail Key (hs://…)</label>
<input type="text" id="serviceTunnelHsUrl" class="input mono" placeholder="hs://s000…" autocomplete="off" spellcheck="false">
</div>
<div class="form-group">
<label class="form-label" for="serviceTunnelLocalPort">Local Port</label>
<input type="number" id="serviceTunnelLocalPort" class="input" placeholder="25565" min="1" max="65535">
</div>
<input type="hidden" id="serviceTunnelEditId">
<div class="modal-error" id="serviceTunnelError"></div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-close-modal="modal-addServiceTunnel">Cancel</button>
<button class="btn btn-primary" id="serviceTunnelSubmit">Connect</button>
</div>
</div>
</div>
<!-- ── Modal: Remove Service Tunnel confirm ───────────── -->
<div class="modal-backdrop" id="modal-removeServiceTunnel">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-removeServiceTunnel-title">
<div class="modal-header">
<span class="modal-title" id="modal-removeServiceTunnel-title">Remove Service Tunnel</span>
<button class="btn-icon" data-close-modal="modal-removeServiceTunnel" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body">
<p class="modal-desc">Remove <strong id="removeServiceTunnelName" style="color:var(--cyan);font-family:'JetBrains Mono',monospace;font-size:12px;"></strong>? The tunnel will be disconnected and the local port will be released.</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-close-modal="modal-removeServiceTunnel">Cancel</button>
<button class="btn btn-danger" id="removeServiceTunnelConfirm">Remove</button>
</div>
</div>
</div>
<!-- ── Modal: Add / Edit SSH Connection ──────────────── -->
<div class="modal-backdrop" id="modal-addSsh">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-addSsh-title">
<div class="modal-header">
<span class="modal-title" id="modal-addSsh-title">Add SSH Connection</span>
<button class="btn-icon" data-close-modal="modal-addSsh" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body">
<p class="modal-desc">Save an SSH connection by providing its Holesail key. The key is used to establish a secure tunnel to the remote peer before connecting via SSH.</p>
<div class="form-group">
<label class="form-label" for="sshConnLabel">Label</label>
<input type="text" id="sshConnLabel" class="input" placeholder="My Server" autocomplete="off">
</div>
<div class="form-group">
<label class="form-label" for="sshConnHsUrl">Holesail Key (hs://…)</label>
<input type="text" id="sshConnHsUrl" class="input mono" placeholder="hs://s000…" autocomplete="off" spellcheck="false">
</div>
<div class="form-group">
<label class="form-label" for="sshConnUsername">Username</label>
<input type="text" id="sshConnUsername" class="input mono" placeholder="root" autocomplete="off">
</div>
<div class="form-group">
<label class="form-label" for="sshConnPassword">Password <span style="color:var(--text4);font-weight:400;">(optional — saved, or leave blank to type in terminal)</span></label>
<input type="password" id="sshConnPassword" class="input" placeholder="Leave blank to enter manually" autocomplete="new-password">
</div>
<input type="hidden" id="sshConnEditId">
<div class="modal-error" id="sshConnError"></div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-close-modal="modal-addSsh">Cancel</button>
<button class="btn btn-primary" id="sshConnSubmit">Save Connection</button>
</div>
</div>
</div>
<!-- ── Modal: Remove SSH Connection confirm ───────────── -->
<div class="modal-backdrop" id="modal-removeSsh">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-removeSsh-title">
<div class="modal-header">
<span class="modal-title" id="modal-removeSsh-title">Remove SSH Connection</span>
<button class="btn-icon" data-close-modal="modal-removeSsh" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body">
<p class="modal-desc">Remove <strong id="removeSshName" style="color:var(--cyan);font-family:'JetBrains Mono',monospace;font-size:12px;"></strong>? This only removes the saved connection — no tunnels are affected.</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-close-modal="modal-removeSsh">Cancel</button>
<button class="btn btn-danger" id="removeSshConfirm">Remove</button>
</div>
</div>
</div>
<!-- ── Modal: SSH Terminal ────────────────────────────── -->
<div class="modal-backdrop" id="modal-sshTerminal">
<div class="modal modal-terminal" role="dialog" aria-modal="true" aria-labelledby="modal-sshTerminal-title">
<div class="terminal-toolbar">
<div class="terminal-status-dot" id="termStatusDot"></div>
<span class="terminal-conn-label" id="termConnLabel"></span>
<span style="font-size:12px;color:var(--text3);font-family:'JetBrains Mono',monospace;" id="termUserHost"></span>
<div class="terminal-spacer"></div>
<button class="btn btn-ghost" id="termCopyBtn" title="Copy selection" style="padding:5px 10px;font-size:12px;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
Copy
</button>
<button class="btn btn-ghost" id="termFullscreenBtn" title="Toggle fullscreen" style="padding:5px 10px;font-size:12px;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><polyline points="15,3 21,3 21,9"/><polyline points="9,21 3,21 3,15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
</button>
<button class="btn btn-danger" id="termDisconnectBtn" style="padding:5px 14px;font-size:12px;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
Disconnect
</button>
</div>
<div id="terminalContainer"></div>
<div class="terminal-statusbar">
<span id="termSizeDisplay"></span>
<span style="color:var(--border2);">|</span>
<span id="termStateDisplay" style="color:var(--green);">Connected</span>
<div style="flex:1"></div>
<span style="color:var(--text4);">xterm-256color</span>
</div>
</div>
</div>
<!-- ── Modal: Add/Edit Remote Desktop Connection ──────── -->
<div class="modal-backdrop" id="modal-addRdp">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-addRdp-title">
<div class="modal-header">
<span class="modal-title" id="modal-addRdp-title">Add Remote Desktop Connection</span>
<button class="btn-icon" data-close-modal="modal-addRdp" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label" for="rdpConnLabel">Label (optional)</label>
<input type="text" id="rdpConnLabel" class="input" placeholder="My Desktop" autocomplete="off">
</div>
<div class="form-group">
<label class="form-label" for="rdpConnHsUrl">hs:// Key</label>
<input type="text" id="rdpConnHsUrl" class="input mono" placeholder="hs://s000..." autocomplete="off" spellcheck="false">
</div>
<div class="form-group">
<label class="form-label">Protocol</label>
<div style="display:flex;gap:8px;">
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:13px;">
<input type="radio" name="rdpProtocol" id="rdpProtocolVnc" value="vnc" checked> VNC
</label>
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:13px;">
<input type="radio" name="rdpProtocol" id="rdpProtocolRdp" value="rdp"> RDP
</label>
</div>
</div>
<div class="row">
<div class="form-group" style="max-width:110px;">
<label class="form-label" for="rdpConnPort">Port</label>
<input type="number" id="rdpConnPort" class="input" value="5900" min="1" max="65535">
</div>
<div class="form-group" style="max-width:110px;">
<label class="form-label" for="rdpConnWidth">Width</label>
<input type="number" id="rdpConnWidth" class="input" value="1280" min="320" max="4096">
</div>
<div class="form-group" style="max-width:110px;">
<label class="form-label" for="rdpConnHeight">Height</label>
<input type="number" id="rdpConnHeight" class="input" value="720" min="240" max="4096">
</div>
</div>
<div id="rdpUsernameGroup" class="form-group" style="display:none;">
<label class="form-label" for="rdpConnUsername">Username</label>
<input type="text" id="rdpConnUsername" class="input" placeholder="Administrator" autocomplete="off">
</div>
<div class="form-group">
<label class="form-label" for="rdpConnPassword">Password (optional)</label>
<input type="password" id="rdpConnPassword" class="input" placeholder="••••••••" autocomplete="new-password">
</div>
<input type="hidden" id="rdpConnEditId">
<div class="modal-error" id="rdpConnError"></div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-close-modal="modal-addRdp">Cancel</button>
<button class="btn btn-primary" id="rdpConnSubmit">Save Connection</button>
</div>
</div>
</div>
<!-- ── Modal: Remove Remote Desktop Connection ─────────── -->
<div class="modal-backdrop" id="modal-removeRdp">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-removeRdp-title">
<div class="modal-header">
<span class="modal-title" id="modal-removeRdp-title">Remove Connection</span>
<button class="btn-icon" data-close-modal="modal-removeRdp" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body">
<p class="modal-desc">Remove <strong id="removeRdpName" style="color:var(--cyan);font-family:'JetBrains Mono',monospace;font-size:12px;"></strong>? This only removes the saved connection.</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-close-modal="modal-removeRdp">Cancel</button>
<button class="btn btn-danger" id="removeRdpConfirm">Remove</button>
</div>
</div>
</div>
<!-- ── Modal: Remote Desktop Viewer ───────────────────── -->
<div class="modal-backdrop" id="modal-rdpViewer">
<div class="modal modal-rdp-viewer" role="dialog" aria-modal="true" aria-labelledby="modal-rdpViewer-title">
<div class="rdp-viewer-toolbar">
<div class="terminal-status-dot" id="rdpStatusDot"></div>
<span class="terminal-conn-label" id="rdpViewerLabel"></span>
<span class="badge" id="rdpProtocolBadge" style="font-size:10px;padding:2px 7px;"></span>
<div class="terminal-spacer"></div>
<button class="btn btn-ghost" id="rdpFullscreenBtn" title="Toggle fullscreen" style="padding:5px 10px;font-size:12px;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><polyline points="15,3 21,3 21,9"/><polyline points="9,21 3,21 3,15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
</button>
<button class="btn btn-danger" id="rdpDisconnectBtn" style="padding:5px 14px;font-size:12px;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
Disconnect
</button>
</div>
<div id="rdpViewerContainer"></div>
<div class="rdp-viewer-statusbar">
<span id="rdpResDisplay"></span>
<span style="color:var(--border2);">|</span>
<span id="rdpStateDisplay" style="color:var(--green);">Connecting…</span>
<div style="flex:1"></div>
<span style="color:var(--text4);" id="rdpViewerInfo"></span>
</div>
</div>
</div>
<!-- ── Modal: Restore Backup confirm ──────────────────── -->
<div class="modal-backdrop" id="modal-restoreBackup">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-restoreBackup-title">
<div class="modal-header">
<span class="modal-title" id="modal-restoreBackup-title">Restore Backup</span>
<button class="btn-icon" data-close-modal="modal-restoreBackup" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body">
<p class="modal-desc">Restore from <strong id="restoreBackupName" style="color:var(--cyan);font-family:'JetBrains Mono',monospace;font-size:12px;word-break:break-all;"></strong>?</p>
<p class="modal-desc" style="margin-top:8px;color:var(--amber);">This will overwrite your current state and certificates with the backup. All running tunnels will need to be restarted. If the CA certificate changes, you may need to reinstall it in your browser.</p>
<div class="modal-error" id="restoreBackupError"></div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-close-modal="modal-restoreBackup">Cancel</button>
<button class="btn btn-danger" id="restoreBackupConfirm">Restore</button>
</div>
</div>
</div>
<!-- ── Modal: Delete Backup confirm ───────────────────── -->
<div class="modal-backdrop" id="modal-deleteBackup">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-deleteBackup-title">
<div class="modal-header">
<span class="modal-title" id="modal-deleteBackup-title">Delete Backup</span>
<button class="btn-icon" data-close-modal="modal-deleteBackup" aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body">
<p class="modal-desc">Permanently delete <strong id="deleteBackupName" style="color:var(--cyan);font-family:'JetBrains Mono',monospace;font-size:12px;word-break:break-all;"></strong>? This cannot be undone.</p>
<div class="modal-error" id="deleteBackupError"></div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-close-modal="modal-deleteBackup">Cancel</button>
<button class="btn btn-danger" id="deleteBackupConfirm">Delete</button>
</div>
</div>
</div>
<script src="dashboard.js"></script>
</body>
</html>