Files
holesail-browser/extension/dashboard/dashboard.html
T
Raven Scott bd48847bf0
CI / Build & Test (push) Successful in 4m25s
feat(dashboard): debounced filter for linked devices table on Sync page
Add search input (name, ID, type) with 250ms debounce; mirror in extension
and renderer dashboards. Update parity docs note in Browser repo only.
2026-03-27 01:42:50 -04:00

1524 lines
85 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="manifest" href="manifest.webmanifest">
<link rel="stylesheet" href="../vendor/xterm.css">
<link rel="stylesheet" href="dashboard.css">
<script src="../vendor/xterm.js"></script>
<script src="../vendor/xterm-addon-fit.js"></script>
<script type="module" src="../vendor/novnc.js"></script>
</head>
<body>
<!-- Boot splash: removed by init.js after first state refresh (sync-aware messaging). -->
<div id="bootSplash" class="boot-splash" role="status" aria-live="polite" aria-busy="true">
<div class="boot-splash__glow" aria-hidden="true"></div>
<div class="boot-splash__inner">
<div class="boot-splash__mark">
<img src="../icons/32.png" width="40" height="40" alt="">
</div>
<h1 class="boot-splash__title">Holesail</h1>
<p class="boot-splash__line" id="bootSplashLine">Please wait</p>
<p class="boot-splash__sub" id="bootSplashSub">Loading your dashboard…</p>
<div class="boot-splash__track" aria-hidden="true">
<div class="boot-splash__bar"></div>
</div>
</div>
</div>
<div class="layout">
<!-- ── Sidebar ──────────────────────────────────── -->
<nav class="sidebar" aria-label="Main navigation">
<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="sync">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
<path d="M3 3v5h5"/>
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/>
<path d="M16 21h5v-5"/>
</svg>
Sync
</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 style="margin-bottom:16px;flex-shrink:0;display:flex;align-items:center;justify-content:flex-end;">
<div id="overviewLastUpdated" style="font-size:11px;color:var(--text4);"></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;flex-shrink:0;">
<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 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:180px;">
<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="16,3 21,3 21,8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21,16 21,21 16,21"/><line x1="15" y1="15" x2="21" y2="21"/></svg>
<span style="font-size:12.5px;font-weight:500;color:var(--text2);">Traffic</span>
<span id="ovTrafficLabel" style="font-size:11.5px;color:var(--text3);"></span>
</div>
</div>
<!-- Onboarding: shown when native host is not connected -->
<div id="onboardingNativeHost" class="card" style="display:none;margin-bottom:14px;flex-shrink:0;border-color:var(--amber-mid);">
<div class="card-body">
<div class="section-heading" style="color:var(--amber);">Native host not found</div>
<p style="font-size:13px;color:var(--text2);margin-bottom:12px;">The extension needs the native host to be installed. Run the command below in a terminal for your operating system, then click <strong>Check again</strong>.</p>
<div style="margin-bottom:8px;font-size:12px;color:var(--text3);" id="onboardingPlatformLabel"></div>
<div style="display:flex;align-items:flex-start;gap:8px;flex-wrap:wrap;">
<pre id="onboardingCommand" style="flex:1;min-width:0;margin:0;padding:10px 12px;background:var(--elevated);border:1px solid var(--border);border-radius:var(--radius-sm);font-size:12px;font-family:'JetBrains Mono',monospace;overflow-x:auto;white-space:pre-wrap;word-break:break-all;"></pre>
<button type="button" class="btn btn-secondary" id="onboardingCopyBtn" title="Copy command">
<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>
</div>
<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap;">
<button type="button" class="btn btn-primary" id="onboardingCheckAgainBtn">Check again</button>
<div style="font-size:12px;color:var(--text4);align-self:center;">Or switch:</div>
<button type="button" class="btn btn-ghost btn-sm" id="onboardingSwitchMacLinux" data-platform="macos">macOS / Linux</button>
<button type="button" class="btn btn-ghost btn-sm" id="onboardingSwitchWin" data-platform="win32">Windows</button>
</div>
</div>
</div>
<!-- Stat cards — 6 across -->
<div class="stats-grid" style="grid-template-columns:repeat(6,1fr);margin-bottom:14px;flex-shrink:0;">
<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: flex column that fills all remaining space between stat cards
and Quick Actions. Each grid row gets equal share via flex:1. -->
<div id="ovListsWrapper" style="flex:1;min-height:0;display:flex;flex-direction:column;gap:10px;margin-bottom:10px;">
<!-- Row 1: Virtual Hosts + Server Tunnels -->
<div style="flex:1;min-height:0;display:grid;grid-template-columns:1fr 1fr;gap: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;flex-shrink:0;">
<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;flex-shrink:0;">
<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>
<!-- Row 2: Service Tunnels + SSH + Remote Desktops -->
<div style="flex:1;min-height:0;display:grid;grid-template-columns:1fr 1fr 1fr;gap: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;flex-shrink:0;">
<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;flex-shrink:0;">
<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;flex-shrink:0;">
<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>
</div><!-- /ovListsWrapper -->
<!-- Quick Actions — sticky at the bottom, never pushed off screen -->
<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>
<!-- Holesail Lookup -->
<div class="card" style="flex-shrink:0;">
<div class="card-header"><div class="card-title">Holesail Lookup</div><div class="card-subtitle">Resolve an hs:// key to see host, port, protocol and privacy details</div></div>
<div class="card-body" style="display:flex;gap:8px;align-items:center;padding-top:10px;padding-bottom:10px;flex-wrap:wrap;">
<input type="text" id="peerLookupInput" class="input mono" placeholder="hs://s000…" autocomplete="off" spellcheck="false" style="flex:1;min-width:220px;">
<button class="btn btn-primary btn-sm" id="btnPeerLookup">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
Lookup
</button>
</div>
</div>
</div>
<!-- ── Virtual Hosts page ──────────────────────── -->
<div class="page" id="page-connections">
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;">
<div id="vhostBulkBar" style="display:none;align-items:center;gap:8px;">
<span id="vhostBulkCount" style="font-size:12.5px;color:var(--text2);"></span>
<button class="btn btn-danger btn-sm" id="btnVhostBulkRemove">Remove Selected</button>
</div>
<div style="flex:1;"></div>
<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 style="width:32px;"><input type="checkbox" id="vhostSelectAll" title="Select all"></th>
<th>Hostname</th>
<th>hs:// URL</th>
<th>Backend</th>
<th>State</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="connectionsTable">
<tr><td colspan="6" 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 style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;">
<div id="serverBulkBar" style="display:none;align-items:center;gap:8px;">
<span id="serverBulkCount" style="font-size:12.5px;color:var(--text2);"></span>
<button class="btn btn-danger btn-sm" id="btnServerBulkStop">Stop Selected</button>
</div>
<div style="flex:1;"></div>
<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 style="width:32px;"><input type="checkbox" id="serverSelectAll" title="Select all"></th>
<th>ID</th>
<th>Port</th>
<th>hs:// URL</th>
<th>Secure</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="swarmsTable">
<tr><td colspan="6" 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="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 virtual host 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:420px;line-height:1.6;">
Install the Holesail root CA into your system keychain so Chrome trusts HTTPS certificates for your virtual hosts. Works with any private domain — <code style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--cyan);">myapp.hole.sail</code>, <code style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--cyan);">api.my.internal</code>, or any custom private TLD you choose.
</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 style="margin-bottom:16px;display:flex;justify-content:flex-end;">
<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 style="margin-bottom:16px;display:flex;justify-content:flex-end;">
<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>
<!-- ── Sync page ─────────────────────────────── -->
<div class="page" id="page-sync">
<div class="card sync-device-card" id="syncDeviceCard" style="margin-bottom:16px;">
<div class="card-body" aria-labelledby="syncDeviceSectionHeading">
<div class="section-heading" id="syncDeviceSectionHeading">This device</div>
<p id="syncDeviceIntro" class="sync-device-intro">Custom name is shown to other linked devices and replaces your computer host name when set. Stored only on this machine; the chosen name still syncs in the group like before.</p>
<div id="syncDeviceHero" class="sync-device-hero" hidden>
<div class="sync-device-hero__main">
<div id="syncDeviceNameDisplay" class="sync-device-hero-name"></div>
<p id="syncDeviceHeroLine" class="sync-device-hero-line">This is how this device appears in your sync group.</p>
</div>
<span class="sync-runtime-badge" id="syncRuntimeBadgeHero" title="How this app runs"></span>
</div>
<div id="syncDeviceEditWrap" class="sync-device-edit-wrap" aria-label="Edit device display name">
<div class="sync-device-edit-row">
<input type="text" id="syncDeviceNameInput" class="input sync-device-input" placeholder="Use computer host name if empty" maxlength="64" autocomplete="off" />
<span id="syncDeviceNameCounter" class="sync-device-name-counter" aria-live="polite">0 / 64</span>
<button type="button" class="btn btn-primary" id="btnSaveDeviceName">Save name</button>
<button type="button" class="btn btn-secondary" id="btnClearDeviceName" hidden>Clear custom name</button>
<span class="sync-runtime-badge" id="syncRuntimeBadgeEdit" title="How this app runs"></span>
</div>
<p id="syncDeviceNameHint" class="sync-device-hint"></p>
</div>
<button type="button" class="btn btn-secondary sync-device-change-btn" id="btnChangeDeviceName" hidden>Change name</button>
</div>
</div>
<div class="card" style="margin-bottom:16px;">
<div class="card-body">
<div class="section-heading">Device sync</div>
<p style="margin:0 0 14px 0;font-size:13px;color:var(--text3);">Sync your Holesail state (virtual hosts, servers, tunnels, SSH/RDP connections, settings) to another device. CA certificates are not synced; install the root CA on the other device if needed.</p>
<div id="syncStatus" class="sync-status-live" role="status" aria-live="polite" aria-atomic="true" style="display:flex;align-items:center;gap:8px;margin-bottom:14px;">
<div class="status-dot" id="syncStatusDot" aria-hidden="true"></div>
<span id="syncStatusText">Checking…</span>
<span id="syncLastSynced" style="font-size:11px;color:var(--text4);"></span>
</div>
<div style="display:flex;flex-wrap:wrap;gap:10px;">
<button class="btn btn-secondary" id="btnCreateSyncInvite">
Create invite
</button>
<button class="btn btn-secondary" id="btnCopyInvite" style="display:none;">
Copy invite
</button>
<div id="syncLinkDeviceForm" style="display:flex;align-items:center;gap:8px;flex:1;min-width:200px;">
<input type="text" id="syncInviteInput" class="input" placeholder="Paste invite from other device" style="flex:1;" />
<button class="btn btn-primary" id="btnPairWithInvite">Link device</button>
</div>
</div>
<div id="syncInviteDisplay" style="display:none;margin-top:12px;padding:10px;background:var(--card);border:1px solid var(--border);border-radius:var(--radius);font-family:var(--font-mono);font-size:12px;word-break:break-all;"></div>
</div>
</div>
<div class="card" id="syncLinkedDevicesCard" style="display:none;margin-bottom:16px;">
<div class="card-body flush">
<div class="section-heading" style="padding:0 16px 10px 16px;">Linked devices</div>
<div class="sync-linked-toolbar">
<label class="sync-linked-filter-label" for="syncLinkedDevicesFilter">Filter devices</label>
<input type="search" id="syncLinkedDevicesFilter" class="input sync-linked-filter-input" placeholder="Search by name, ID, or type" autocomplete="off" />
</div>
<table class="table sync-linked-table">
<thead>
<tr>
<th>Device</th>
<th>ID</th>
<th>Type</th>
</tr>
</thead>
<tbody id="syncLinkedDevicesTable">
<tr><td colspan="3" class="empty-cell">Not linked</td></tr>
</tbody>
</table>
<div style="padding:12px 16px;border-top:1px solid var(--border);display:flex;gap:10px;flex-wrap:wrap;">
<button class="btn btn-secondary" id="btnDelink">Delink</button>
<button class="btn btn-secondary" id="btnDisband" style="display:none;">Disband group</button>
</div>
</div>
</div>
</div>
<!-- ── Settings page ──────────────────────────── -->
<div class="page" id="page-settings">
<div class="card">
<div class="card-body">
<div class="section-heading">Appearance</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Light Theme</div>
<div class="setting-desc">Switch between dark (default) and light color scheme</div>
</div>
<div class="setting-control">
<div class="toggle" id="toggleTheme"></div>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Dashboard Refresh</div>
<div class="setting-desc">How often to refresh state. Paused when the tab is in the background.</div>
</div>
<div class="setting-control">
<select id="dashboardRefreshIntervalMs" class="input" style="width:120px;">
<option value="2000">Every 2 s</option>
<option value="5000">Every 5 s</option>
<option value="10000">Every 10 s</option>
<option value="0">Paused</option>
</select>
</div>
</div>
<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">Tunnel Error Alert</div>
<div class="setting-desc">Show a notification when a virtual host or service tunnel enters an error state</div>
</div>
<div class="setting-control">
<div class="toggle" id="toggleNotifyTunnelError" data-setting="notifyOnTunnelError"></div>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Tunnel Auto-Reconnect</div>
<div class="setting-desc">Automatically reconnect virtual hosts and service tunnels on error or disconnect (exponential backoff)</div>
</div>
<div class="setting-control">
<div class="toggle" id="toggleTunnelAutoReconnect" data-setting="tunnelAutoReconnect"></div>
</div>
</div>
<div class="section-heading">Sync</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Sync Servers</div>
<div class="setting-desc">When enabled, server tunnels are synced across linked devices. When disabled (default), server tunnels stay local to this device.</div>
</div>
<div class="setting-control">
<div class="toggle" id="toggleSyncServers" data-setting="syncServers"></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">Latency Ping</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Enable Latency Ping</div>
<div class="setting-desc">Periodically ping tunnel ports to display live latency badges on virtual hosts and service tunnels</div>
</div>
<div class="setting-control">
<div class="toggle" id="toggleLatencyPing" data-setting="latencyPingEnabled"></div>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Ping Interval (seconds)</div>
<div class="setting-desc">How often to ping tunnel ports for latency. Minimum 1 s, default 5 s.</div>
</div>
<div class="setting-control">
<input type="number" id="latencyPingIntervalMs" min="1" max="60" step="0.5" class="input" style="width:90px;" placeholder="5">
</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 class="setting-row">
<div class="setting-info">
<div class="setting-label">Auto-Backup Interval (hours)</div>
<div class="setting-desc">Automatically create a backup every N hours. Set to 0 to disable.</div>
</div>
<div class="setting-control">
<input type="number" id="backupIntervalHours" min="0" max="720" class="input" style="width:90px;" placeholder="0">
</div>
</div>
<div class="section-heading">Import / Export</div>
<div class="setting-row" style="align-items:flex-start;">
<div class="setting-info">
<div class="setting-label">Export Configuration</div>
<div class="setting-desc">Download virtual hosts, service tunnels, SSH &amp; RDP connections as JSON. CA private key is never included.</div>
</div>
<div class="setting-control">
<button class="btn btn-secondary btn-sm" id="btnExportConfig">
<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="3" x2="12" y2="15"/></svg>
Export
</button>
</div>
</div>
<div class="setting-row" style="align-items:flex-start;">
<div class="setting-info">
<div class="setting-label">Import Configuration</div>
<div class="setting-desc">Merge connections from a previously exported JSON file. Existing entries are not overwritten.</div>
</div>
<div class="setting-control">
<label class="btn btn-secondary btn-sm" style="cursor:pointer;">
<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
<input type="file" id="importConfigFile" accept=".json" style="display:none;">
</label>
</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 style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;">
<div id="svcBulkBar" style="display:none;align-items:center;gap:8px;">
<span id="svcBulkCount" style="font-size:12.5px;color:var(--text2);"></span>
<button class="btn btn-danger btn-sm" id="btnSvcBulkRemove">Remove Selected</button>
</div>
<div style="flex:1;"></div>
<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 style="width:32px;"><input type="checkbox" id="svcSelectAll" title="Select all"></th>
<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="6" 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 style="margin-bottom:16px;display:flex;justify-content:flex-end;">
<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="logs-toolbar">
<input type="text" class="input logs-filter" id="logsFilter" placeholder="Filter logs…">
<div style="display:flex;gap:4px;">
<button class="btn btn-secondary btn-sm log-level-btn active" data-level="all">All</button>
<button class="btn btn-secondary btn-sm log-level-btn" data-level="info">Info</button>
<button class="btn btn-secondary btn-sm log-level-btn" data-level="warn" style="color:var(--amber);">Warn</button>
<button class="btn btn-secondary btn-sm log-level-btn" data-level="error" style="color:var(--red);">Error</button>
</div>
<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-secondary" id="btnDownloadLogs">
<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="3" x2="12" y2="15"/></svg>
Download
</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="form-group">
<label class="checkbox-row">
<input type="checkbox" id="addVhostUseTls" autocomplete="off">
<label for="addVhostUseTls">Use TLS (secure connection)</label>
</label>
<div style="font-size:11px;color:var(--text4);margin-top:4px;">Enable if the service behind the tunnel uses HTTPS (port 443).</div>
</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="form-group">
<label class="form-label" for="startServerLabel">Label <span style="color:var(--text4);font-weight:400;">(optional)</span></label>
<input type="text" id="startServerLabel" class="input" placeholder="e.g. My Web App" autocomplete="off">
</div>
<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 your virtual host domains — including any private TLD you use (e.g. <code style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--cyan);">myapp.hole.sail</code>, <code style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--cyan);">api.my.internal</code>).
</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>
<label class="checkbox-row" style="margin-top:8px;">
<input type="checkbox" id="sshConnAutoReconnect">
<label for="sshConnAutoReconnect">Auto-reconnect on disconnect</label>
</label>
<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>
<!-- ── Modal: Delink (sync) confirm ────────────────────── -->
<div class="modal-backdrop" id="modal-delink">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-delink-title" style="max-width:420px;">
<div class="modal-header">
<span class="modal-title" id="modal-delink-title">Delink this device</span>
<button class="btn-icon" data-close-modal="modal-delink" 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 will remove this device from the sync group. Peers and the master will see you leave. Your local state (virtual hosts, tunnels, SSH/RDP connections, settings) will be <strong>reset to defaults</strong>. This cannot be undone.</p>
<p class="modal-desc" style="margin-top:8px;color:var(--amber);">Create a backup first if you want to keep a copy.</p>
<div class="modal-error" id="delinkError"></div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-close-modal="modal-delink">Cancel</button>
<button class="btn btn-primary" id="delinkConfirm">Delink and reset</button>
</div>
</div>
</div>
<!-- ── Modal: Disband (sync) confirm ───────────────────── -->
<div class="modal-backdrop" id="modal-disband">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-disband-title" style="max-width:420px;">
<div class="modal-header">
<span class="modal-title" id="modal-disband-title">Disband sync group</span>
<button class="btn-icon" data-close-modal="modal-disband" 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">As the master, you can disband the sync group. <strong>All other linked devices will be reset to defaults</strong> and removed from the group; they can create a new group or join another. Your own state on this device is kept.</p>
<p class="modal-desc" style="margin-top:8px;">This device will no longer be linked after disbanding.</p>
<div class="modal-error" id="disbandError"></div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-close-modal="modal-disband">Cancel</button>
<button class="btn btn-primary" id="disbandConfirm">Disband group</button>
</div>
</div>
</div>
<!-- ── Modal: Link device (sync) confirm ────────────────── -->
<div class="modal-backdrop" id="modal-linkDevice">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-linkDevice-title" style="max-width:420px;">
<div class="modal-header">
<span class="modal-title" id="modal-linkDevice-title">Link device</span>
<button class="btn-icon" data-close-modal="modal-linkDevice" 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">Linking will <strong>replace all current state on this device</strong> with the state from the other device.</p>
<p class="modal-desc" style="margin-top:8px;color:var(--amber);">Your virtual hosts, server tunnels, service tunnels, SSH and RDP connections, and settings on this device will be removed and replaced. This cannot be undone. Create a backup first if you want to keep a copy.</p>
<p class="modal-desc" style="margin-top:8px;">After linking, both devices will stay in sync: changes on either device will update the other.</p>
<div class="modal-error" id="linkDeviceError"></div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-close-modal="modal-linkDevice">Cancel</button>
<button class="btn btn-primary" id="linkDeviceConfirm">Replace state and link</button>
</div>
</div>
</div>
<!-- ── Modal: Holesail Lookup Result ───────────────────── -->
<div class="modal-backdrop" id="modal-holesailLookup">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-holesailLookup-title" style="max-width:420px;">
<div class="modal-header">
<span class="modal-title" id="modal-holesailLookup-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;vertical-align:-3px;margin-right:6px;"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
Holesail Lookup Result
</span>
<button class="btn-icon" data-close-modal="modal-holesailLookup" 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" id="holesailLookupBody" style="padding:20px 24px;">
</div>
<div class="modal-footer">
<button class="btn btn-primary" data-close-modal="modal-holesailLookup">Close</button>
</div>
</div>
</div>
<!-- Load order: data → core → ui → pages → refresh → events → init -->
<!-- Data layer -->
<script src="data/tlds.js"></script>
<script src="data/hostname-validator.js"></script>
<script src="data/install-commands.js"></script>
<!-- Core utilities and state -->
<script src="core/utils.js"></script>
<script src="core/state.js"></script>
<script src="core/messaging.js"></script>
<script src="core/navigation.js"></script>
<!-- UI primitives -->
<script src="ui/toast.js"></script>
<script src="ui/modal.js"></script>
<script src="ui/state-tag.js"></script>
<!-- Page modules -->
<script src="pages/settings.js"></script>
<script src="pages/overview.js"></script>
<script src="pages/virtual-hosts.js"></script>
<script src="pages/servers.js"></script>
<script src="pages/service-tunnels.js"></script>
<script src="pages/proxy-ca.js"></script>
<script src="pages/backups.js"></script>
<script src="pages/sync.js"></script>
<script src="pages/logs.js"></script>
<script src="pages/ssh.js"></script>
<script src="pages/rdp.js"></script>
<!-- Orchestration -->
<script src="refresh.js"></script>
<script src="events.js"></script>
<!-- Entry point (runs init()) -->
<script src="core/init.js"></script>
<!-- PWA service worker registration (required for installability) -->
<script src="register-sw.js"></script>
</body>
</html>