Files
p2ns/includes/admin/admin-frontend/index.html
T
2026-05-30 21:25:24 -04:00

1708 lines
103 KiB
HTML

<!DOCTYPE html>
<html lang="en" class="dark" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#0a0e1a">
<title>P2NS Admin Panel</title>
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="icon" href="/branding/p2ns-favicon-32.png" type="image/png" sizes="32x32">
<link rel="icon" href="/branding/p2ns-favicon-192.png" type="image/png" sizes="192x192">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">
<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&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/tailwind.css">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
body.admin-app {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #0a0e1a 0%, #0f1419 50%, #1a1f2e 100%);
background-attachment: fixed;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow: hidden;
height: 100dvh;
}
body.admin-app.no-scroll {
overflow: hidden;
height: 100dvh;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/xterm.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/css/xterm.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
</head>
<body class="admin-app" style="color: var(--text-primary);">
<div class="admin-shell">
<aside id="admin-sidebar" class="admin-sidebar" aria-label="Main navigation">
<div class="admin-sidebar-brand">
<a href="/" class="admin-sidebar-brand-link" aria-label="P2NS Admin home">
<img src="/branding/p2ns-icon.png" alt="" class="admin-sidebar-brand-logo" width="28" height="28">
<span class="admin-sidebar-brand-title">P2NS</span>
</a>
</div>
<nav class="admin-sidebar-nav">
<div class="admin-nav-section">
<p class="admin-nav-section-label">Core</p>
<button type="button" data-tab-id="domains" class="admin-nav-link" onclick="if(window.navigateToTab) window.navigateToTab('domains'); else location.hash = 'domains';"><i class="fas fa-globe admin-nav-icon" aria-hidden="true"></i>Domains</button>
<button type="button" data-tab-id="host" class="admin-nav-link" onclick="if(window.navigateToTab) window.navigateToTab('host'); else location.hash = 'host';"><i class="fas fa-server admin-nav-icon" aria-hidden="true"></i>Host</button>
<button type="button" data-tab-id="entries" class="admin-nav-link" onclick="if(window.navigateToTab) window.navigateToTab('entries'); else location.hash = 'entries';"><i class="fas fa-list admin-nav-icon" aria-hidden="true"></i>Entries</button>
<button type="button" data-tab-id="peers" class="admin-nav-link" onclick="if(window.navigateToTab) window.navigateToTab('peers'); else location.hash = 'peers';"><i class="fas fa-network-wired admin-nav-icon" aria-hidden="true"></i>Peers</button>
</div>
<div class="admin-nav-section">
<p class="admin-nav-section-label">Network</p>
<button type="button" data-tab-id="local-dns" class="admin-nav-link" onclick="if(window.navigateToTab) window.navigateToTab('local-dns'); else location.hash = 'local-dns';"><i class="fas fa-sitemap admin-nav-icon" aria-hidden="true"></i>Local DNS</button>
<button type="button" data-tab-id="interfaces" class="admin-nav-link" onclick="if(window.navigateToTab) window.navigateToTab('interfaces'); else location.hash = 'interfaces';"><i class="fas fa-ethernet admin-nav-icon" aria-hidden="true"></i>Interfaces</button>
</div>
<div class="admin-nav-section">
<p class="admin-nav-section-label">Security</p>
<button type="button" data-tab-id="certs" class="admin-nav-link" onclick="if(window.navigateToTab) window.navigateToTab('certs'); else location.hash = 'certs';"><i class="fas fa-certificate admin-nav-icon" aria-hidden="true"></i>Certificates</button>
</div>
<div class="admin-nav-section">
<p class="admin-nav-section-label">Monitoring</p>
<button type="button" data-tab-id="stats" class="admin-nav-link" onclick="if(window.navigateToTab) window.navigateToTab('stats'); else location.hash = 'stats';"><i class="fas fa-chart-line admin-nav-icon" aria-hidden="true"></i>Stats</button>
<button type="button" data-tab-id="logs" class="admin-nav-link" onclick="if(window.navigateToTab) window.navigateToTab('logs'); else location.hash = 'logs';"><i class="fas fa-terminal admin-nav-icon" aria-hidden="true"></i>Logs</button>
</div>
<div class="admin-nav-section">
<p class="admin-nav-section-label">System</p>
<button type="button" data-tab-id="backups" class="admin-nav-link" onclick="if(window.navigateToTab) window.navigateToTab('backups'); else location.hash = 'backups';"><i class="fas fa-database admin-nav-icon" aria-hidden="true"></i>Backups</button>
<button type="button" data-tab-id="plugins" class="admin-nav-link" onclick="if(window.navigateToTab) window.navigateToTab('plugins'); else location.hash = 'plugins';"><i class="fas fa-puzzle-piece admin-nav-icon" aria-hidden="true"></i>Plugins</button>
<button type="button" data-tab-id="settings" class="admin-nav-link" onclick="if(window.navigateToTab) window.navigateToTab('settings'); else location.hash = 'settings';"><i class="fas fa-cog admin-nav-icon" aria-hidden="true"></i>Settings</button>
</div>
</nav>
<div class="admin-sidebar-footer">
<span class="admin-sidebar-footer-text">P2NS Network</span>
</div>
</aside>
<div id="admin-sidebar-overlay" class="admin-sidebar-overlay" onclick="if(window.closeAdminSidebar) window.closeAdminSidebar();" aria-hidden="true"></div>
<div class="admin-main">
<header class="admin-topbar">
<button type="button" id="admin-sidebar-toggle" class="admin-sidebar-toggle" onclick="if(window.toggleAdminSidebar) window.toggleAdminSidebar();" aria-label="Toggle navigation menu" aria-expanded="false">
<i class="fas fa-bars" aria-hidden="true"></i>
</button>
<div class="admin-topbar-spacer"></div>
<div id="status-indicator" class="admin-status-indicator"></div>
<div class="admin-alerts-wrap">
<button type="button" id="admin-alerts-button" class="admin-topbar-action admin-alerts-bell" title="Alerts" aria-label="Alerts" aria-expanded="false" aria-haspopup="true">
<i class="fas fa-bell" aria-hidden="true"></i>
</button>
<span id="admin-alerts-badge" class="admin-alerts-badge" aria-hidden="true">0</span>
<div id="admin-alerts-panel" class="admin-alerts-panel" role="dialog" aria-label="Alerts">
<header class="admin-alerts-panel-header">
<h3 class="admin-alerts-panel-title">Alerts</h3>
<button type="button" class="admin-alerts-panel-close" onclick="if(window.closeAdminAlertsPanel) window.closeAdminAlertsPanel();" aria-label="Close alerts">
<i class="fas fa-xmark" aria-hidden="true"></i>
</button>
</header>
<div id="admin-alerts-empty" class="admin-alerts-empty admin-alerts-empty--visible">
<i class="fas fa-bell-slash admin-alerts-empty-icon" aria-hidden="true"></i>
<p class="admin-alerts-empty-title">All clear</p>
<p class="admin-alerts-empty-text theme-text-tertiary">No active warnings or issues.</p>
</div>
<div id="admin-alerts-list" class="admin-alerts-list"></div>
</div>
</div>
<button id="refresh-button" onclick="handleRefresh()" class="admin-topbar-action" title="Gracefully stop the process" aria-label="Refresh">
<i class="fas fa-redo" aria-hidden="true"></i>
</button>
</header>
<div id="admin-content" class="admin-content admin-content--fill">
<div id="domains" class="tab-content hidden flex flex-col admin-tab-panel--fixed">
<header class="admin-tab-header flex-shrink-0">
<div class="admin-tab-header-main">
<h2 class="admin-tab-title">Domains</h2>
<p class="admin-tab-description">Browse resolved domains, consensus status, and local claims on the P2NS network.</p>
</div>
<button type="button" onclick="openInfoModal('domains')" class="theme-button-info text-sm px-3 py-1 rounded">Info</button>
</header>
<div class="domains-layout flex-1 min-h-0 flex flex-col">
<section class="theme-card admin-panel domains-panel flex flex-col flex-1 min-h-0">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Resolved domains</h3>
<p class="admin-panel-subtitle" id="domainsCount">Loading…</p>
</div>
<div class="admin-toolbar flex-shrink-0 domains-toolbar">
<label class="admin-search-field">
<i class="fas fa-magnifying-glass admin-search-icon" aria-hidden="true"></i>
<input id="search-domains" type="search" placeholder="Search by domain, hash, or status…" class="theme-input admin-search-input" oninput="filterDomains()" autocomplete="off">
</label>
<div class="domains-filter-row">
<label class="domains-filter-field">
<span class="domains-filter-label">Consensus</span>
<select id="filter-consensus-status" onchange="filterDomains(); saveDomainFilterSettings()" class="theme-input domains-filter-select">
<option value="all" selected>All statuses</option>
<option value="resolved">Resolved</option>
<option value="internal">Internal</option>
<option value="conflict">Conflict</option>
<option value="tie">Tie</option>
<option value="insufficient_quorum">No quorum</option>
<option value="no_claims">No claims</option>
<option value="error">Error</option>
<option value="unknown">Unknown</option>
</select>
</label>
<label class="domains-filter-field">
<span class="domains-filter-label">Hash type</span>
<select id="filter-hash-type" onchange="filterDomains(); saveDomainFilterSettings()" class="theme-input domains-filter-select">
<option value="all">All types</option>
<option value="internal">Internal</option>
<option value="holesail" selected>Holesail</option>
</select>
</label>
<label class="domains-filter-field">
<span class="domains-filter-label">Ownership</span>
<select id="filter-ownership" onchange="filterDomains(); saveDomainFilterSettings()" class="theme-input domains-filter-select">
<option value="all" selected>All</option>
<option value="local">Local</option>
<option value="remote">Remote</option>
</select>
</label>
</div>
</div>
<div class="domains-table-scroll flex-1 min-h-0 overflow-auto">
<table class="w-full theme-table domains-table">
<thead class="theme-table thead sticky top-0 z-10">
<tr>
<th class="p-3 text-left">Domain</th>
<th class="p-3 text-left">Hash</th>
<th class="p-3 text-left w-[11rem]">Consensus</th>
<th class="p-3 text-right w-[4.5rem]"><span class="sr-only">Actions</span></th>
</tr>
</thead>
<tbody id="domainsTable"></tbody>
</table>
<div id="domainsDisconnected" class="local-dns-empty">
<i class="fas fa-plug local-dns-empty-icon" aria-hidden="true"></i>
<p class="local-dns-empty-title">Disconnected</p>
<p class="local-dns-empty-text theme-text-tertiary">Reconnecting to P2NS server…</p>
</div>
<div id="domainsEmpty" class="local-dns-empty">
<i class="fas fa-globe local-dns-empty-icon" aria-hidden="true"></i>
<p class="local-dns-empty-title">No domains</p>
<p class="local-dns-empty-text theme-text-tertiary">Resolved domains will appear here as the network syncs.</p>
</div>
<div id="domainsFilteredEmpty" class="local-dns-empty">
<i class="fas fa-filter local-dns-empty-icon" aria-hidden="true"></i>
<p class="local-dns-empty-title">No matching domains</p>
<p class="local-dns-empty-text theme-text-tertiary">Try adjusting your search or filters.</p>
</div>
<div id="domainsEnd" class="local-dns-list-end" aria-live="polite">You have reached the end</div>
<div id="domainsScrollSentinel" class="scroll-sentinel"></div>
</div>
<div id="domainsPagination" class="hidden"></div>
<div class="admin-panel-footer flex justify-end flex-shrink-0">
<button type="button" onclick="openAddModal()" class="admin-btn admin-btn--primary">
<i class="fas fa-plus" aria-hidden="true"></i> Add domain
</button>
</div>
</section>
</div>
</div>
<div id="local-dns" class="tab-content hidden flex flex-col admin-tab-panel--fixed">
<header class="admin-tab-header flex-shrink-0">
<div class="admin-tab-header-main">
<h2 class="admin-tab-title">Local DNS</h2>
<p class="admin-tab-description">Override resolution with custom records, choose public vs P2P when both exist, and set hash preferences for conflicting domains.</p>
</div>
<button type="button" onclick="openInfoModal('local-dns')" class="theme-button-info text-sm px-3 py-1 rounded">Info</button>
</header>
<div class="admin-subtabs flex flex-shrink-0 mb-4">
<button type="button" onclick="showSubTab('local-dns', 'records')" id="local-dns-subtab-records" class="admin-subtab-btn admin-subtab-btn--active">
<i class="fas fa-list-ul admin-subtab-icon" aria-hidden="true"></i>
DNS Records
<span id="localDnsSubtabBadge" class="admin-subtab-badge" hidden>0</span>
</button>
<button type="button" onclick="showSubTab('local-dns', 'conflicts')" id="local-dns-subtab-conflicts" class="admin-subtab-btn">
<i class="fas fa-arrows-left-right admin-subtab-icon" aria-hidden="true"></i>
DNS Conflicts
<span id="dnsConflictsSubtabBadge" class="admin-subtab-badge" hidden>0</span>
</button>
<button type="button" onclick="showSubTab('local-dns', 'p2p-conflicts')" id="local-dns-subtab-p2p-conflicts" class="admin-subtab-btn">
<i class="fas fa-code-branch admin-subtab-icon" aria-hidden="true"></i>
P2P Conflicts
<span id="p2pConflictsSubtabBadge" class="admin-subtab-badge" hidden>0</span>
</button>
</div>
<!-- DNS Records -->
<div id="local-dns-records" class="sub-tab-content flex flex-col flex-1 min-h-0">
<div class="local-dns-layout flex-1 min-h-0 grid grid-cols-1 lg:grid-cols-3 gap-4">
<section class="theme-card admin-panel local-dns-panel lg:col-span-2 flex flex-col min-h-0">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Custom records</h3>
<p class="admin-panel-subtitle" id="localDnsCount">Loading…</p>
</div>
<div class="admin-toolbar flex-shrink-0">
<label class="admin-search-field">
<i class="fas fa-magnifying-glass admin-search-icon" aria-hidden="true"></i>
<input id="search-local-dns" type="search" placeholder="Search by name, type, or value…" class="theme-input admin-search-input" oninput="filterLocalDNS()" autocomplete="off">
</label>
</div>
<div class="local-dns-table-scroll flex-1 min-h-0 overflow-auto">
<table class="w-full theme-table local-dns-table">
<thead class="theme-table thead sticky top-0 z-10">
<tr>
<th class="p-3 text-left">Name</th>
<th class="p-3 text-left w-[5.5rem]">Type</th>
<th class="p-3 text-left">Value</th>
<th class="p-3 text-left w-[4.5rem]">TTL</th>
<th class="p-3 text-right w-[6.5rem]"><span class="sr-only">Actions</span></th>
</tr>
</thead>
<tbody id="localDnsTable"></tbody>
</table>
<div id="localDnsEmpty" class="local-dns-empty">
<i class="fas fa-sitemap local-dns-empty-icon" aria-hidden="true"></i>
<p class="local-dns-empty-title">No local DNS records</p>
<p class="local-dns-empty-text theme-text-tertiary">Add overrides that take precedence over P2NS resolution on this node.</p>
</div>
<div id="localDnsFilteredEmpty" class="local-dns-empty">
<i class="fas fa-filter local-dns-empty-icon" aria-hidden="true"></i>
<p class="local-dns-empty-title">No matching records</p>
<p class="local-dns-empty-text theme-text-tertiary">Try a different search term.</p>
</div>
<div id="localDnsEnd" class="local-dns-list-end" aria-live="polite">You have reached the end</div>
<div id="localDnsScrollSentinel" class="scroll-sentinel"></div>
</div>
<div id="localDnsPagination" class="hidden"></div>
<div class="admin-panel-footer flex justify-end flex-shrink-0">
<button type="button" onclick="openLocalDnsModal()" class="admin-btn admin-btn--primary">
<i class="fas fa-plus" aria-hidden="true"></i> Add record
</button>
</div>
</section>
<aside class="local-dns-sidebar flex flex-col gap-4 min-h-0">
<section class="theme-card admin-panel local-dns-summary-panel">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Overview</h3>
<p class="admin-panel-subtitle" id="localDnsOverviewLine"></p>
</div>
<dl class="local-dns-stat-grid">
<div class="local-dns-stat">
<dt>Total</dt>
<dd id="localDnsStatTotal"></dd>
</div>
<div class="local-dns-stat">
<dt>A / AAAA</dt>
<dd id="localDnsStatAddress"></dd>
</div>
<div class="local-dns-stat">
<dt>CNAME</dt>
<dd id="localDnsStatCname"></dd>
</div>
<div class="local-dns-stat">
<dt>Other</dt>
<dd id="localDnsStatOther"></dd>
</div>
</dl>
</section>
<section class="theme-card admin-panel local-dns-note-panel">
<div class="admin-panel-header">
<h3 class="admin-panel-title">About local DNS</h3>
</div>
<p class="local-dns-note-text theme-text-tertiary">Records in <code class="local-dns-inline-code">cache/local_dns.json</code> override P2NS and public resolution for matching names on this node.</p>
</section>
</aside>
</div>
</div>
<!-- DNS Conflicts -->
<div id="local-dns-conflicts" class="sub-tab-content hidden flex flex-col flex-1 min-h-0">
<section class="theme-card admin-panel local-dns-conflicts-panel flex flex-col flex-1 min-h-0">
<div class="admin-panel-header">
<div class="flex items-start justify-between gap-2">
<div>
<h3 class="admin-panel-title">DNS conflict selector</h3>
<p class="admin-panel-subtitle" id="dnsConflictsCount">Loading…</p>
</div>
<button type="button" onclick="openInfoModal('dns-conflicts')" class="theme-button-info text-sm px-3 py-1 rounded flex-shrink-0">Info</button>
</div>
</div>
<div class="admin-toolbar flex-shrink-0">
<label class="admin-search-field">
<i class="fas fa-magnifying-glass admin-search-icon" aria-hidden="true"></i>
<input id="search-dns-conflicts" type="search" placeholder="Search by domain or IP…" class="theme-input admin-search-input" oninput="filterDnsConflicts()" autocomplete="off">
</label>
</div>
<div class="local-dns-table-scroll flex-1 min-h-0 overflow-auto">
<table class="w-full theme-table local-dns-conflicts-table">
<thead class="theme-table thead sticky top-0 z-10">
<tr>
<th class="p-3 text-left">Domain</th>
<th class="p-3 text-left">Public IP</th>
<th class="p-3 text-left w-[11rem]">Preferred source</th>
</tr>
</thead>
<tbody id="dnsConflictsTable"></tbody>
</table>
<div id="dnsConflictsEmpty" class="local-dns-empty">
<i class="fas fa-circle-check local-dns-empty-icon" aria-hidden="true"></i>
<p class="local-dns-empty-title">No DNS conflicts</p>
<p class="local-dns-empty-text theme-text-tertiary">Domains with both public and P2P resolution will appear here.</p>
</div>
<div id="dnsConflictsFilteredEmpty" class="local-dns-empty">
<i class="fas fa-filter local-dns-empty-icon" aria-hidden="true"></i>
<p class="local-dns-empty-title">No matching conflicts</p>
<p class="local-dns-empty-text theme-text-tertiary">Try a different search term.</p>
</div>
<div id="dnsConflictsEnd" class="local-dns-list-end" aria-live="polite">You have reached the end</div>
<div id="dnsConflictsScrollSentinel" class="scroll-sentinel"></div>
</div>
<div id="dnsConflictsPagination" class="hidden"></div>
</section>
</div>
<!-- P2P Domain Conflicts -->
<div id="local-dns-p2p-conflicts" class="sub-tab-content hidden flex flex-col flex-1 min-h-0">
<section class="theme-card admin-panel local-dns-p2p-panel flex flex-col flex-1 min-h-0">
<div class="admin-panel-header">
<div class="flex items-start justify-between gap-2">
<div>
<h3 class="admin-panel-title">P2P domain conflicts</h3>
<p class="admin-panel-subtitle" id="p2pConflictsCount">Loading…</p>
</div>
<button type="button" onclick="openInfoModal('p2p-domain-conflicts')" class="theme-button-info text-sm px-3 py-1 rounded flex-shrink-0">Info</button>
</div>
</div>
<div class="admin-toolbar flex-shrink-0">
<label class="admin-search-field">
<i class="fas fa-magnifying-glass admin-search-icon" aria-hidden="true"></i>
<input id="search-p2p-domain-conflicts" type="search" placeholder="Search by domain or hash…" class="theme-input admin-search-input" oninput="filterP2pDomainConflicts()" autocomplete="off">
</label>
</div>
<div class="local-dns-table-scroll flex-1 min-h-0 overflow-auto">
<table class="w-full theme-table local-dns-p2p-table">
<thead class="theme-table thead sticky top-0 z-10">
<tr>
<th class="p-3 text-left">Domain</th>
<th class="p-3 text-left">Local hash</th>
<th class="p-3 text-left">Resolved hash</th>
<th class="p-3 text-left w-[11rem]">Hash preference</th>
</tr>
</thead>
<tbody id="p2pDomainConflictsTable"></tbody>
</table>
<div id="p2pConflictsEmpty" class="local-dns-empty">
<i class="fas fa-circle-check local-dns-empty-icon" aria-hidden="true"></i>
<p class="local-dns-empty-title">No P2P conflicts</p>
<p class="local-dns-empty-text theme-text-tertiary">Domains where your local claim differs from network consensus appear here.</p>
</div>
<div id="p2pConflictsFilteredEmpty" class="local-dns-empty">
<i class="fas fa-filter local-dns-empty-icon" aria-hidden="true"></i>
<p class="local-dns-empty-title">No matching conflicts</p>
<p class="local-dns-empty-text theme-text-tertiary">Try a different search term.</p>
</div>
<div id="p2pConflictsEnd" class="local-dns-list-end" aria-live="polite">You have reached the end</div>
<div id="p2pDomainConflictsScrollSentinel" class="scroll-sentinel"></div>
</div>
<div id="p2pDomainConflictsPagination" class="hidden"></div>
</section>
</div>
</div>
<div id="entries" class="tab-content hidden flex flex-col admin-tab-panel--fixed">
<header class="admin-tab-header flex-shrink-0">
<div class="admin-tab-header-main">
<h2 class="admin-tab-title">Autopass Entries</h2>
<p class="admin-tab-description">Browse the distributed ledger — claims, votes, and other key-value records stored in Autopass.</p>
</div>
<button type="button" onclick="openInfoModal('entries')" class="theme-button-info text-sm px-3 py-1 rounded">Info</button>
</header>
<div class="entries-layout flex-1 min-h-0 grid grid-cols-1 lg:grid-cols-3 gap-4">
<section class="theme-card admin-panel entries-panel lg:col-span-2 flex flex-col min-h-0">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Ledger entries</h3>
<p class="admin-panel-subtitle" id="entriesCount">Loading…</p>
</div>
<div class="admin-toolbar flex-shrink-0">
<label class="admin-search-field">
<i class="fas fa-magnifying-glass admin-search-icon" aria-hidden="true"></i>
<input id="search-entries" type="search" placeholder="Search by key or value…" class="theme-input admin-search-input" oninput="filterEntries()" autocomplete="off">
</label>
</div>
<div class="entries-table-scroll flex-1 min-h-0 overflow-auto">
<table class="w-full theme-table entries-table">
<thead class="theme-table thead sticky top-0 z-10">
<tr>
<th class="p-3 text-left w-[6.5rem]">Type</th>
<th class="p-3 text-left">Key</th>
<th class="p-3 text-left">Value</th>
<th class="p-3 text-right w-[3.5rem]"><span class="sr-only">Actions</span></th>
</tr>
</thead>
<tbody id="entriesTable"></tbody>
</table>
<div id="entriesEmpty" class="entry-empty">
<i class="fas fa-database entry-empty-icon" aria-hidden="true"></i>
<p class="entry-empty-title">No ledger entries</p>
<p class="entry-empty-text theme-text-tertiary">Claims and votes will appear here as the network syncs.</p>
</div>
<div id="entriesFilteredEmpty" class="entry-empty">
<i class="fas fa-filter entry-empty-icon" aria-hidden="true"></i>
<p class="entry-empty-title">No matching entries</p>
<p class="entry-empty-text theme-text-tertiary">Try a different search term.</p>
</div>
<div id="entriesEnd" class="entry-list-end" aria-live="polite">You have reached the end</div>
<div id="entriesScrollSentinel" class="scroll-sentinel"></div>
</div>
<div id="entriesPagination" class="hidden"></div>
</section>
<aside class="entries-sidebar flex flex-col gap-4 min-h-0">
<section class="theme-card admin-panel entries-summary-panel">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Overview</h3>
<p class="admin-panel-subtitle" id="entriesOverviewLine"></p>
</div>
<dl class="entry-stat-grid">
<div class="entry-stat">
<dt>Total</dt>
<dd id="entryStatTotal"></dd>
</div>
<div class="entry-stat">
<dt>Claims</dt>
<dd id="entryStatClaims"></dd>
</div>
<div class="entry-stat">
<dt>Votes</dt>
<dd id="entryStatVotes"></dd>
</div>
<div class="entry-stat">
<dt>Other</dt>
<dd id="entryStatOther"></dd>
</div>
</dl>
</section>
<section class="theme-card admin-panel entries-note-panel">
<div class="admin-panel-header">
<h3 class="admin-panel-title">About the ledger</h3>
</div>
<p class="entry-note-text theme-text-tertiary">Each row is a key-value record from Autopass. Claims register domain ownership; votes express consensus preferences for resolved domains.</p>
</section>
</aside>
</div>
</div>
<div id="peers" class="tab-content hidden flex flex-col admin-tab-panel--fixed">
<header class="admin-tab-header flex-shrink-0">
<div class="admin-tab-header-main">
<h2 class="admin-tab-title">Connected Peers</h2>
<p class="admin-tab-description">View peer connections, connection metrics, and block or unblock nodes on the network.</p>
</div>
<button type="button" onclick="openInfoModal('peers')" class="theme-button-info text-sm px-3 py-1 rounded">Info</button>
</header>
<div class="peers-layout flex-1 min-h-0 grid grid-cols-1 lg:grid-cols-3 gap-4">
<section class="theme-card admin-panel peers-panel lg:col-span-2 flex flex-col min-h-0">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Peer connections</h3>
<p class="admin-panel-subtitle" id="peersCount">Loading…</p>
</div>
<div class="admin-toolbar flex-shrink-0">
<label class="admin-search-field">
<i class="fas fa-magnifying-glass admin-search-icon" aria-hidden="true"></i>
<input id="search-peers" type="search" placeholder="Search by peer ID or status…" class="theme-input admin-search-input" oninput="filterPeers()" autocomplete="off">
</label>
</div>
<div class="peer-list-scroll flex-1 min-h-0 overflow-y-auto">
<ul id="peersList" class="peer-list"></ul>
<div id="peersEmpty" class="peer-empty">
<i class="fas fa-network-wired peer-empty-icon" aria-hidden="true"></i>
<p class="peer-empty-title">No peers connected</p>
<p class="peer-empty-text theme-text-tertiary">Other nodes will appear here when they join the network.</p>
</div>
<div id="peersFilteredEmpty" class="peer-empty">
<i class="fas fa-filter peer-empty-icon" aria-hidden="true"></i>
<p class="peer-empty-title">No matching peers</p>
<p class="peer-empty-text theme-text-tertiary">Try a different search term or status.</p>
</div>
<div id="peersEnd" class="peer-list-end" aria-live="polite">You have reached the end</div>
<div id="peersScrollSentinel" class="scroll-sentinel"></div>
</div>
<div id="peersPagination" class="hidden"></div>
</section>
<aside class="peers-sidebar flex flex-col gap-4 min-h-0">
<section class="theme-card admin-panel peers-summary-panel">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Overview</h3>
<p class="admin-panel-subtitle" id="peersOverviewLine"></p>
</div>
<dl class="peer-stat-grid">
<div class="peer-stat">
<dt>Connected</dt>
<dd id="peerStatConnected"></dd>
</div>
<div class="peer-stat">
<dt>Disconnected</dt>
<dd id="peerStatDisconnected"></dd>
</div>
<div class="peer-stat">
<dt>Blocked</dt>
<dd id="peerStatBlocked"></dd>
</div>
</dl>
</section>
<section class="theme-card admin-panel peers-chart-panel flex flex-col">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Status distribution</h3>
<p class="admin-panel-subtitle">Current peer states on this node</p>
</div>
<div class="peer-chart-wrap">
<canvas id="peer-graph-chart" aria-label="Peer status distribution chart"></canvas>
</div>
</section>
</aside>
</div>
</div>
<div id="certs" class="tab-content hidden flex flex-col admin-tab-panel--fixed">
<header class="admin-tab-header flex-shrink-0">
<div class="admin-tab-header-main">
<h2 class="admin-tab-title">Certificates</h2>
<p class="admin-tab-description">Manage TLS certificates for domains and the local root certificate authority.</p>
</div>
<button type="button" onclick="openInfoModal('certs')" class="theme-button-info text-sm px-3 py-1 rounded">Info</button>
</header>
<div class="certs-layout flex-1 min-h-0 grid grid-cols-1 lg:grid-cols-3 gap-4">
<section class="theme-card admin-panel certs-panel lg:col-span-2 flex flex-col min-h-0">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Domain certificates</h3>
<p class="admin-panel-subtitle">Click a domain to view PEM details.</p>
</div>
<div class="admin-toolbar flex-shrink-0">
<label class="admin-search-field">
<i class="fas fa-magnifying-glass admin-search-icon" aria-hidden="true"></i>
<input id="search-certs" type="search" placeholder="Search certificates…" class="theme-input admin-search-input" oninput="filterCerts()" autocomplete="off">
</label>
</div>
<div class="cert-list-scroll flex-1 min-h-0 overflow-y-auto">
<ul id="certsList" class="cert-list"></ul>
<div id="certsEmpty" class="cert-empty">
<i class="fas fa-certificate cert-empty-icon" aria-hidden="true"></i>
<p class="cert-empty-title">No certificates yet</p>
<p class="cert-empty-text theme-text-tertiary">Generate a certificate for a domain below.</p>
</div>
<div id="certsEnd" class="cert-list-end" aria-live="polite">You have reached the end</div>
<div id="certsScrollSentinel" class="scroll-sentinel"></div>
</div>
<div id="certsPagination" class="hidden"></div>
<div class="admin-panel-footer cert-generate-bar flex-shrink-0">
<label class="cert-generate-field">
<span class="cert-generate-label">Domain</span>
<input id="cert-domain" type="text" placeholder="example.p2p" class="theme-input cert-generate-input" autocomplete="off">
</label>
<button type="button" onclick="generateCert()" class="admin-btn admin-btn--primary">
<i class="fas fa-plus" aria-hidden="true"></i> Generate certificate
</button>
</div>
</section>
<section class="theme-card admin-panel cert-ca-panel flex flex-col">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Root CA</h3>
<p class="admin-panel-subtitle">Signs all domain certificates on this node.</p>
</div>
<div class="cert-ca-note">
<i class="fas fa-triangle-exclamation" aria-hidden="true"></i>
<p>Regenerating the root CA invalidates every existing certificate. You will need to reinstall the CA and regenerate domain certs.</p>
</div>
<div class="cert-ca-actions">
<button type="button" onclick="openInfoModal('ca-management')" class="admin-btn admin-btn--secondary admin-btn--sm w-full">
<i class="fas fa-circle-info" aria-hidden="true"></i> About CA management
</button>
<button type="button" onclick="installCA()" class="admin-btn admin-btn--primary w-full">
<i class="fas fa-download" aria-hidden="true"></i> Install root CA
</button>
<button type="button" onclick="regenerateCA()" class="admin-btn admin-btn--danger w-full">
<i class="fas fa-rotate-right" aria-hidden="true"></i> Regenerate root CA
</button>
</div>
</section>
</div>
</div>
<div id="interfaces" class="tab-content hidden flex flex-col admin-tab-panel--fixed">
<header class="admin-tab-header flex-shrink-0">
<div class="admin-tab-header-main">
<h2 class="admin-tab-title">Virtual Interfaces</h2>
<p class="admin-tab-description">Domain-to-IP mappings for local routing, subnet usage, and tunnel bindings.</p>
</div>
<button type="button" onclick="openInfoModal('interfaces')" class="theme-button-info text-sm px-3 py-1 rounded">Info</button>
</header>
<div class="interfaces-layout flex-1 min-h-0 grid grid-cols-1 lg:grid-cols-3 gap-4">
<section class="theme-card admin-panel interfaces-panel lg:col-span-2 flex flex-col min-h-0">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Domain mappings</h3>
<p class="admin-panel-subtitle"><span id="interfacesCount">0</span> assigned</p>
</div>
<div class="admin-toolbar flex-shrink-0">
<label class="admin-search-field">
<i class="fas fa-magnifying-glass admin-search-icon" aria-hidden="true"></i>
<input id="search-interfaces" type="search" placeholder="Search by domain, IP, or subnet…" class="theme-input admin-search-input" oninput="filterInterfaces()" autocomplete="off">
</label>
</div>
<div class="interfaces-table-scroll flex-1 min-h-0 overflow-auto">
<table class="w-full theme-table interfaces-table">
<thead class="theme-table thead sticky top-0 z-10">
<tr>
<th class="p-3 text-left">Domain</th>
<th class="p-3 text-left">IP address</th>
<th class="p-3 text-left">Type</th>
<th class="p-3 text-left">Subnet</th>
<th class="p-3 text-left">Status</th>
<th class="p-3 text-left">Tunnels</th>
<th class="p-3 text-right">Actions</th>
</tr>
</thead>
<tbody id="interfacesTable"></tbody>
</table>
<div id="interfacesEmpty" class="iface-empty">
<i class="fas fa-ethernet iface-empty-icon" aria-hidden="true"></i>
<p class="iface-empty-title">No interface mappings</p>
<p class="iface-empty-text theme-text-tertiary">Domains receive virtual IPs when resolved or when Holesail clients connect.</p>
</div>
<div id="interfacesFilteredEmpty" class="iface-empty">
<i class="fas fa-filter iface-empty-icon" aria-hidden="true"></i>
<p class="iface-empty-title">No matching mappings</p>
<p class="iface-empty-text theme-text-tertiary">Try a different search term.</p>
</div>
<div id="interfacesEnd" class="iface-list-end" aria-live="polite">You have reached the end</div>
<div id="interfacesScrollSentinel" class="scroll-sentinel"></div>
</div>
<div id="interfacesPagination" class="hidden"></div>
</section>
<aside class="interfaces-sidebar flex flex-col gap-4 min-h-0">
<section class="theme-card admin-panel interfaces-summary-panel">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Overview</h3>
<p class="admin-panel-subtitle" id="interfacesStatusLine">Loading…</p>
</div>
<dl class="iface-stat-grid" id="interfacesStatGrid">
<div class="iface-stat">
<dt>Mappings</dt>
<dd id="ifaceStatMappings"></dd>
</div>
<div class="iface-stat">
<dt>On system</dt>
<dd id="ifaceStatConfigured"></dd>
</div>
<div class="iface-stat">
<dt>Tunnel links</dt>
<dd id="ifaceStatConnections"></dd>
</div>
<div class="iface-stat">
<dt>Clients</dt>
<dd id="ifaceStatClients"></dd>
</div>
</dl>
</section>
<section class="theme-card admin-panel interfaces-subnets-panel flex flex-col min-h-0">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Subnets</h3>
<p class="admin-panel-subtitle">Capacity across configured ranges</p>
</div>
<div id="interfacesSubnetsList" class="iface-subnets-list flex-1 min-h-0 overflow-y-auto">
<p class="iface-subnets-empty theme-text-tertiary">No subnet configuration</p>
</div>
<div class="admin-panel-footer">
<button type="button" onclick="if(window.navigateToTab) window.navigateToTab('settings'); else location.hash='settings';" class="admin-btn admin-btn--secondary admin-btn--sm w-full">
<i class="fas fa-cog" aria-hidden="true"></i> Subnet settings
</button>
</div>
</section>
<section class="theme-card admin-panel interfaces-actions-panel">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Maintenance</h3>
<p class="admin-panel-subtitle">Stop tunnels and remove virtual IPs from the loopback interface.</p>
</div>
<button type="button" onclick="cleanupInterfaces()" class="admin-btn admin-btn--danger w-full">
<i class="fas fa-broom" aria-hidden="true"></i> Cleanup interfaces
</button>
</section>
</aside>
</div>
</div>
<div id="logs" class="tab-content hidden admin-tab-panel--fixed flex flex-col">
<h2 class="text-2xl font-bold mb-4 flex items-center gap-2 flex-shrink-0">
Logs
<button onclick="openInfoModal('logs')" class="text-sm px-3 py-1 theme-button-info rounded transition-colors">Info</button>
</h2>
<div class="flex flex-wrap items-center gap-3 mb-3 flex-shrink-0">
<label for="log-channel-select" class="text-sm theme-text-secondary">Log file</label>
<select id="log-channel-select" onchange="onLogChannelChange()" class="theme-input log-channel-select rounded-lg px-3 py-2 text-sm min-w-[12rem]">
<option value="core">Core</option>
<option value="proxy">Internal Proxy</option>
<option value="httpProxy">HTTP Proxy</option>
<option value="dns">DNS</option>
<option value="plugins">Plugins</option>
<option value="holesail">Holesail</option>
</select>
<div class="flex-1 min-w-[16rem] max-w-2xl flex items-center gap-2">
<div class="relative flex-1">
<input
id="log-filter-input"
type="search"
autocomplete="off"
spellcheck="false"
placeholder="Filter lines… text or /regex/flags"
class="w-full theme-input log-filter-input rounded-lg py-2 pl-3 pr-9 text-sm"
aria-label="Filter log lines"
oninput="onLogFilterInput()"
onkeydown="if (event.key === 'Escape') { clearLogFilter(); event.preventDefault(); }"
/>
<button
id="log-filter-clear"
type="button"
class="hidden absolute right-1.5 top-1/2 -translate-y-1/2 p-1.5 rounded-md theme-text-tertiary hover:theme-text-primary hover:bg-white/10 transition-colors"
title="Clear filter (Esc)"
aria-label="Clear filter"
onclick="clearLogFilter()"
>
<i class="fas fa-times text-sm" aria-hidden="true"></i>
</button>
</div>
<span id="log-filter-stats" class="text-xs theme-text-tertiary tabular-nums whitespace-nowrap min-w-[5rem] text-right"></span>
</div>
<span class="text-xs theme-text-tertiary hidden lg:inline">Files under <code class="text-indigo-300">logs/</code> — live tail</span>
</div>
<div id="terminal" class="log-terminal bg-black rounded-lg overflow-hidden flex-1 min-h-0"></div>
</div>
<div id="host" class="tab-content hidden flex flex-col admin-tab-panel--fixed">
<h2 class="text-2xl font-bold mb-4 flex items-center gap-2 flex-shrink-0">
Holesail
</h2>
<div class="admin-subtabs flex gap-2 mb-4 flex-shrink-0">
<button onclick="showSubTab('host', 'servers')" id="host-subtab-servers" class="admin-subtab-btn admin-subtab-btn--active px-4 py-2 rounded transition-colors">Servers</button>
<button onclick="showSubTab('host', 'clients')" id="host-subtab-clients" class="admin-subtab-btn px-4 py-2 rounded transition-colors">Clients</button>
</div>
<!-- Servers Sub-tab -->
<div id="host-servers" class="sub-tab-content flex flex-col flex-1 min-h-0">
<div class="mb-4 flex items-center gap-2 flex-shrink-0">
<h3 class="text-xl font-bold">Holesail Servers</h3>
<button onclick="openInfoModal('holesail-servers')" class="text-sm px-3 py-1 theme-button-info rounded transition-colors">Info</button>
</div>
<div class="mb-6 flex-shrink-0">
<input id="search-holesail" type="text" placeholder="Search servers..." class="w-full p-3 rounded-lg theme-input focus:outline-none focus:ring-2 focus:ring-primary" oninput="filterHolesailServers()">
</div>
<div class="overflow-x-auto overflow-y-auto flex-1 min-h-0">
<table class="w-full theme-table rounded-lg">
<thead class="theme-table thead sticky top-0 z-10">
<tr>
<th class="p-3 text-left">Name/ID</th>
<th class="p-3 text-left">Port</th>
<th class="p-3 text-left">Host</th>
<th class="p-3 text-left">URL</th>
<th class="p-3 text-left">Protocol</th>
<th class="p-3 text-left">Status</th>
<th class="p-3 text-left">Actions</th>
</tr>
</thead>
<tbody id="holesailTable"></tbody>
</table>
<div id="holesailScrollSentinel" class="scroll-sentinel"></div>
</div>
<div id="holesailPagination" class="flex justify-center mt-4 space-x-2 flex-shrink-0"></div>
<div class="mt-4 flex justify-end flex-shrink-0">
<button onclick="openCreateHolesailModal()" class="px-6 py-2.5 bg-primary text-white rounded-lg hover:bg-primary-hover transition-all font-medium shadow-md hover:shadow-lg" style="min-width: auto; width: auto;">Create Server</button>
</div>
</div>
<!-- Clients Sub-tab -->
<div id="host-clients" class="sub-tab-content hidden flex flex-col flex-1 min-h-0">
<div class="mb-4 flex items-center gap-2 flex-shrink-0">
<h3 class="text-xl font-bold">Holesail Clients</h3>
<button onclick="openInfoModal('holesail-clients')" class="text-sm px-3 py-1 theme-button-info rounded transition-colors">Info</button>
</div>
<div class="mb-6 flex-shrink-0">
<input id="search-holesail-clients" type="text" placeholder="Search clients..." class="w-full p-3 rounded-lg theme-input focus:outline-none focus:ring-2 focus:ring-primary" oninput="filterHolesailClients()">
</div>
<div class="overflow-x-auto overflow-y-auto flex-1 min-h-0">
<table class="w-full theme-table rounded-lg">
<thead class="theme-table thead sticky top-0 z-10">
<tr>
<th class="p-3 text-left">Domain</th>
<th class="p-3 text-left">Key</th>
<th class="p-3 text-left">Port</th>
<th class="p-3 text-left">Protocol</th>
<th class="p-3 text-left">Status</th>
<th class="p-3 text-left">Actions</th>
</tr>
</thead>
<tbody id="holesailClientsTable"></tbody>
</table>
<div id="holesailClientsScrollSentinel" class="scroll-sentinel"></div>
</div>
<div id="holesailClientsPagination" class="flex justify-center mt-4 space-x-2 flex-shrink-0"></div>
<div class="flex gap-3 mt-4 justify-end flex-shrink-0">
<button onclick="openCreateClientModal()" class="px-6 py-2.5 bg-primary text-white rounded-lg hover:bg-primary-hover transition-all font-medium shadow-md hover:shadow-lg" style="min-width: auto; width: auto;">Create Client</button>
<button onclick="openServiceSubscriptionModal()" class="px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-all font-medium shadow-md hover:shadow-lg" style="min-width: auto; width: auto;">Service Subscription</button>
</div>
</div>
</div>
<div id="settings" class="tab-content hidden admin-tab-panel--scroll">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold flex items-center gap-2">
Settings
<button onclick="openInfoModal('settings')" class="text-sm px-3 py-1 theme-button-info rounded transition-colors">Info</button>
</h2>
<button id="save-settings-btn" onclick="saveSettings(this)" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Save Settings</button>
</div>
<div class="mb-6">
<input id="search-settings" type="text" placeholder="Search settings..." class="w-full p-3 rounded-lg theme-input focus:outline-none focus:ring-2 focus:ring-primary" oninput="filterSettings()">
</div>
<div id="settingsContainer" class="space-y-6"></div>
</div>
<div id="plugins" class="tab-content hidden admin-tab-panel--scroll">
<h2 class="text-2xl font-bold mb-4 flex items-center gap-2">
Plugins
<button onclick="openInfoModal('plugins')" class="text-sm px-3 py-1 theme-button-info rounded transition-colors">Info</button>
</h2>
<div class="mb-6">
<input id="search-plugins" type="text" placeholder="Search plugins..." class="w-full p-3 rounded-lg theme-input focus:outline-none focus:ring-2 focus:ring-primary" oninput="filterPlugins()">
</div>
<div id="pluginsContainer" class="space-y-6"></div>
</div>
<div id="backups" class="tab-content hidden flex flex-col admin-tab-panel--fixed">
<header class="admin-tab-header flex-shrink-0">
<div class="admin-tab-header-main">
<h2 class="admin-tab-title">Backup &amp; Restore</h2>
<p class="admin-tab-description">Create snapshots of configuration and data, restore previous states, and manage retention on this node.</p>
</div>
<button type="button" onclick="openInfoModal('backups')" class="theme-button-info text-sm px-3 py-1 rounded">Info</button>
</header>
<div class="backups-layout flex-1 min-h-0 grid grid-cols-1 lg:grid-cols-3 gap-4">
<section class="theme-card admin-panel backups-panel lg:col-span-2 flex flex-col min-h-0">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Backup archive</h3>
<p class="admin-panel-subtitle" id="backupsCount">Loading…</p>
</div>
<div class="admin-toolbar flex-shrink-0">
<label class="admin-search-field">
<i class="fas fa-magnifying-glass admin-search-icon" aria-hidden="true"></i>
<input id="search-backups" type="search" placeholder="Search by name, date, or version…" class="theme-input admin-search-input" oninput="filterBackups()" autocomplete="off">
</label>
</div>
<div class="backups-table-scroll flex-1 min-h-0 overflow-auto">
<table class="w-full theme-table backups-table">
<thead class="theme-table thead sticky top-0 z-10">
<tr>
<th class="p-3 text-left">Name</th>
<th class="p-3 text-left w-[11rem]">Timestamp</th>
<th class="p-3 text-left w-[6rem]">Size</th>
<th class="p-3 text-left w-[4.5rem]">Files</th>
<th class="p-3 text-left w-[5.5rem]">Version</th>
<th class="p-3 text-right w-[8.5rem]"><span class="sr-only">Actions</span></th>
</tr>
</thead>
<tbody id="backupsTable"></tbody>
</table>
<div id="backupsEmpty" class="local-dns-empty">
<i class="fas fa-database local-dns-empty-icon" aria-hidden="true"></i>
<p class="local-dns-empty-title">No backups yet</p>
<p class="local-dns-empty-text theme-text-tertiary">Create a manual backup or wait for the next scheduled snapshot.</p>
</div>
<div id="backupsFilteredEmpty" class="local-dns-empty">
<i class="fas fa-filter local-dns-empty-icon" aria-hidden="true"></i>
<p class="local-dns-empty-title">No matching backups</p>
<p class="local-dns-empty-text theme-text-tertiary">Try a different search term.</p>
</div>
<div id="backupsEnd" class="local-dns-list-end" aria-live="polite">You have reached the end</div>
<div id="backupsScrollSentinel" class="scroll-sentinel"></div>
</div>
<div id="backupsPagination" class="hidden"></div>
<div class="admin-panel-footer flex justify-end flex-shrink-0">
<button type="button" id="create-backup-btn" onclick="createBackup(this)" class="admin-btn admin-btn--primary">
<i class="fas fa-plus" aria-hidden="true"></i> Create backup
</button>
</div>
</section>
<aside class="backups-sidebar flex flex-col gap-4 min-h-0">
<section class="theme-card admin-panel backups-summary-panel">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Overview</h3>
<p class="admin-panel-subtitle" id="backupsOverviewLine"></p>
</div>
<dl class="local-dns-stat-grid">
<div class="local-dns-stat">
<dt>Total</dt>
<dd id="backupsStatTotal"></dd>
</div>
<div class="local-dns-stat">
<dt>Total size</dt>
<dd id="backupsStatSize"></dd>
</div>
<div class="local-dns-stat">
<dt>Latest</dt>
<dd id="backupsStatLatest" class="backups-stat-latest"></dd>
</div>
<div class="local-dns-stat">
<dt>Files (latest)</dt>
<dd id="backupsStatFiles"></dd>
</div>
</dl>
</section>
<section class="theme-card admin-panel backups-note-panel">
<div class="admin-panel-header">
<h3 class="admin-panel-title">About backups</h3>
</div>
<p class="local-dns-note-text theme-text-tertiary">Automatic backups run on a schedule configured in <button type="button" class="backups-settings-link" onclick="if(window.navigateToTab) window.navigateToTab('settings'); else location.hash = 'settings';">Settings</button>. Old archives are pruned based on retention limits.</p>
</section>
</aside>
</div>
</div>
<div id="stats" class="tab-content hidden admin-tab-panel--scroll stats-tab">
<header class="admin-tab-header stats-tab-header flex-shrink-0">
<div class="admin-tab-header-main">
<h2 class="admin-tab-title">Statistics &amp; Metrics</h2>
<p class="admin-tab-description">Live health, network metrics, resource usage, storage stats, and network diagnostics.</p>
</div>
<button type="button" onclick="openInfoModal('stats')" class="theme-button-info text-sm px-3 py-1 rounded">Info</button>
</header>
<section class="theme-card admin-panel stats-toolbar">
<div class="stats-toolbar-row">
<label class="stats-toolbar-check">
<input type="checkbox" id="auto-refresh-stats" checked>
<span>Auto-refresh</span>
</label>
<select id="refresh-interval-selector" class="theme-input stats-toolbar-select" aria-label="Refresh interval">
<option value="1000">Realtime (1s)</option>
<option value="2000" selected>Fast (2s)</option>
<option value="5000">Normal (5s)</option>
<option value="10000">Slow (10s)</option>
<option value="30000">Very Slow (30s)</option>
</select>
<select id="time-range-selector" class="theme-input stats-toolbar-select" aria-label="Historical time range">
<option value="1">Last 1 minute</option>
<option value="5" selected>Last 5 minutes</option>
<option value="15">Last 15 minutes</option>
<option value="30">Last 30 minutes</option>
<option value="60">Last 1 hour</option>
<option value="360">Last 6 hours</option>
<option value="1440">Last 24 hours</option>
<option value="2880">Last 48 hours</option>
</select>
<button type="button" onclick="exportStats()" class="admin-btn admin-btn--secondary">
<i class="fas fa-download" aria-hidden="true"></i> Export data
</button>
</div>
</section>
<div class="stats-sections">
<section class="theme-card admin-panel stats-section">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Health overview</h3>
<p class="admin-panel-subtitle">Overall node health and uptime</p>
</div>
<div class="stats-metric-grid stats-metric-grid--2">
<div class="stats-metric">
<span class="stats-metric-label">Overall status</span>
<p id="health-status" class="stats-metric-value stats-health-status">-</p>
</div>
<div class="stats-metric">
<span class="stats-metric-label">System uptime</span>
<p id="health-uptime" class="stats-metric-value">-</p>
</div>
</div>
</section>
<section class="theme-card admin-panel stats-section">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Service status</h3>
<p class="admin-panel-subtitle">DNS, proxy, swarm, and corestore health</p>
</div>
<div id="health-services" class="stats-service-grid"></div>
</section>
<section class="theme-card admin-panel stats-section">
<div class="admin-panel-header">
<h3 class="admin-panel-title">System overview</h3>
<p class="admin-panel-subtitle">Node role, connectivity, peers, and domains</p>
</div>
<div class="stats-metric-grid stats-metric-grid--3">
<div class="stats-metric">
<span class="stats-metric-label">Node type</span>
<p id="node-type-display" class="stats-metric-value">-</p>
<span id="connection-status" class="stats-status-pill stats-status-pill--muted"></span>
</div>
<div class="stats-metric">
<span class="stats-metric-label">Connected peers</span>
<p id="peers-current" class="stats-metric-value">-</p>
<span id="peers-status" class="stats-status-pill stats-status-pill--muted"></span>
</div>
<div class="stats-metric">
<span class="stats-metric-label">Total domains</span>
<p id="domains-current" class="stats-metric-value">-</p>
</div>
</div>
</section>
<section class="theme-card admin-panel stats-section">
<div class="admin-panel-header">
<h3 class="admin-panel-title">DNS statistics</h3>
<p class="admin-panel-subtitle">Query volume, success rate, and resolution performance</p>
<span id="dns-status" class="stats-status-pill stats-status-pill--muted"></span>
</div>
<div class="stats-metric-grid stats-metric-grid--4">
<div class="stats-metric"><span class="stats-metric-label">Total queries</span><p id="dns-queries-total" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Success rate</span><p id="dns-success-rate" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Avg response</span><p id="dns-avg-response" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">P2P rate</span><p id="dns-p2p-rate" class="stats-metric-value">-</p></div>
</div>
<div class="stats-chart-grid stats-chart-grid--2">
<div class="stats-chart-wrap"><canvas id="dns-queries-chart"></canvas></div>
<div class="stats-chart-wrap"><canvas id="dns-types-chart"></canvas></div>
</div>
<div class="stats-subsection">
<h4 class="stats-subsection-title">Top queried domains</h4>
<div id="top-domains-list" class="stats-list"></div>
</div>
</section>
<section class="theme-card admin-panel stats-section">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Network &amp; peers</h3>
<p class="admin-panel-subtitle">Peer connection metrics and events</p>
</div>
<div class="stats-metric-grid stats-metric-grid--3">
<div class="stats-metric"><span class="stats-metric-label">Connected</span><p id="peers-connected-count" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Total connections</span><p id="peers-total-connections" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Avg duration</span><p id="peers-avg-duration" class="stats-metric-value">-</p></div>
</div>
<div class="stats-chart-wrap stats-chart-wrap--tall"><canvas id="peer-events-chart"></canvas></div>
</section>
<section class="theme-card admin-panel stats-section">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Holesail statistics</h3>
<p class="admin-panel-subtitle">Tunnel clients, servers, and session duration</p>
<span id="holesail-status" class="stats-status-pill stats-status-pill--muted"></span>
</div>
<div class="stats-metric-grid stats-metric-grid--4">
<div class="stats-metric"><span class="stats-metric-label">Active connections</span><p id="holesail-active" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Clients started</span><p id="holesail-clients-started" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Servers started</span><p id="holesail-servers-started" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Avg duration</span><p id="holesail-avg-duration" class="stats-metric-value">-</p></div>
</div>
<div class="stats-chart-grid stats-chart-grid--2">
<div class="stats-chart-wrap"><canvas id="holesail-events-chart"></canvas></div>
<div class="stats-chart-wrap"><canvas id="holesail-protocol-chart"></canvas></div>
</div>
</section>
<section class="theme-card admin-panel stats-section">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Holesail children</h3>
<p class="admin-panel-subtitle">Running Holesail child processes</p>
</div>
<div id="holesail-children-container" class="stats-detail-stack">
<p class="stats-empty-note theme-text-tertiary">Loading children data…</p>
</div>
</section>
<section class="theme-card admin-panel stats-section">
<div class="admin-panel-header">
<h3 class="admin-panel-title">API &amp; requests</h3>
<p class="admin-panel-subtitle">Admin API traffic and endpoint usage</p>
</div>
<div class="stats-metric-grid stats-metric-grid--4">
<div class="stats-metric"><span class="stats-metric-label">Total requests</span><p id="requests-total" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Success rate</span><p id="requests-success-rate" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Avg response</span><p id="requests-avg-response" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Failed requests</span><p id="requests-failed" class="stats-metric-value">-</p></div>
</div>
<div class="stats-chart-grid stats-chart-grid--2">
<div class="stats-chart-wrap"><canvas id="requests-timeline-chart"></canvas></div>
<div class="stats-chart-wrap"><canvas id="endpoint-usage-chart"></canvas></div>
</div>
</section>
<section class="theme-card admin-panel stats-section">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Resource usage</h3>
<p class="admin-panel-subtitle">Sockets, servers, tunnels, and plugin RPC channels</p>
</div>
<div class="stats-metric-grid stats-metric-grid--4">
<div class="stats-metric"><span class="stats-metric-label">Active sockets</span><p id="resources-sockets" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Active servers</span><p id="resources-servers" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Holesail connections</span><p id="resources-holesails" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Plugin RPC</span><p id="resources-channels" class="stats-metric-value">-</p></div>
</div>
<div class="stats-chart-wrap stats-chart-wrap--tall"><canvas id="resources-timeline-chart"></canvas></div>
</section>
<section class="theme-card admin-panel stats-section">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Core RPC</h3>
<p class="admin-panel-subtitle">Control plane on <code class="stats-inline-code">p2ns.core-request-rpc</code> — invite flow via <code class="stats-inline-code">invite.request</code>, <code class="stats-inline-code">invite.deliver</code>, <code class="stats-inline-code">invite.ack</code></p>
</div>
<div class="stats-metric-grid stats-metric-grid--6">
<div class="stats-metric"><span class="stats-metric-label">DNS pass</span><p id="core-dns-pass" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">RPC open</span><p id="core-rpc-open" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Request ch.</span><p id="core-request-open" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Pending ACK</span><p id="core-pending-ack" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Failed peers</span><p id="core-failed-peers" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Invite failures</span><p id="core-consecutive-failures" class="stats-metric-value">-</p></div>
</div>
<p id="core-protocol-summary" class="stats-protocol-summary hidden"></p>
<div id="core-details" class="stats-detail-stack"><p class="stats-empty-note theme-text-tertiary">Loading core RPC data…</p></div>
<div id="core-recommendations" class="stats-recommendations hidden"></div>
</section>
<section class="theme-card admin-panel stats-section">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Plugin RPC</h3>
<p class="admin-panel-subtitle">protomux-rpc protocols registered by plugins</p>
</div>
<div class="stats-metric-grid stats-metric-grid--4">
<div class="stats-metric"><span class="stats-metric-label">Total plugins</span><p id="peer-channels-total-plugins" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Total protocols</span><p id="peer-channels-total-protocols" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">RPC open</span><p id="peer-channels-open" class="stats-metric-value stats-metric-value--ok">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Methods</span><p id="peer-channels-methods" class="stats-metric-value">-</p></div>
</div>
<div id="peer-channels-details" class="stats-detail-stack"><p class="stats-empty-note theme-text-tertiary">Loading plugin RPC data…</p></div>
</section>
<section class="theme-card admin-panel stats-section">
<div class="admin-panel-header">
<h3 class="admin-panel-title">HyperDB databases</h3>
<p class="admin-panel-subtitle">Plugin database stores and replication</p>
</div>
<div class="stats-metric-grid stats-metric-grid--3">
<div class="stats-metric"><span class="stats-metric-label">Total stores</span><p id="hyperdb-total-stores" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Open databases</span><p id="hyperdb-total-databases" class="stats-metric-value stats-metric-value--ok">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Replication active</span><p id="hyperdb-replication-active" class="stats-metric-value">-</p></div>
</div>
<div id="hyperdb-details" class="stats-detail-stack"><p class="stats-empty-note theme-text-tertiary">Loading HyperDB data…</p></div>
</section>
<section class="theme-card admin-panel stats-section">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Hyperdrive storage</h3>
<p class="admin-panel-subtitle">Drive instances and replication state</p>
</div>
<div class="stats-metric-grid stats-metric-grid--3">
<div class="stats-metric"><span class="stats-metric-label">Total drives</span><p id="hyperdrive-total-drives" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Replication active</span><p id="hyperdrive-replication-active" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Writable drives</span><p id="hyperdrive-writable" class="stats-metric-value stats-metric-value--ok">-</p></div>
</div>
<div id="hyperdrive-details" class="stats-detail-stack"><p class="stats-empty-note theme-text-tertiary">Loading Hyperdrive data…</p></div>
</section>
<section class="theme-card admin-panel stats-section">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Domain management</h3>
<p class="admin-panel-subtitle">Domain add/remove activity over time</p>
</div>
<div class="stats-metric-grid stats-metric-grid--3">
<div class="stats-metric"><span class="stats-metric-label">Total added</span><p id="domains-added" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Total removed</span><p id="domains-removed" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Net change</span><p id="domains-net" class="stats-metric-value">-</p></div>
</div>
<div class="stats-chart-wrap stats-chart-wrap--tall"><canvas id="domain-events-chart"></canvas></div>
</section>
<section class="theme-card admin-panel stats-section">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Process usage</h3>
<p class="admin-panel-subtitle">Node.js runtime, memory, CPU, and event loop metrics</p>
</div>
<div class="stats-metric-grid stats-metric-grid--4 stats-metric-grid--info">
<div class="stats-metric stats-metric--compact"><span class="stats-metric-label">Node.js</span><p id="process-node-version" class="stats-metric-value stats-metric-value--sm">-</p></div>
<div class="stats-metric stats-metric--compact"><span class="stats-metric-label">Platform</span><p id="process-platform" class="stats-metric-value stats-metric-value--sm">-</p></div>
<div class="stats-metric stats-metric--compact"><span class="stats-metric-label">Architecture</span><p id="process-arch" class="stats-metric-value stats-metric-value--sm">-</p></div>
<div class="stats-metric stats-metric--compact"><span class="stats-metric-label">Process uptime</span><p id="process-uptime" class="stats-metric-value stats-metric-value--sm">-</p></div>
</div>
<div class="stats-subsection">
<h4 class="stats-subsection-title">Memory usage</h4>
<div class="stats-metric-grid stats-metric-grid--5">
<div class="stats-metric"><span class="stats-metric-label">Heap used</span><p id="process-memory-heap-used" class="stats-metric-value stats-metric-value--md">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Heap total</span><p id="process-memory-heap-total" class="stats-metric-value stats-metric-value--md">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">RSS</span><p id="process-memory-rss" class="stats-metric-value stats-metric-value--md">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">External</span><p id="process-memory-external" class="stats-metric-value stats-metric-value--md">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Array buffers</span><p id="process-memory-array-buffers" class="stats-metric-value stats-metric-value--md">-</p></div>
</div>
<div class="stats-chart-wrap stats-chart-wrap--tall"><canvas id="process-memory-chart"></canvas></div>
</div>
<div class="stats-subsection">
<h4 class="stats-subsection-title">CPU usage</h4>
<div class="stats-metric-grid stats-metric-grid--3">
<div class="stats-metric"><span class="stats-metric-label">Current CPU %</span><p id="process-cpu-current" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Average CPU %</span><p id="process-cpu-avg" class="stats-metric-value">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Active handles</span><p id="process-handles-active" class="stats-metric-value">-</p></div>
</div>
<div class="stats-chart-wrap stats-chart-wrap--tall"><canvas id="process-cpu-chart"></canvas></div>
</div>
<div class="stats-subsection">
<h4 class="stats-subsection-title">System resources</h4>
<div class="stats-metric-grid stats-metric-grid--4">
<div class="stats-metric"><span class="stats-metric-label">Total memory</span><p id="process-system-total-memory" class="stats-metric-value stats-metric-value--md">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Free memory</span><p id="process-system-free-memory" class="stats-metric-value stats-metric-value--md">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Used memory %</span><p id="process-system-used-percent" class="stats-metric-value stats-metric-value--md">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Load average</span><p id="process-system-load-avg" class="stats-metric-value stats-metric-value--md">-</p></div>
</div>
<div class="stats-chart-wrap stats-chart-wrap--tall"><canvas id="process-system-chart"></canvas></div>
</div>
<div class="stats-subsection">
<h4 class="stats-subsection-title">Event loop performance</h4>
<div class="stats-metric-grid stats-metric-grid--3">
<div class="stats-metric"><span class="stats-metric-label">Current lag</span><p id="process-eventloop-current" class="stats-metric-value stats-metric-value--md">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Average lag</span><p id="process-eventloop-avg" class="stats-metric-value stats-metric-value--md">-</p></div>
<div class="stats-metric"><span class="stats-metric-label">Max lag</span><p id="process-eventloop-max" class="stats-metric-value stats-metric-value--md">-</p></div>
</div>
<div class="stats-chart-wrap stats-chart-wrap--tall"><canvas id="process-eventloop-chart"></canvas></div>
</div>
</section>
<section class="theme-card admin-panel stats-section">
<div class="admin-panel-header">
<h3 class="admin-panel-title">Network diagnostics</h3>
<p class="admin-panel-subtitle">DNS lookup, ping, traceroute, connection tests, and invite diagnostics</p>
<button type="button" onclick="openInfoModal('diagnostics')" class="theme-button-info text-sm px-3 py-1 rounded stats-section-info-btn">Info</button>
</div>
<div class="stats-diag-grid">
<div class="stats-diag-card">
<h4 class="stats-diag-title">DNS lookup</h4>
<div class="stats-diag-form">
<input id="dns-lookup-domain" type="text" placeholder="Domain name" class="theme-input stats-diag-input" autocomplete="off">
<select id="dns-lookup-type" class="theme-input stats-diag-input">
<option value="A">A</option><option value="AAAA">AAAA</option><option value="MX">MX</option><option value="TXT">TXT</option><option value="NS">NS</option><option value="CNAME">CNAME</option><option value="SRV">SRV</option><option value="PTR">PTR</option><option value="SOA">SOA</option>
</select>
<button type="button" onclick="runDnsLookup()" class="admin-btn admin-btn--primary w-full">Lookup</button>
</div>
</div>
<div class="stats-diag-card">
<h4 class="stats-diag-title">Ping</h4>
<div class="stats-diag-form">
<input id="ping-target" type="text" placeholder="Target (domain or IP)" class="theme-input stats-diag-input" autocomplete="off">
<input id="ping-count" type="number" placeholder="Count (default: 4)" value="4" min="1" max="10" class="theme-input stats-diag-input">
<button type="button" onclick="runPing()" class="admin-btn admin-btn--primary w-full">Ping</button>
</div>
</div>
<div class="stats-diag-card">
<h4 class="stats-diag-title">Traceroute</h4>
<div class="stats-diag-form">
<input id="traceroute-target" type="text" placeholder="Target (domain or IP)" class="theme-input stats-diag-input" autocomplete="off">
<button type="button" onclick="runTraceroute()" class="admin-btn admin-btn--primary w-full">Traceroute</button>
</div>
</div>
<div class="stats-diag-card">
<h4 class="stats-diag-title">Connection test</h4>
<div class="stats-diag-form">
<input id="connection-domain" type="text" placeholder="Domain" class="theme-input stats-diag-input" autocomplete="off">
<input id="connection-port" type="number" placeholder="Port" min="1" max="65535" class="theme-input stats-diag-input">
<button type="button" onclick="testConnection()" class="admin-btn admin-btn--primary w-full">Test connection</button>
</div>
</div>
<div class="stats-diag-card stats-diag-card--wide">
<h4 class="stats-diag-title">Invite diagnostics</h4>
<p class="stats-diag-desc theme-text-tertiary">Diagnose RPC invite flow, peer connections, pending acks, and master queue state.</p>
<div class="stats-diag-actions">
<button type="button" onclick="runInviteDiagnostics()" class="admin-btn admin-btn--primary flex-1">Run invite diagnostics</button>
<button type="button" onclick="cleanDnsPassStorage()" class="admin-btn admin-btn--danger">Clean &amp; restart</button>
</div>
</div>
</div>
<div class="stats-subsection">
<h4 class="stats-subsection-title">Results</h4>
<div id="diagnostics-results" class="stats-diag-results"></div>
</div>
</section>
</div>
</div>
</div>
</div>
</div>
<dialog id="addDomainModal" class="local-dns-modal theme-glass rounded-lg shadow-xl w-full max-w-md p-0 border-0">
<div class="local-dns-modal-inner">
<div class="local-dns-modal-header">
<div>
<h3 class="local-dns-modal-title">Add domain</h3>
<p class="local-dns-modal-subtitle theme-text-tertiary">Register a local claim on the P2NS network.</p>
</div>
<button type="button" onclick="document.getElementById('addDomainModal').close()" class="admin-btn admin-btn--secondary admin-btn--sm" aria-label="Close">
<i class="fas fa-xmark" aria-hidden="true"></i>
</button>
</div>
<div class="local-dns-modal-body">
<label class="local-dns-form-field">
<span class="local-dns-form-label">Domain</span>
<input id="modal-domain" placeholder="example.tld" class="theme-input w-full">
</label>
<label class="local-dns-form-field">
<span class="local-dns-form-label">Holesail hash</span>
<input id="modal-hash" placeholder="hs://…" class="theme-input w-full">
</label>
<label class="local-dns-form-field domains-ssl-field">
<span class="local-dns-form-label">TLS</span>
<label class="domains-ssl-toggle">
<input id="modal-ssl" type="checkbox" class="sr-only">
<span class="plugin-toggle-switch" aria-hidden="true"></span>
<span class="domains-ssl-label">This Holesail connection uses SSL/TLS</span>
</label>
</label>
</div>
<div class="local-dns-modal-footer">
<button type="button" onclick="document.getElementById('addDomainModal').close()" class="admin-btn admin-btn--secondary">Cancel</button>
<button type="button" onclick="submitAddDomain()" class="admin-btn admin-btn--primary">Add domain</button>
</div>
</div>
</dialog>
<dialog id="localDnsModal" class="local-dns-modal theme-glass rounded-lg shadow-xl w-full max-w-md p-0 border-0">
<div class="local-dns-modal-inner">
<div class="local-dns-modal-header">
<div>
<h3 id="local-dns-title" class="local-dns-modal-title">Add Local DNS Record</h3>
<p class="local-dns-modal-subtitle theme-text-tertiary">Overrides apply on this node only.</p>
</div>
<button type="button" onclick="document.getElementById('localDnsModal').close()" class="admin-btn admin-btn--secondary admin-btn--sm" aria-label="Close">
<i class="fas fa-xmark" aria-hidden="true"></i>
</button>
</div>
<div class="local-dns-modal-body">
<label class="local-dns-form-field">
<span class="local-dns-form-label">Name</span>
<input id="local-name" placeholder="example.tld or host.example.tld" class="theme-input w-full">
</label>
<label class="local-dns-form-field">
<span class="local-dns-form-label">Type</span>
<select id="local-type" onchange="updateLocalForm()" class="theme-input w-full">
<option value="A">A</option>
<option value="AAAA">AAAA</option>
<option value="CNAME">CNAME</option>
<option value="MX">MX</option>
<option value="TXT">TXT</option>
<option value="SRV">SRV</option>
<option value="SOA">SOA</option>
<option value="CAA">CAA</option>
<option value="NS">NS</option>
<option value="PTR">PTR</option>
<option value="OTHER">OTHER</option>
</select>
</label>
<div id="local-value-fields" class="local-dns-dynamic-fields"></div>
<label class="local-dns-form-field">
<span class="local-dns-form-label">TTL (seconds)</span>
<input id="local-ttl" type="number" placeholder="3600" value="3600" class="theme-input w-full" min="0">
</label>
</div>
<div class="local-dns-modal-footer">
<button type="button" onclick="document.getElementById('localDnsModal').close()" class="admin-btn admin-btn--secondary">Cancel</button>
<button type="button" id="local-submit" onclick="submitLocalDns(this)" class="admin-btn admin-btn--primary">Add</button>
</div>
</div>
</dialog>
<dialog id="certDetailsModal" class="p-6 theme-glass rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
<div class="flex justify-between items-start gap-4 mb-4">
<div>
<h3 class="text-lg font-semibold theme-text-primary">Certificate details</h3>
<p id="cert-details-domain" class="text-sm theme-text-tertiary mt-1 font-mono break-all"></p>
</div>
<button type="button" onclick="document.getElementById('certDetailsModal').close()" class="admin-btn admin-btn--secondary admin-btn--sm cert-modal-close" aria-label="Close">
<i class="fas fa-xmark" aria-hidden="true"></i>
</button>
</div>
<div class="flex-1 overflow-y-auto mb-4 min-h-0">
<pre id="cert-details-content" class="cert-details-pre whitespace-pre-wrap break-all text-sm font-mono theme-text-primary leading-relaxed"></pre>
</div>
<div class="flex justify-end gap-2 pt-4 border-t" style="border-color: var(--border-color);">
<button type="button" onclick="copyCertDetails()" class="admin-btn admin-btn--secondary">
<i class="fas fa-copy" aria-hidden="true"></i> Copy
</button>
<button type="button" onclick="document.getElementById('certDetailsModal').close()" class="admin-btn admin-btn--primary">Close</button>
</div>
</dialog>
<!-- Confirmation modal is created dynamically by confirmation-modal.js -->
<dialog id="createHolesailModal" class="p-6 theme-glass rounded-lg shadow-xl w-full max-w-md">
<h3 class="text-xl font-bold mb-4">Create Holesail Server</h3>
<input id="holesail-name" placeholder="Name (optional)" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<input id="holesail-port" type="number" placeholder="Port (required)" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<input id="holesail-host" placeholder="Host (default 0.0.0.0)" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<input id="holesail-key" placeholder="Key (optional)" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<input id="holesail-domain" placeholder="Assign to domain (optional)" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<label class="flex items-center mb-4"><input id="holesail-secure" type="checkbox" class="mr-2"> Secure</label>
<label class="flex items-center mb-4"><input id="holesail-udp" type="checkbox" checked class="mr-2"> UDP</label>
<select id="holesail-log" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<option value="false">No Logs</option>
<option value="0">Debug</option>
<option value="1" selected>Info</option>
<option value="2">Warn</option>
<option value="3">Error</option>
</select>
<div class="flex justify-end space-x-2">
<button onclick="document.getElementById('createHolesailModal').close()" class="px-4 py-2 theme-button-info theme-text-primary rounded theme-glass-hover">Cancel</button>
<button id="create-holesail-btn" onclick="submitCreateHolesail()" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Create</button>
</div>
</dialog>
<dialog id="createClientModal" class="p-6 theme-glass rounded-lg shadow-xl w-full max-w-md">
<h3 class="text-xl font-bold mb-4">Create Holesail Client</h3>
<select id="client-domain" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"></select>
<input id="client-service-name" placeholder="Service Name (required, e.g., web, api)" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<input id="client-key" placeholder="Key (required)" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<input id="client-port" type="number" placeholder="Local Port (required)" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<select id="client-protocol" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<option value="tcp" selected>TCP</option>
<option value="udp">UDP</option>
</select>
<div class="flex justify-end space-x-2">
<button onclick="document.getElementById('createClientModal').close()" class="px-4 py-2 theme-button-info theme-text-primary rounded theme-glass-hover">Cancel</button>
<button id="create-client-btn" onclick="submitCreateClient()" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Create</button>
</div>
</dialog>
<dialog id="serviceSubscriptionModal" class="p-6 theme-glass rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] overflow-y-auto">
<h3 class="text-xl font-bold mb-4">Service Subscription</h3>
<div class="mb-4">
<input id="subscription-search" type="text" placeholder="Search domains..." class="w-full p-3 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary" oninput="filterSubscriptionDomains()">
</div>
<div id="subscription-domains-list" class="space-y-4">
<p class="theme-text-tertiary">Loading domains...</p>
</div>
<div class="flex justify-end space-x-2 mt-4">
<button onclick="document.getElementById('serviceSubscriptionModal').close()" class="px-4 py-2 theme-button-info theme-text-primary rounded theme-glass-hover">Close</button>
</div>
</dialog>
<dialog id="holesailLogModal" class="p-6 theme-glass rounded-lg shadow-xl w-full max-w-4xl">
<h3 id="holesail-log-title" class="text-xl font-bold mb-4">Holesail Logs</h3>
<div id="holesail-terminal" class="bg-black rounded-lg overflow-hidden h-96"></div>
<div class="flex justify-end mt-4">
<button onclick="document.getElementById('holesailLogModal').close()" class="px-4 py-2 theme-button-info theme-text-primary rounded theme-glass-hover">Close</button>
</div>
</dialog>
<dialog id="infoModal" class="p-6 theme-glass rounded-lg shadow-xl w-full max-w-3xl max-h-[90vh] overflow-y-auto">
<div class="flex justify-between items-start mb-4">
<h3 id="info-modal-title" class="text-2xl font-bold"></h3>
<button onclick="document.getElementById('infoModal').close()" class="theme-text-tertiary hover:text-gray-700 dark:hover:text-gray-200 text-2xl leading-none">&times;</button>
</div>
<div id="info-modal-content" class="mb-6 space-y-4"></div>
<div class="flex justify-end space-x-2">
<button onclick="document.getElementById('infoModal').close()" class="px-4 py-2 theme-button-info theme-text-primary rounded theme-glass-hover">Close</button>
</div>
</dialog>
<dialog id="backupDetailsModal" class="backups-modal local-dns-modal theme-glass rounded-lg shadow-xl w-full max-w-3xl p-0 border-0">
<div class="local-dns-modal-inner backups-modal-inner">
<div class="local-dns-modal-header">
<div>
<h3 class="local-dns-modal-title">Backup details</h3>
<p id="backup-details-subtitle" class="local-dns-modal-subtitle theme-text-tertiary font-mono break-all"></p>
</div>
<button type="button" onclick="document.getElementById('backupDetailsModal').close()" class="admin-btn admin-btn--secondary admin-btn--sm" aria-label="Close">
<i class="fas fa-xmark" aria-hidden="true"></i>
</button>
</div>
<div id="backup-details-content" class="local-dns-modal-body backups-modal-body"></div>
<div class="local-dns-modal-footer">
<button type="button" onclick="document.getElementById('backupDetailsModal').close()" class="admin-btn admin-btn--secondary">Close</button>
</div>
</div>
</dialog>
<dialog id="peerDetailsModal" class="p-6 theme-glass rounded-lg shadow-xl w-full max-w-3xl max-h-[90vh] overflow-y-auto">
<div class="flex justify-between items-start mb-4 gap-4">
<div>
<h3 class="text-xl font-semibold">Peer details</h3>
<p class="text-sm theme-text-tertiary mt-1">Connection info, metrics, and recent history.</p>
</div>
<button type="button" onclick="document.getElementById('peerDetailsModal').close()" class="admin-btn admin-btn--secondary admin-btn--sm" aria-label="Close">
<i class="fas fa-xmark" aria-hidden="true"></i>
</button>
</div>
<div id="peer-details-content" class="mb-6"></div>
<div class="flex justify-end">
<button type="button" onclick="document.getElementById('peerDetailsModal').close()" class="admin-btn admin-btn--secondary">Close</button>
</div>
</dialog>
<dialog id="resetIdentityModal" class="p-6 theme-glass rounded-lg shadow-xl w-full max-w-md">
<h3 class="text-lg font-semibold mb-4 text-red-400"><i class="fas fa-triangle-exclamation mr-2" aria-hidden="true"></i>Reset Identity</h3>
<p class="text-sm theme-text-primary mb-3">
<strong>Warning:</strong> This action will permanently delete your current identity keypair (<code class="theme-glass px-1 rounded">cache/keypair.json</code>) and generate a new one.
</p>
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3 mb-3">
<p class="text-sm text-red-300 font-semibold mb-2">This action cannot be undone.</p>
<ul class="text-xs text-red-700 dark:text-red-400 list-disc list-inside space-y-1">
<li>You will lose access to all domains you have claimed</li>
<li>Your claims will become unrecoverable unless network consensus allows you to reclaim them</li>
<li>Your votes will no longer be associated with your new identity</li>
<li>You will need to rejoin the network with your new identity</li>
</ul>
</div>
<p class="text-sm theme-text-primary mb-3">
To confirm, please type <strong>"I, Understand"</strong> (case-sensitive):
</p>
<input
id="reset-identity-confirm"
type="text"
placeholder="Type: I, Understand"
class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
oninput="onResetIdentityConfirmChange()"
>
<div class="flex justify-end space-x-2">
<button
onclick="document.getElementById('resetIdentityModal').close()"
class="px-4 py-2 theme-button-info theme-text-primary rounded theme-glass-hover"
>
Cancel
</button>
<button
id="reset-identity-submit"
onclick="resetIdentity(this)"
disabled
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
Reset Identity
</button>
</div>
</dialog>
<dialog id="subnetModal" class="p-6 theme-glass rounded-lg shadow-xl w-full max-w-md">
<h3 id="subnet-modal-title" class="text-xl font-bold mb-4">Add Subnet</h3>
<div class="mb-4">
<label for="subnet-name" class="block text-sm font-medium theme-text-primary mb-2">Subnet Name</label>
<input id="subnet-name" placeholder="e.g., Primary Subnet" class="w-full p-3 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<p class="text-xs theme-text-tertiary mt-1">A descriptive name for this subnet</p>
</div>
<div class="mb-4">
<label for="subnet-base" class="block text-sm font-medium theme-text-primary mb-2">Base IP Address</label>
<input id="subnet-base" placeholder="e.g., 192.168.3.0" class="w-full p-3 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<p class="text-xs theme-text-tertiary mt-1">The network address of the subnet (typically ends in .0)</p>
</div>
<div class="mb-4">
<label for="subnet-cidr" class="block text-sm font-medium theme-text-primary mb-2">CIDR Notation</label>
<input id="subnet-cidr" type="number" min="1" max="32" placeholder="24" value="24" class="w-full p-3 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<p class="text-xs theme-text-tertiary mt-1">Subnet mask in CIDR notation (1-32). /24 = 255.255.255.0 (254 usable IPs)</p>
</div>
<div class="mb-4">
<label for="subnet-startIndex" class="block text-sm font-medium theme-text-primary mb-2">Start IP Index</label>
<input id="subnet-startIndex" type="number" min="1" max="254" placeholder="2" value="2" class="w-full p-3 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<p class="text-xs theme-text-tertiary mt-1">First usable IP address index (1-254). IPs will be assigned starting from this number</p>
</div>
<div class="flex justify-end space-x-2">
<button onclick="document.getElementById('subnetModal').close()" class="px-4 py-2 theme-button-info theme-text-primary rounded theme-glass-hover">Cancel</button>
<button id="subnet-submit" onclick="submitSubnet(this)" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Add</button>
</div>
</dialog>
<dialog id="dnsServerModal" class="p-6 theme-glass rounded-lg shadow-xl max-w-md" style="width: 100%; max-width: 28rem;">
<h3 id="dns-server-modal-title" class="text-xl font-bold mb-4">Add Server</h3>
<input id="dns-server-ip" placeholder="DNS Server IP (e.g., 1.1.1.1)" class="w-full p-3 mb-4 theme-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary">
<div class="flex justify-end space-x-2">
<button onclick="document.getElementById('dnsServerModal').close()" class="px-4 py-2 theme-button-info theme-text-primary rounded theme-glass-hover">Cancel</button>
<button id="dns-server-submit" onclick="submitDnsServer(this)" class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover">Add</button>
</div>
</dialog>
<div id="notifications" class="admin-toast-stack" aria-live="polite" aria-relevant="additions"></div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/xterm.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@xterm/[email protected]/lib/addon-fit.js"></script>
<!-- Load modules in order: config -> state -> utils -> core -> notifications -> ws-client -> ui modules -> main -->
<script src="ui/config.js"></script>
<script src="ui/state.js"></script>
<script src="utils.js"></script>
<script src="ui/core.js"></script>
<script src="ui/notifications.js"></script>
<script src="ui/alerts.js"></script>
<script src="ui/alerts-sync.js"></script>
<script src="ui/confirmation-modal.js"></script>
<script src="ws-client.js"></script>
<script src="ui/domains.js"></script>
<script src="ui/certs.js"></script>
<script src="ui/interfaces.js"></script>
<script src="ui/entries.js"></script>
<script src="ui/local-dns.js"></script>
<script src="ui/logs.js"></script>
<script src="ui/settings.js"></script>
<script src="ui/stats.js"></script>
<script src="ui/holesail.js"></script>
<script src="ui/info-modal.js"></script>
<script src="ui/system.js"></script>
<script src="ui/backups.js"></script>
<script src="ui/health.js"></script>
<script src="ui/diagnostics.js"></script>
<script src="ui/peers.js"></script>
<script src="ui/plugins.js"></script>
<script src="admin.js"></script>
<script>
function handleHashChange() {
var tabId = location.hash.substring(1);
if (tabId && document.getElementById(tabId) && typeof showTab === 'function') {
showTab(tabId);
}
}
window.addEventListener('hashchange', handleHashChange);
window.addEventListener('load', function() {
// Wait for admin.js to load
if (typeof showTab === 'function') {
var tabId = location.hash.substring(1);
if (tabId && document.getElementById(tabId)) {
showTab(tabId);
} else {
showTab('domains');
}
} else {
// Retry after a short delay if script hasn't loaded
setTimeout(function() {
if (typeof showTab === 'function') {
var tabId = location.hash.substring(1);
if (tabId && document.getElementById(tabId)) {
showTab(tabId);
} else {
showTab('domains');
}
}
}, 100);
}
});
</script>
</body>
</html>