Files
holesail-browser/extension/dashboard.html
T
Raven Scott d58a0b6e2d
CI / Build & Test (push) Has been cancelled
first commit
2026-02-27 18:13:59 -05:00

1840 lines
79 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;
background: var(--cyan-dim);
border: 1px solid var(--cyan-mid);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.sidebar-logo-icon svg { width: 18px; height: 18px; color: var(--cyan); }
.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: 900px) { .stats-grid { grid-template-columns: repeat(2, 1fr); } }
.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); }
/* ── 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; }
/* ── Refresh FAB ───────────────────────────────── */
.fab {
position: fixed;
bottom: 24px;
right: 24px;
width: 44px;
height: 44px;
border-radius: 50%;
background: var(--cyan);
color: #09090b;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 16px rgba(34,211,238,.35);
transition: all 0.2s;
z-index: 50;
}
.fab:hover { transform: rotate(180deg); background: #06b6d4; }
.fab svg { width: 20px; height: 20px; }
/* ── 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;
align-items: center;
gap: 12px;
transition: border-color var(--transition), background var(--transition);
cursor: default;
}
.rdp-conn-card:hover { border-color: var(--border2); background: var(--elevated); }
.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); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rdp-conn-meta { font-size: 12px; color: var(--text3); margin-top: 2px; font-family: 'JetBrains Mono', monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rdp-conn-actions { display: flex; gap: 6px; flex-shrink: 0; }
.rdp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 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;
align-items: center;
gap: 14px;
transition: border-color var(--transition), background var(--transition);
cursor: default;
}
.ssh-conn-card:hover { border-color: var(--border2); background: var(--elevated); }
.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); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ssh-conn-meta { font-size: 12px; color: var(--text3); margin-top: 2px; font-family: 'JetBrains Mono', monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ssh-conn-actions { display: flex; gap: 6px; flex-shrink: 0; }
.ssh-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 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">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<path d="M3 17l4-8 4 4 4-6 4 10"/>
<path d="M3 21h18"/>
</svg>
</div>
<div class="sidebar-logo-text">
<div class="sidebar-logo-name">Holesail</div>
<div class="sidebar-logo-version">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>
<div class="stats-grid">
<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 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>
<div class="stat-value" id="dashTabs"></div>
<div class="stat-label">Proxy Port</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"></div>
<div class="stat-label">CA Status</div>
</div>
</div>
<!-- Virtual hosts preview -->
<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>
<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>
</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 class="card" style="margin-top:16px;">
<div class="card-header">
<div>
<div class="card-title">Backup &amp; Restore</div>
<div class="card-subtitle">Export all connections to a JSON file, or import from a previous backup</div>
</div>
</div>
<div class="card-body" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<button class="btn btn-secondary" id="btnExportConnections">
<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="7,10 12,15 17,10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Export Connections
</button>
<button class="btn btn-secondary" id="btnImportConnections">
<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>
Import Connections
</button>
<input type="file" id="importFileInput" accept=".json" style="display:none;">
<span id="importStatus" style="font-size:12px;color:var(--text3);"></span>
</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 -->
<!-- ── FAB ──────────────────────────────────────────── -->
<button class="fab" id="refreshBtn" title="Refresh">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<polyline points="23,4 23,10 17,10"/>
<polyline points="1,20 1,14 7,14"/>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
</svg>
</button>
<!-- ── 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">(must end with .hole.sail)</span></label>
<input type="text" id="addVhostHostname" class="input mono" placeholder="myapp.hole.sail" autocomplete="off" spellcheck="false">
</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(--amber);">
You will be prompted for your administrator password. This is required to add the certificate to the system trust store.
</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>
<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, not saved)</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>