Files

1184 lines
46 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AUTARCH DNS — Command</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Oswald:wght@300;400;500;600;700&display=swap');
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root {
--bg-deep: #06090c;
--bg-panel: #0b1117;
--bg-surface: #101820;
--bg-elevated: #162030;
--border: #1a2a3a;
--border-bright: #1e3a50;
--green-dim: #1a3a2a;
--green: #00d672;
--green-bright: #00ff88;
--green-glow: rgba(0,214,114,0.15);
--amber: #ffb020;
--amber-dim: #3a2a10;
--red: #ff3a4a;
--red-dim: #3a1a1a;
--cyan: #00c8ff;
--cyan-dim: #0a2a3a;
--text: #c8d8e8;
--text-dim: #607888;
--text-bright: #e8f0f8;
--mono: 'JetBrains Mono', 'Consolas', monospace;
--display: 'Oswald', 'Impact', sans-serif;
--radius: 2px;
--sidebar-w: 220px;
}
html,body{height:100%;overflow:hidden}
body{
font-family:var(--mono);
font-size:12px;
color:var(--text);
background:var(--bg-deep);
line-height:1.5;
-webkit-font-smoothing:antialiased;
}
::selection{background:var(--green);color:var(--bg-deep)}
::-webkit-scrollbar{width:6px;height:6px}
::-webkit-scrollbar-track{background:var(--bg-panel)}
::-webkit-scrollbar-thumb{background:var(--border-bright);border-radius:3px}
::-webkit-scrollbar-thumb:hover{background:var(--green-dim)}
/* ═══ LOGIN ═══ */
#login-screen{
position:fixed;inset:0;z-index:9999;
background:var(--bg-deep);
display:flex;align-items:center;justify-content:center;
flex-direction:column;
}
#login-screen.hidden{display:none}
.login-bg{
position:absolute;inset:0;overflow:hidden;opacity:0.08;
background:
repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,214,114,0.03) 2px,rgba(0,214,114,0.03) 4px),
repeating-linear-gradient(90deg,transparent,transparent 40px,rgba(0,214,114,0.02) 40px,rgba(0,214,114,0.02) 41px);
}
.login-box{
position:relative;z-index:1;
width:380px;
background:var(--bg-panel);
border:1px solid var(--border-bright);
padding:0;
}
.login-header{
background:var(--bg-surface);
border-bottom:1px solid var(--border-bright);
padding:20px 28px 16px;
text-align:center;
}
.login-header h1{
font-family:var(--display);
font-weight:700;
font-size:28px;
letter-spacing:6px;
text-transform:uppercase;
color:var(--green);
margin-bottom:4px;
}
.login-header .sub{
font-size:10px;
letter-spacing:3px;
color:var(--text-dim);
text-transform:uppercase;
}
.login-body{padding:28px}
.login-body label{
display:block;
font-size:9px;
letter-spacing:2px;
text-transform:uppercase;
color:var(--text-dim);
margin-bottom:6px;
}
.login-body input{
width:100%;
background:var(--bg-deep);
border:1px solid var(--border);
color:var(--text-bright);
font-family:var(--mono);
font-size:13px;
padding:10px 12px;
margin-bottom:18px;
outline:none;
transition:border-color 0.2s;
}
.login-body input:focus{border-color:var(--green)}
.login-btn{
width:100%;
background:var(--green);
color:var(--bg-deep);
border:none;
font-family:var(--display);
font-size:14px;
font-weight:600;
letter-spacing:3px;
text-transform:uppercase;
padding:12px;
cursor:pointer;
transition:all 0.15s;
}
.login-btn:hover{background:var(--green-bright);transform:translateY(-1px)}
.login-btn:active{transform:translateY(0)}
.login-error{
color:var(--red);
font-size:11px;
text-align:center;
margin-top:12px;
min-height:16px;
}
.login-footer{
border-top:1px solid var(--border);
padding:12px 28px;
font-size:9px;
color:var(--text-dim);
text-align:center;
letter-spacing:1px;
}
/* ═══ APP SHELL ═══ */
#app{display:none;height:100vh;flex-direction:column}
#app.active{display:flex}
/* Top bar */
.topbar{
height:36px;
background:var(--bg-panel);
border-bottom:1px solid var(--border);
display:flex;
align-items:center;
padding:0 16px;
flex-shrink:0;
gap:12px;
}
.topbar-brand{
font-family:var(--display);
font-weight:600;
font-size:13px;
letter-spacing:4px;
color:var(--green);
text-transform:uppercase;
}
.topbar-sep{width:1px;height:16px;background:var(--border-bright)}
.topbar-status{
display:flex;align-items:center;gap:6px;
font-size:10px;color:var(--text-dim);
}
.status-dot{
width:6px;height:6px;border-radius:50%;
background:var(--green);
box-shadow:0 0 6px var(--green);
animation:pulse 2s infinite;
}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
.topbar-right{margin-left:auto;display:flex;align-items:center;gap:12px}
.topbar-user{font-size:10px;color:var(--text-dim);letter-spacing:1px;text-transform:uppercase}
.topbar-logout{
background:none;border:1px solid var(--border);
color:var(--text-dim);font-family:var(--mono);font-size:10px;
padding:3px 10px;cursor:pointer;letter-spacing:1px;
transition:all 0.15s;
}
.topbar-logout:hover{border-color:var(--red);color:var(--red)}
/* Layout */
.main-layout{display:flex;flex:1;overflow:hidden}
/* Sidebar */
.sidebar{
width:var(--sidebar-w);
background:var(--bg-panel);
border-right:1px solid var(--border);
display:flex;flex-direction:column;
flex-shrink:0;
overflow-y:auto;
}
.nav-section{
padding:16px 0 8px 16px;
font-size:9px;letter-spacing:2px;
text-transform:uppercase;
color:var(--text-dim);
}
.nav-item{
display:flex;align-items:center;gap:10px;
padding:8px 16px;
cursor:pointer;
font-size:11px;
color:var(--text-dim);
border-left:2px solid transparent;
transition:all 0.12s;
}
.nav-item:hover{color:var(--text);background:rgba(0,214,114,0.03)}
.nav-item.active{
color:var(--green);
border-left-color:var(--green);
background:rgba(0,214,114,0.05);
}
.nav-icon{
width:16px;height:16px;
display:flex;align-items:center;justify-content:center;
font-size:12px;opacity:0.7;
}
.nav-item.active .nav-icon{opacity:1}
/* Content */
.content{flex:1;overflow-y:auto;padding:20px;background:var(--bg-deep)}
.page{display:none}
.page.active{display:block;animation:fadeIn 0.2s ease}
@keyframes fadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
/* ═══ COMPONENTS ═══ */
/* Section header */
.section-hdr{
font-family:var(--display);
font-size:18px;font-weight:500;
letter-spacing:3px;text-transform:uppercase;
color:var(--text-bright);
margin-bottom:16px;
padding-bottom:8px;
border-bottom:1px solid var(--border);
}
.section-hdr .accent{color:var(--green)}
/* Cards */
.card{
background:var(--bg-panel);
border:1px solid var(--border);
margin-bottom:16px;
}
.card-head{
padding:10px 16px;
background:var(--bg-surface);
border-bottom:1px solid var(--border);
font-size:10px;letter-spacing:2px;
text-transform:uppercase;
color:var(--text-dim);
display:flex;align-items:center;justify-content:space-between;
}
.card-body{padding:16px}
/* Stat grid */
.stat-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;margin-bottom:20px}
.stat-card{
background:var(--bg-panel);
border:1px solid var(--border);
padding:14px 16px;
}
.stat-label{
font-size:9px;letter-spacing:2px;
text-transform:uppercase;color:var(--text-dim);
margin-bottom:6px;
}
.stat-value{
font-size:22px;font-weight:300;
color:var(--green-bright);
font-variant-numeric:tabular-nums;
}
.stat-value.amber{color:var(--amber)}
.stat-value.cyan{color:var(--cyan)}
.stat-value.red{color:var(--red)}
.stat-value.white{color:var(--text-bright)}
/* Tables */
.tbl{width:100%;border-collapse:collapse;font-size:11px}
.tbl th{
text-align:left;
padding:8px 12px;
font-size:9px;letter-spacing:1.5px;
text-transform:uppercase;
color:var(--text-dim);
background:var(--bg-surface);
border-bottom:1px solid var(--border);
font-weight:500;
position:sticky;top:0;z-index:1;
}
.tbl td{
padding:6px 12px;
border-bottom:1px solid var(--border);
color:var(--text);
max-width:300px;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.tbl tr:hover td{background:rgba(0,214,114,0.02)}
.tbl .mono{font-variant-numeric:tabular-nums}
/* Badges */
.badge{
display:inline-block;
padding:1px 6px;
font-size:9px;letter-spacing:1px;
text-transform:uppercase;
border:1px solid;
}
.badge-green{color:var(--green);border-color:var(--green-dim);background:rgba(0,214,114,0.05)}
.badge-amber{color:var(--amber);border-color:var(--amber-dim);background:rgba(255,176,32,0.05)}
.badge-red{color:var(--red);border-color:var(--red-dim);background:rgba(255,58,74,0.05)}
.badge-cyan{color:var(--cyan);border-color:var(--cyan-dim);background:rgba(0,200,255,0.05)}
/* Buttons */
.btn{
font-family:var(--mono);font-size:10px;
letter-spacing:1px;text-transform:uppercase;
padding:6px 14px;cursor:pointer;
border:1px solid var(--border-bright);
background:var(--bg-surface);
color:var(--text);
transition:all 0.12s;
}
.btn:hover{border-color:var(--green);color:var(--green)}
.btn-primary{background:var(--green);color:var(--bg-deep);border-color:var(--green);font-weight:600}
.btn-primary:hover{background:var(--green-bright)}
.btn-danger{border-color:var(--red-dim);color:var(--red)}
.btn-danger:hover{background:var(--red-dim);border-color:var(--red)}
.btn-sm{padding:3px 8px;font-size:9px}
.btn-group{display:flex;gap:6px;flex-wrap:wrap}
/* Inputs */
.input{
font-family:var(--mono);font-size:12px;
background:var(--bg-deep);
border:1px solid var(--border);
color:var(--text-bright);
padding:8px 12px;
outline:none;width:100%;
transition:border-color 0.15s;
}
.input:focus{border-color:var(--green)}
.input::placeholder{color:var(--text-dim)}
.input-sm{padding:5px 8px;font-size:11px}
.form-row{display:flex;gap:8px;margin-bottom:10px;align-items:flex-end}
.form-group{flex:1}
.form-label{
display:block;
font-size:9px;letter-spacing:1.5px;
text-transform:uppercase;
color:var(--text-dim);
margin-bottom:4px;
}
/* Toggle */
.toggle-row{display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--border)}
.toggle-row:last-child{border-bottom:none}
.toggle-info{flex:1}
.toggle-label{font-size:12px;color:var(--text-bright)}
.toggle-desc{font-size:10px;color:var(--text-dim);margin-top:2px}
.toggle{position:relative;width:40px;height:20px;flex-shrink:0}
.toggle input{opacity:0;width:0;height:0}
.toggle-track{
position:absolute;inset:0;
background:var(--bg-deep);
border:1px solid var(--border-bright);
cursor:pointer;transition:all 0.2s;
}
.toggle-track::after{
content:'';position:absolute;
left:2px;top:2px;width:14px;height:14px;
background:var(--text-dim);transition:all 0.2s;
}
.toggle input:checked+.toggle-track{border-color:var(--green);background:var(--green-dim)}
.toggle input:checked+.toggle-track::after{transform:translateX(20px);background:var(--green)}
/* NS grid */
.ns-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.ns-slot{
background:var(--bg-deep);
border:1px solid var(--border);
padding:10px 12px;
}
.ns-slot-label{
font-size:9px;letter-spacing:2px;
text-transform:uppercase;
color:var(--text-dim);
margin-bottom:6px;
}
.ns-slot input{
width:100%;background:transparent;
border:none;border-bottom:1px solid var(--border);
color:var(--green-bright);
font-family:var(--mono);font-size:13px;
padding:4px 0;outline:none;
}
.ns-slot input:focus{border-bottom-color:var(--green)}
.ns-slot input::placeholder{color:var(--text-dim)}
/* Empty state */
.empty{
text-align:center;padding:40px;
color:var(--text-dim);font-size:11px;
letter-spacing:1px;
}
/* Scan line overlay */
.scanline{
pointer-events:none;position:fixed;inset:0;z-index:10000;
background:repeating-linear-gradient(
0deg,
transparent, transparent 2px,
rgba(0,0,0,0.03) 2px, rgba(0,0,0,0.03) 4px
);
opacity:0.5;
}
/* Toast */
#toast{
position:fixed;bottom:20px;right:20px;z-index:99999;
background:var(--bg-elevated);
border:1px solid var(--green);
color:var(--green);
padding:10px 18px;
font-size:11px;letter-spacing:1px;
transform:translateY(80px);opacity:0;
transition:all 0.3s cubic-bezier(0.22,1,0.36,1);
}
#toast.show{transform:translateY(0);opacity:1}
#toast.error{border-color:var(--red);color:var(--red)}
/* Modal */
.modal-overlay{
position:fixed;inset:0;z-index:50000;
background:rgba(6,9,12,0.85);
display:none;align-items:center;justify-content:center;
}
.modal-overlay.active{display:flex}
.modal{
background:var(--bg-panel);
border:1px solid var(--border-bright);
width:480px;max-width:90vw;max-height:80vh;overflow-y:auto;
}
.modal-head{
padding:14px 20px;
background:var(--bg-surface);
border-bottom:1px solid var(--border);
font-family:var(--display);
font-size:14px;letter-spacing:2px;
text-transform:uppercase;
display:flex;align-items:center;justify-content:space-between;
}
.modal-close{
background:none;border:none;color:var(--text-dim);
font-size:18px;cursor:pointer;line-height:1;
}
.modal-close:hover{color:var(--red)}
.modal-body{padding:20px}
/* Loading shimmer */
@keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
.loading{
background:linear-gradient(90deg,var(--bg-panel) 25%,var(--bg-surface) 50%,var(--bg-panel) 75%);
background-size:200% 100%;
animation:shimmer 1.5s infinite;
height:20px;margin:8px 0;
}
</style>
</head>
<body>
<div class="scanline"></div>
<!-- ═══ LOGIN SCREEN ═══ -->
<div id="login-screen">
<div class="login-bg"></div>
<div class="login-box">
<div class="login-header">
<h1>Autarch</h1>
<div class="sub">DNS Command Interface</div>
</div>
<div class="login-body">
<label>Operator ID</label>
<input type="text" id="login-user" autocomplete="username" spellcheck="false">
<label>Access Code</label>
<input type="password" id="login-pass" autocomplete="current-password">
<button class="login-btn" onclick="doLogin()">Authenticate</button>
<div class="login-error" id="login-error"></div>
</div>
<div class="login-footer">SETEC LABORATORIES &mdash; AUTHORIZED PERSONNEL ONLY</div>
</div>
</div>
<!-- ═══ APP ═══ -->
<div id="app">
<div class="topbar">
<div class="topbar-brand">Autarch DNS</div>
<div class="topbar-sep"></div>
<div class="topbar-status">
<div class="status-dot" id="status-dot"></div>
<span id="topbar-uptime">--</span>
</div>
<div class="topbar-sep"></div>
<div class="topbar-status">
<span id="topbar-queries">0</span> queries
</div>
<div class="topbar-right">
<span class="topbar-user" id="topbar-user">admin</span>
<button class="topbar-logout" onclick="doLogout()">Logout</button>
</div>
</div>
<div class="main-layout">
<div class="sidebar">
<div class="nav-section">Operations</div>
<div class="nav-item active" data-page="dashboard" onclick="nav(this)">
<div class="nav-icon">&#9632;</div> Dashboard
</div>
<div class="nav-item" data-page="querylog" onclick="nav(this)">
<div class="nav-icon">&#9654;</div> Query Log
</div>
<div class="nav-item" data-page="analytics" onclick="nav(this)">
<div class="nav-icon">&#9650;</div> Analytics
</div>
<div class="nav-section">DNS Management</div>
<div class="nav-item" data-page="zones" onclick="nav(this)">
<div class="nav-icon">&#9830;</div> Zones &amp; Records
</div>
<div class="nav-item" data-page="nameservers" onclick="nav(this)">
<div class="nav-icon">&#9733;</div> Nameservers
</div>
<div class="nav-item" data-page="cache" onclick="nav(this)">
<div class="nav-icon">&#9679;</div> Cache
</div>
<div class="nav-section">Security</div>
<div class="nav-item" data-page="blocklist" onclick="nav(this)">
<div class="nav-icon">&#10006;</div> Blocklist
</div>
<div class="nav-item" data-page="hosts" onclick="nav(this)">
<div class="nav-icon">&#9783;</div> Hosts
</div>
<div class="nav-item" data-page="encryption" onclick="nav(this)">
<div class="nav-icon">&#9670;</div> Encryption
</div>
<div class="nav-section">System</div>
<div class="nav-item" data-page="config" onclick="nav(this)">
<div class="nav-icon">&#9881;</div> Configuration
</div>
</div>
<div class="content">
<!-- DASHBOARD -->
<div class="page active" id="page-dashboard">
<div class="section-hdr">System <span class="accent">Dashboard</span></div>
<div class="stat-grid" id="dash-stats"></div>
<div class="card">
<div class="card-head">Recent Queries</div>
<div class="card-body" style="max-height:320px;overflow-y:auto;padding:0">
<table class="tbl"><thead><tr>
<th>Time</th><th>Client</th><th>Name</th><th>Type</th><th>Result</th><th>Latency</th>
</tr></thead><tbody id="dash-recent"></tbody></table>
</div>
</div>
</div>
<!-- QUERY LOG -->
<div class="page" id="page-querylog">
<div class="section-hdr">Query <span class="accent">Log</span></div>
<div class="btn-group" style="margin-bottom:12px">
<button class="btn" onclick="loadQueryLog()">Refresh</button>
<button class="btn btn-danger" onclick="clearQueryLog()">Clear Log</button>
</div>
<div class="card"><div class="card-body" style="max-height:calc(100vh - 200px);overflow-y:auto;padding:0">
<table class="tbl"><thead><tr>
<th>Timestamp</th><th>Client</th><th>Name</th><th>Type</th><th>Rcode</th><th>Answers</th><th>Latency</th><th>Source</th>
</tr></thead><tbody id="qlog-body"></tbody></table>
</div></div>
</div>
<!-- ANALYTICS -->
<div class="page" id="page-analytics">
<div class="section-hdr">Query <span class="accent">Analytics</span></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
<div class="card">
<div class="card-head">Top Domains</div>
<div class="card-body" style="max-height:400px;overflow-y:auto;padding:0">
<table class="tbl"><thead><tr><th>Domain</th><th>Queries</th></tr></thead>
<tbody id="analytics-domains"></tbody></table>
</div>
</div>
<div class="card">
<div class="card-head">Query Types</div>
<div class="card-body" id="analytics-types" style="padding:12px"></div>
</div>
<div class="card" style="grid-column:1/-1">
<div class="card-head">Clients</div>
<div class="card-body" style="max-height:300px;overflow-y:auto;padding:0">
<table class="tbl"><thead><tr><th>Client IP</th><th>Queries</th></tr></thead>
<tbody id="analytics-clients"></tbody></table>
</div>
</div>
</div>
</div>
<!-- ZONES -->
<div class="page" id="page-zones">
<div class="section-hdr">Zones &amp; <span class="accent">Records</span></div>
<div class="form-row" style="margin-bottom:16px">
<div class="form-group">
<input class="input" id="zone-new-domain" placeholder="example.com">
</div>
<button class="btn btn-primary" onclick="createZone()">Create Zone</button>
</div>
<div id="zones-list"></div>
<div id="zone-detail" style="display:none">
<div class="card">
<div class="card-head">
<span>Zone: <span id="zone-detail-name" style="color:var(--green)"></span></span>
<div class="btn-group">
<button class="btn btn-sm" onclick="closeZoneDetail()">Back</button>
<button class="btn btn-sm btn-danger" onclick="deleteCurrentZone()">Delete Zone</button>
</div>
</div>
<div class="card-body">
<div class="form-row">
<div class="form-group"><div class="form-label">Name</div><input class="input input-sm" id="rec-name" placeholder="subdomain.example.com."></div>
<div class="form-group" style="max-width:100px"><div class="form-label">Type</div>
<select class="input input-sm" id="rec-type"><option>A</option><option>AAAA</option><option>CNAME</option><option>MX</option><option>TXT</option><option>NS</option><option>SRV</option><option>PTR</option></select>
</div>
<div class="form-group"><div class="form-label">Value</div><input class="input input-sm" id="rec-value" placeholder="1.2.3.4"></div>
<div class="form-group" style="max-width:80px"><div class="form-label">TTL</div><input class="input input-sm" id="rec-ttl" value="300" type="number"></div>
<button class="btn btn-primary btn-sm" onclick="addRecord()" style="margin-bottom:0">Add</button>
</div>
<div style="max-height:400px;overflow-y:auto">
<table class="tbl"><thead><tr><th>Name</th><th>Type</th><th>Value</th><th>TTL</th><th></th></tr></thead>
<tbody id="records-body"></tbody></table>
</div>
</div>
</div>
</div>
</div>
<!-- NAMESERVERS -->
<div class="page" id="page-nameservers">
<div class="section-hdr">Nameserver <span class="accent">Configuration</span></div>
<div class="card">
<div class="card-head">Primary Nameserver Identity</div>
<div class="card-body">
<div class="form-row">
<div class="form-group">
<div class="form-label">Primary NS Hostname</div>
<input class="input" id="ns-primary" placeholder="ns1.example.com">
</div>
<div class="form-group">
<div class="form-label">Domain</div>
<input class="input" id="ns-domain" placeholder="example.com">
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-head">Upstream Nameservers (Forwarders / Fallback)</div>
<div class="card-body">
<div class="ns-grid" id="ns-grid"></div>
<div style="margin-top:14px;display:flex;justify-content:flex-end">
<button class="btn btn-primary" onclick="saveNameservers()">Save Nameservers</button>
</div>
</div>
</div>
</div>
<!-- CACHE -->
<div class="page" id="page-cache">
<div class="section-hdr">DNS <span class="accent">Cache</span></div>
<div class="btn-group" style="margin-bottom:12px">
<button class="btn" onclick="loadCache()">Refresh</button>
<button class="btn btn-danger" onclick="flushCache()">Flush All</button>
</div>
<div class="card"><div class="card-body" style="max-height:calc(100vh - 220px);overflow-y:auto;padding:0">
<table class="tbl"><thead><tr>
<th>Name</th><th>Type</th><th>TTL</th><th>Answers</th><th>Expires</th><th></th>
</tr></thead><tbody id="cache-body"></tbody></table>
</div></div>
</div>
<!-- BLOCKLIST -->
<div class="page" id="page-blocklist">
<div class="section-hdr">Domain <span class="accent">Blocklist</span></div>
<div class="form-row" style="margin-bottom:12px">
<div class="form-group">
<input class="input" id="block-domain" placeholder="malware.example.com">
</div>
<button class="btn btn-primary" onclick="addBlock()">Block</button>
<button class="btn" onclick="loadBlocklist()">Refresh</button>
</div>
<div class="card">
<div class="card-head"><span>Blocked Domains</span><span id="block-count">0</span></div>
<div class="card-body" style="max-height:calc(100vh - 260px);overflow-y:auto;padding:0">
<table class="tbl"><thead><tr><th>Domain</th><th></th></tr></thead>
<tbody id="block-body"></tbody></table>
</div>
</div>
</div>
<!-- HOSTS -->
<div class="page" id="page-hosts">
<div class="section-hdr">Hosts <span class="accent">File</span></div>
<div class="form-row" style="margin-bottom:12px">
<div class="form-group" style="max-width:160px">
<input class="input input-sm" id="host-ip" placeholder="IP Address">
</div>
<div class="form-group">
<input class="input input-sm" id="host-name" placeholder="hostname.local">
</div>
<button class="btn btn-primary btn-sm" onclick="addHost()">Add</button>
<button class="btn btn-sm" onclick="loadHosts()">Refresh</button>
</div>
<div class="card"><div class="card-body" style="max-height:calc(100vh - 240px);overflow-y:auto;padding:0">
<table class="tbl"><thead><tr><th>IP</th><th>Hostname</th><th>Aliases</th><th></th></tr></thead>
<tbody id="hosts-body"></tbody></table>
</div></div>
</div>
<!-- ENCRYPTION -->
<div class="page" id="page-encryption">
<div class="section-hdr">Transport <span class="accent">Encryption</span></div>
<div class="card">
<div class="card-head">Upstream Encryption Mode</div>
<div class="card-body">
<div class="toggle-row">
<div class="toggle-info">
<div class="toggle-label">DNS-over-TLS (DoT)</div>
<div class="toggle-desc">Encrypt upstream queries via TLS on port 853</div>
</div>
<label class="toggle"><input type="checkbox" id="enc-dot" onchange="saveEncryption()"><span class="toggle-track"></span></label>
</div>
<div class="toggle-row">
<div class="toggle-info">
<div class="toggle-label">DNS-over-HTTPS (DoH)</div>
<div class="toggle-desc">Encrypt upstream queries via HTTPS (RFC 8484)</div>
</div>
<label class="toggle"><input type="checkbox" id="enc-doh" onchange="saveEncryption()"><span class="toggle-track"></span></label>
</div>
</div>
</div>
<div class="card">
<div class="card-head">Encryption Status</div>
<div class="card-body" id="enc-status"></div>
</div>
</div>
<!-- CONFIG -->
<div class="page" id="page-config">
<div class="section-hdr">Server <span class="accent">Configuration</span></div>
<div class="card">
<div class="card-head">Settings</div>
<div class="card-body" id="config-body"></div>
</div>
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:8px">
<button class="btn btn-primary" onclick="saveConfig()">Save Configuration</button>
</div>
</div>
</div>
</div>
</div>
<div id="toast"></div>
<div class="modal-overlay" id="modal">
<div class="modal">
<div class="modal-head"><span id="modal-title">Modal</span><button class="modal-close" onclick="closeModal()">&times;</button></div>
<div class="modal-body" id="modal-body"></div>
</div>
</div>
<script>
/* ═══ CORE ═══ */
let currentPage='dashboard', currentZone=null, toastTimer=null;
async function api(path,opts={}){
try{
const r=await fetch(path,opts);
if(r.status===401){showLogin();return null}
return await r.json();
}catch(e){toast('Connection error','error');return null}
}
const post=(p,b)=>api(p,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});
const put=(p,b)=>api(p,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});
const del=(p,b)=>api(p,{method:'DELETE',headers:{'Content-Type':'application/json'},body:b?JSON.stringify(b):undefined});
function toast(msg,type='success'){
const t=document.getElementById('toast');
t.textContent=msg;t.className=type==='error'?'error':'';
clearTimeout(toastTimer);
requestAnimationFrame(()=>{t.classList.add('show');toastTimer=setTimeout(()=>t.classList.remove('show'),3000)});
}
function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
/* ═══ AUTH ═══ */
async function checkAuth(){
const r=await api('/auth/check');
if(r&&r.ok){
document.getElementById('topbar-user').textContent=r.username;
hideLogin();loadPage('dashboard');
}else{showLogin()}
}
function showLogin(){document.getElementById('login-screen').classList.remove('hidden');document.getElementById('app').classList.remove('active')}
function hideLogin(){document.getElementById('login-screen').classList.add('hidden');document.getElementById('app').classList.add('active')}
async function doLogin(){
const u=document.getElementById('login-user').value;
const p=document.getElementById('login-pass').value;
const r=await post('/auth/login',{username:u,password:p});
if(r&&r.ok){hideLogin();document.getElementById('topbar-user').textContent=r.username;loadPage('dashboard')}
else{document.getElementById('login-error').textContent=r?r.error:'Connection failed'}
}
async function doLogout(){await api('/auth/logout');showLogin()}
document.getElementById('login-pass').addEventListener('keydown',e=>{if(e.key==='Enter')doLogin()});
document.getElementById('login-user').addEventListener('keydown',e=>{if(e.key==='Enter')document.getElementById('login-pass').focus()});
/* ═══ NAVIGATION ═══ */
function nav(el){
const page=el.dataset.page;
document.querySelectorAll('.nav-item').forEach(n=>n.classList.remove('active'));
el.classList.add('active');
document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));
document.getElementById('page-'+page).classList.add('active');
currentPage=page;
loadPage(page);
}
function loadPage(page){
switch(page){
case'dashboard':loadDashboard();break;
case'querylog':loadQueryLog();break;
case'analytics':loadAnalytics();break;
case'zones':loadZones();break;
case'nameservers':loadNameservers();break;
case'cache':loadCache();break;
case'blocklist':loadBlocklist();break;
case'hosts':loadHosts();break;
case'encryption':loadEncryption();break;
case'config':loadConfig();break;
}
}
/* ═══ DASHBOARD ═══ */
async function loadDashboard(){
const[s,m]=await Promise.all([api('/api/status'),api('/api/metrics')]);
if(!s||!m)return;
const met=m.metrics||m;
document.getElementById('topbar-uptime').textContent=s.uptime||'--';
document.getElementById('topbar-queries').textContent=(met.total_queries||0).toLocaleString();
document.getElementById('dash-stats').innerHTML=`
<div class="stat-card"><div class="stat-label">Total Queries</div><div class="stat-value">${(met.total_queries||0).toLocaleString()}</div></div>
<div class="stat-card"><div class="stat-label">Cache Hits</div><div class="stat-value cyan">${(met.cache_hits||0).toLocaleString()}</div></div>
<div class="stat-card"><div class="stat-label">Cache Misses</div><div class="stat-value amber">${(met.cache_misses||0).toLocaleString()}</div></div>
<div class="stat-card"><div class="stat-label">Local Answers</div><div class="stat-value white">${(met.local_answers||0).toLocaleString()}</div></div>
<div class="stat-card"><div class="stat-label">Resolved</div><div class="stat-value">${(met.resolved||0).toLocaleString()}</div></div>
<div class="stat-card"><div class="stat-label">Blocked</div><div class="stat-value red">${(met.blocked||0).toLocaleString()}</div></div>
<div class="stat-card"><div class="stat-label">Failed</div><div class="stat-value red">${(met.failed||0).toLocaleString()}</div></div>
<div class="stat-card"><div class="stat-label">Cache Size</div><div class="stat-value cyan">${(m.cache_size||s.cache_size||0)}</div></div>
<div class="stat-card"><div class="stat-label">Active Zones</div><div class="stat-value white">${s.zones||0}</div></div>
<div class="stat-card"><div class="stat-label">Uptime</div><div class="stat-value white" style="font-size:14px">${s.uptime||'--'}</div></div>
`;
const ql=await api('/api/querylog?limit=20');
const tb=document.getElementById('dash-recent');
if(ql&&ql.entries){
tb.innerHTML=ql.entries.map(e=>`<tr>
<td class="mono">${esc(e.timestamp?.substring(11,19)||'')}</td>
<td>${esc(e.client?.split(':')[0]||'')}</td>
<td>${esc(e.name||'')}</td>
<td><span class="badge badge-cyan">${esc(e.type||'')}</span></td>
<td><span class="badge ${e.rcode==='NOERROR'?'badge-green':e.rcode==='NXDOMAIN'?'badge-amber':'badge-red'}">${esc(e.rcode||'')}</span></td>
<td class="mono">${esc(e.latency||'')}</td>
</tr>`).join('');
}else{tb.innerHTML='<tr><td colspan="6" class="empty">No queries yet</td></tr>'}
}
/* ═══ QUERY LOG ═══ */
async function loadQueryLog(){
const r=await api('/api/querylog?limit=500');
const tb=document.getElementById('qlog-body');
if(r&&r.entries&&r.entries.length){
tb.innerHTML=r.entries.map(e=>`<tr>
<td class="mono">${esc(e.timestamp||'')}</td>
<td>${esc(e.client||'')}</td>
<td>${esc(e.name||'')}</td>
<td><span class="badge badge-cyan">${esc(e.type||'')}</span></td>
<td><span class="badge ${e.rcode==='NOERROR'?'badge-green':'badge-amber'}">${esc(e.rcode||'')}</span></td>
<td>${e.answers||0}</td>
<td class="mono">${esc(e.latency||'')}</td>
<td><span class="badge ${e.source==='cache'?'badge-cyan':e.source==='local'?'badge-green':e.source==='blocked'?'badge-red':'badge-amber'}">${esc(e.source||'')}</span></td>
</tr>`).join('');
}else{tb.innerHTML='<tr><td colspan="8" class="empty">No log entries</td></tr>'}
}
async function clearQueryLog(){await del('/api/querylog');loadQueryLog();toast('Query log cleared')}
/* ═══ ANALYTICS ═══ */
async function loadAnalytics(){
const[d,t,c]=await Promise.all([api('/api/stats/top-domains?limit=30'),api('/api/stats/query-types'),api('/api/stats/clients')]);
const db=document.getElementById('analytics-domains');
if(d&&d.domains&&d.domains.length){
const max=d.domains[0]?.count||1;
db.innerHTML=d.domains.map(e=>`<tr><td>${esc(e.domain||'')}</td><td class="mono"><div style="display:flex;align-items:center;gap:8px"><span>${e.count}</span><div style="height:4px;background:var(--green);width:${(e.count/max*100).toFixed(0)}px;max-width:120px;opacity:0.5"></div></div></td></tr>`).join('');
}else{db.innerHTML='<tr><td colspan="2" class="empty">No data</td></tr>'}
const tp=document.getElementById('analytics-types');
if(t&&t.types){
const entries=Object.entries(t.types).sort((a,b)=>b[1]-a[1]);
const max=entries[0]?.[1]||1;
tp.innerHTML=entries.map(([k,v])=>`<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px">
<span style="width:50px;text-align:right;color:var(--cyan);font-size:11px">${esc(k)}</span>
<div style="flex:1;height:18px;background:var(--bg-deep);position:relative"><div style="height:100%;width:${(v/max*100).toFixed(1)}%;background:linear-gradient(90deg,var(--cyan-dim),rgba(0,200,255,0.3))"></div></div>
<span class="mono" style="width:50px;font-size:11px">${v}</span>
</div>`).join('');
}
const cb=document.getElementById('analytics-clients');
if(c&&c.clients){
const entries=Object.entries(c.clients).sort((a,b)=>b[1]-a[1]);
cb.innerHTML=entries.map(([k,v])=>`<tr><td>${esc(k)}</td><td class="mono">${v}</td></tr>`).join('');
}
}
/* ═══ ZONES ═══ */
async function loadZones(){
document.getElementById('zone-detail').style.display='none';
document.getElementById('zones-list').style.display='block';
const r=await api('/api/zones');
const c=document.getElementById('zones-list');
if(r&&r.zones&&r.zones.length){
c.innerHTML=r.zones.map(z=>`<div class="card" style="cursor:pointer" onclick="openZone('${esc(z.domain)}')">
<div class="card-body" style="display:flex;align-items:center;justify-content:space-between;padding:12px 16px">
<div><span style="color:var(--green);font-size:13px;font-weight:500">${esc(z.domain)}</span>
<span style="color:var(--text-dim);margin-left:12px">${z.records} records</span></div>
<span class="badge badge-green">${z.dnssec?'DNSSEC':'Active'}</span>
</div>
</div>`).join('');
}else{c.innerHTML='<div class="empty">No zones configured. Create one above.</div>'}
}
async function createZone(){
const d=document.getElementById('zone-new-domain').value.trim();
if(!d)return;
const r=await post('/api/zones',{domain:d});
if(r&&r.ok){toast('Zone created: '+d);document.getElementById('zone-new-domain').value='';loadZones()}
else toast(r?.error||'Failed','error');
}
async function openZone(domain){
currentZone=domain;
document.getElementById('zones-list').style.display='none';
document.getElementById('zone-detail').style.display='block';
document.getElementById('zone-detail-name').textContent=domain;
const r=await api('/api/zones/'+encodeURIComponent(domain));
const tb=document.getElementById('records-body');
if(r&&r.zone&&r.zone.records){
tb.innerHTML=r.zone.records.map(rec=>`<tr>
<td>${esc(rec.name||'')}</td>
<td><span class="badge badge-cyan">${esc(rec.type||'')}</span></td>
<td>${esc(rec.value||'')}</td>
<td class="mono">${rec.ttl||300}</td>
<td><button class="btn btn-danger btn-sm" onclick="deleteRecord('${esc(rec.id)}')">Del</button></td>
</tr>`).join('');
}else{tb.innerHTML='<tr><td colspan="5" class="empty">No records</td></tr>'}
}
function closeZoneDetail(){currentZone=null;loadZones()}
async function addRecord(){
if(!currentZone)return;
const rec={name:document.getElementById('rec-name').value,type:document.getElementById('rec-type').value,
value:document.getElementById('rec-value').value,ttl:parseInt(document.getElementById('rec-ttl').value)||300};
if(!rec.name||!rec.value)return toast('Name and value required','error');
const r=await post('/api/zones/'+encodeURIComponent(currentZone)+'/records',rec);
if(r&&r.ok){toast('Record added');openZone(currentZone);document.getElementById('rec-name').value='';document.getElementById('rec-value').value=''}
else toast(r?.error||'Failed','error');
}
async function deleteRecord(id){
if(!currentZone)return;
const r=await del('/api/zones/'+encodeURIComponent(currentZone)+'/records/'+id);
if(r&&r.ok){toast('Record deleted');openZone(currentZone)}
}
async function deleteCurrentZone(){
if(!currentZone||!confirm('Delete zone '+currentZone+'?'))return;
await del('/api/zones/'+encodeURIComponent(currentZone));
toast('Zone deleted');closeZoneDetail();
}
/* ═══ NAMESERVERS ═══ */
const nsLabels=['Primary','Secondary','Tertiary','Quaternary','Quinary','Senary'];
async function loadNameservers(){
const r=await api('/webui/nameservers');
const ns=r?.nameservers||{};
const grid=document.getElementById('ns-grid');
const servers=ns.nameservers||['','','','','',''];
grid.innerHTML=servers.map((s,i)=>`<div class="ns-slot">
<div class="ns-slot-label">NS ${i+1}${nsLabels[i]||'Slot '+(i+1)}</div>
<input id="ns-${i}" value="${esc(s||'')}" placeholder="IP or hostname:port" spellcheck="false">
</div>`).join('');
document.getElementById('ns-primary').value=ns.primary||'';
document.getElementById('ns-domain').value=ns.domain||'';
}
async function saveNameservers(){
const servers=[];
for(let i=0;i<6;i++)servers.push(document.getElementById('ns-'+i)?.value||'');
const cfg={nameservers:servers,primary:document.getElementById('ns-primary').value,domain:document.getElementById('ns-domain').value};
const r=await put('/webui/nameservers',cfg);
if(r&&r.ok)toast('Nameservers saved');else toast('Failed','error');
}
/* ═══ CACHE ═══ */
async function loadCache(){
const r=await api('/api/cache');
const tb=document.getElementById('cache-body');
if(r&&r.entries&&r.entries.length){
tb.innerHTML=r.entries.map(e=>`<tr>
<td>${esc(e.name||'')}</td>
<td><span class="badge badge-cyan">${esc(e.type||'')}</span></td>
<td class="mono">${e.ttl_remaining||0}s</td>
<td>${e.answers||0}</td>
<td class="mono">${esc(e.expires_at?.substring(11,19)||'')}</td>
<td><button class="btn btn-danger btn-sm" onclick="flushEntry('${esc(e.key)}')">Flush</button></td>
</tr>`).join('');
}else{tb.innerHTML='<tr><td colspan="6" class="empty">Cache empty</td></tr>'}
}
async function flushCache(){await del('/api/cache');loadCache();toast('Cache flushed')}
async function flushEntry(key){await del('/api/cache?key='+encodeURIComponent(key));loadCache();toast('Entry flushed')}
/* ═══ BLOCKLIST ═══ */
async function loadBlocklist(){
const r=await api('/api/blocklist');
document.getElementById('block-count').textContent=r?.count||0;
const tb=document.getElementById('block-body');
if(r&&r.domains&&r.domains.length){
tb.innerHTML=r.domains.map(d=>`<tr>
<td>${esc(d)}</td>
<td style="width:60px"><button class="btn btn-danger btn-sm" onclick="removeBlock('${esc(d)}')">Remove</button></td>
</tr>`).join('');
}else{tb.innerHTML='<tr><td colspan="2" class="empty">No blocked domains</td></tr>'}
}
async function addBlock(){
const d=document.getElementById('block-domain').value.trim();
if(!d)return;
await post('/api/blocklist',{domain:d});
document.getElementById('block-domain').value='';
loadBlocklist();toast('Blocked: '+d);
}
async function removeBlock(d){await del('/api/blocklist',{domain:d});loadBlocklist();toast('Unblocked: '+d)}
/* ═══ HOSTS ═══ */
async function loadHosts(){
const r=await api('/api/hosts');
const tb=document.getElementById('hosts-body');
const entries=r?.entries||r?.hosts||[];
if(entries.length){
tb.innerHTML=entries.map(h=>`<tr>
<td class="mono">${esc(h.ip||'')}</td>
<td>${esc(h.hostname||'')}</td>
<td style="color:var(--text-dim)">${(h.aliases||[]).join(', ')}</td>
<td style="width:60px"><button class="btn btn-danger btn-sm" onclick="removeHost('${esc(h.hostname)}')">Del</button></td>
</tr>`).join('');
}else{tb.innerHTML='<tr><td colspan="4" class="empty">No host entries</td></tr>'}
}
async function addHost(){
const ip=document.getElementById('host-ip').value.trim();
const name=document.getElementById('host-name').value.trim();
if(!ip||!name)return toast('IP and hostname required','error');
await post('/api/hosts',{ip,hostname:name});
document.getElementById('host-ip').value='';document.getElementById('host-name').value='';
loadHosts();toast('Added: '+name);
}
async function removeHost(name){await del('/api/hosts',{hostname:name});loadHosts();toast('Removed: '+name)}
/* ═══ ENCRYPTION ═══ */
async function loadEncryption(){
const r=await api('/api/encryption');
if(!r)return;
document.getElementById('enc-dot').checked=!!r.dot_enabled;
document.getElementById('enc-doh').checked=!!r.doh_enabled;
const mode=r.preferred_mode||'plain';
let html=`<div style="margin-bottom:12px"><span class="badge ${mode==='doh'?'badge-green':mode==='dot'?'badge-cyan':'badge-amber'}">${mode.toUpperCase()}</span> <span style="color:var(--text-dim)">current preferred mode</span></div>`;
if(r.doh_servers){
html+='<div class="form-label" style="margin:12px 0 6px">Known DoH Endpoints</div>';
Object.entries(r.doh_servers).forEach(([ip,url])=>{html+=`<div style="font-size:11px;padding:3px 0"><span class="mono" style="color:var(--cyan)">${esc(ip)}</span> &rarr; <span style="color:var(--text-dim)">${esc(url)}</span></div>`});
}
document.getElementById('enc-status').innerHTML=html;
}
async function saveEncryption(){
const dot=document.getElementById('enc-dot').checked;
const doh=document.getElementById('enc-doh').checked;
await put('/api/config',{enable_dot:dot,enable_doh:doh});
toast('Encryption updated');
setTimeout(loadEncryption,500);
}
/* ═══ CONFIG ═══ */
const configFields=[
{key:'listen_dns',label:'DNS Listen',type:'text'},
{key:'listen_api',label:'API Listen',type:'text'},
{key:'cache_ttl',label:'Cache TTL (sec)',type:'number'},
{key:'rate_limit',label:'Rate Limit (qps/ip)',type:'number'},
{key:'max_udp_size',label:'Max UDP Size',type:'number'},
{key:'querylog_max',label:'Query Log Max',type:'number'},
{key:'negative_cache_ttl',label:'Negative Cache TTL',type:'number'},
{key:'servfail_cache_ttl',label:'SERVFAIL Cache TTL',type:'number'},
{key:'log_queries',label:'Log Queries',type:'bool'},
{key:'refuse_any',label:'Refuse ANY Queries',type:'bool'},
{key:'minimal_responses',label:'Minimal Responses',type:'bool'},
{key:'prefetch_enabled',label:'Prefetch Enabled',type:'bool'},
{key:'enable_dot',label:'DNS-over-TLS',type:'bool'},
{key:'enable_doh',label:'DNS-over-HTTPS',type:'bool'},
];
let currentConfig={};
async function loadConfig(){
const r=await api('/api/config');
if(!r||!r.config)return;
currentConfig=r.config;
const c=document.getElementById('config-body');
c.innerHTML=configFields.map(f=>{
const v=currentConfig[f.key];
if(f.type==='bool'){
return`<div class="toggle-row"><div class="toggle-info"><div class="toggle-label">${f.label}</div><div class="toggle-desc">${f.key}</div></div>
<label class="toggle"><input type="checkbox" id="cfg-${f.key}" ${v?'checked':''}><span class="toggle-track"></span></label></div>`;
}
return`<div class="form-row"><div class="form-group"><div class="form-label">${f.label} <span style="color:var(--text-dim)">(${f.key})</span></div>
<input class="input input-sm" id="cfg-${f.key}" value="${v!==undefined?v:''}" type="${f.type}"></div></div>`;
}).join('');
}
async function saveConfig(){
const updates={};
configFields.forEach(f=>{
const el=document.getElementById('cfg-'+f.key);
if(!el)return;
if(f.type==='bool')updates[f.key]=el.checked;
else if(f.type==='number')updates[f.key]=parseInt(el.value)||0;
else updates[f.key]=el.value;
});
updates.upstream=currentConfig.upstream||[];
const r=await put('/api/config',updates);
if(r&&r.ok)toast('Configuration saved');else toast('Failed','error');
}
/* ═══ MODAL ═══ */
function openModal(title,html){
document.getElementById('modal-title').textContent=title;
document.getElementById('modal-body').innerHTML=html;
document.getElementById('modal').classList.add('active');
}
function closeModal(){document.getElementById('modal').classList.remove('active')}
/* ═══ INIT ═══ */
checkAuth();
// Auto-refresh dashboard every 10s
setInterval(()=>{if(currentPage==='dashboard')loadDashboard()},10000);
</script>
</body>
</html>