438 lines
120 KiB
HTML
438 lines
120 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<meta name="theme-color" content="#08101d">
|
||
<meta name="color-scheme" content="dark">
|
||
<title>Site Gateway</title>
|
||
<meta name="description" content="Host sites, proxy services, and manage HTTPS from one simple dashboard.">
|
||
<link rel="icon" type="image/png" href="/site-gateway-icon-approved.png">
|
||
<link rel="stylesheet" href="/styles.css?v=0.16.47">
|
||
</head>
|
||
|
||
<!-- ================================================================
|
||
Login shell: password sign-in card + MFA code card (toggled by app.js)
|
||
================================================================ -->
|
||
<body>
|
||
<div id="login" class="login-shell hidden">
|
||
<form id="login-form" class="login-card">
|
||
<div class="brand-stack">
|
||
<img class="brand-icon-lg" src="/site-gateway-icon-approved.png" alt="">
|
||
<img class="brand-wordmark-lg" src="/site-gateway-wordmark-approved.png" alt="Site Gateway">
|
||
</div>
|
||
<p class="eyebrow">Host. Proxy. Secure.</p>
|
||
<h1 id="login-title">Welcome back</h1>
|
||
<p id="login-copy" class="muted">Sign in to manage your sites.</p>
|
||
<label>Username<input name="username" autocomplete="username" required></label>
|
||
<label>Password<input name="password" type="password" autocomplete="current-password" required></label>
|
||
<p id="login-error" class="error" role="alert"></p>
|
||
<button class="button primary wide">Sign in</button>
|
||
</form>
|
||
<form id="mfa-login-form" class="login-card hidden">
|
||
<div class="brand-stack">
|
||
<img class="brand-icon-lg" src="/site-gateway-icon-approved.png" alt="">
|
||
<img class="brand-wordmark-lg" src="/site-gateway-wordmark-approved.png" alt="Site Gateway">
|
||
</div>
|
||
<p class="eyebrow">Two-factor authentication</p>
|
||
<h1>Enter your code</h1>
|
||
<p class="muted">Enter the 6-digit code from your authenticator app, or one of your recovery codes.</p>
|
||
<label>Code<input name="code" autocomplete="one-time-code" inputmode="numeric" maxlength="11" required autofocus></label>
|
||
<p id="mfa-login-error" class="error" role="alert"></p>
|
||
<button class="button primary wide">Verify</button>
|
||
<button type="button" id="mfa-login-cancel" class="button secondary wide">Back to sign in</button>
|
||
</form>
|
||
</div>
|
||
|
||
|
||
<!-- First-run admin setup dialog (shown once, when installationSetupPending) -->
|
||
<dialog id="setup-dialog" class="setup-dialog">
|
||
<form id="setup-form" class="dialog-card setup-card">
|
||
<div class="brand-stack">
|
||
<img class="brand-icon-lg" src="/site-gateway-icon-approved.png" alt="">
|
||
<img class="brand-wordmark-lg" src="/site-gateway-wordmark-approved.png" alt="Site Gateway">
|
||
</div>
|
||
<p class="eyebrow">First-time setup</p>
|
||
<h1>Secure your administrator account</h1>
|
||
<p class="muted">Confirm or change the administrator details below. The credentials supplied during installation were used only to bootstrap this account.</p>
|
||
<label>Display name<input name="displayName" value="Administrator" maxlength="80" autocomplete="name" required></label>
|
||
<label>Administrator username<input name="username" minlength="3" maxlength="64" pattern="[A-Za-z0-9][A-Za-z0-9._-]{2,63}" autocomplete="username" required></label>
|
||
<label>New password<input name="password" type="password" minlength="8" autocomplete="new-password" required><small>Use at least 8 characters and a password unique to Site Gateway.</small></label>
|
||
<label>Confirm password<input name="confirmPassword" type="password" minlength="8" autocomplete="new-password" required></label>
|
||
<p id="setup-error" class="error" role="alert"></p>
|
||
<button class="button primary wide">Save administrator account</button>
|
||
</form>
|
||
</dialog>
|
||
|
||
|
||
<!-- ================================================================
|
||
Main app shell: sidebar nav (aside) + content area (main)
|
||
================================================================ -->
|
||
<div id="dashboard" class="app-shell hidden">
|
||
<aside>
|
||
<button type="button" class="brand" data-view="overview" aria-label="Go to dashboard"><img class="brand-icon" src="/site-gateway-icon-approved.png" alt=""><img class="brand-wordmark" src="/site-gateway-wordmark-approved.png" alt="Site Gateway"></button>
|
||
<nav aria-label="Publishing types">
|
||
<button class="nav-active" data-view="overview">Dashboard</button>
|
||
<button data-view="hosted">Hosted sites <span id="hosted-count">0</span></button>
|
||
<button data-view="proxies">Proxy hosts <span id="proxy-count">0</span></button>
|
||
<button data-view="streaming">Streaming hosts <span id="streaming-count">0</span></button>
|
||
<button data-view="redirects">Redirect hosts <span id="redirect-count">0</span></button>
|
||
<button data-view="certificates">Certificates <span id="certificate-count">0</span></button>
|
||
<button data-view="access">Access Lists <span id="access-count">0</span></button>
|
||
<button data-view="performance">Performance</button>
|
||
<button data-view="logs">Logs</button>
|
||
</nav>
|
||
<div class="aside-utilities"><button class="admin-only" data-view="administration">Administration</button><button data-view="account">My Account</button><button data-view="documentation">Documentation</button></div>
|
||
<div class="aside-footer"><span>Installed version</span><strong id="version-label">v—</strong></div>
|
||
</aside>
|
||
<main>
|
||
<div class="utility-bar" aria-label="Account and appearance">
|
||
<div class="account-control"><span>Signed in as <strong id="user-label">admin</strong></span><button id="logout" class="text-button">Sign out</button></div>
|
||
</div>
|
||
<nav class="mobile-nav" aria-label="Dashboard sections">
|
||
<button class="nav-active" data-view="overview">Dashboard</button>
|
||
<button data-view="hosted">Hosted</button>
|
||
<button data-view="proxies">Proxies</button>
|
||
<button data-view="redirects">Redirects</button>
|
||
<button data-view="certificates">TLS</button>
|
||
<button data-view="performance">Perf</button>
|
||
<button data-view="logs">Logs</button>
|
||
<button class="admin-only" data-view="administration">Admin</button><button data-view="account">Account</button><button data-view="documentation">Docs</button>
|
||
</nav>
|
||
<header>
|
||
<div><p class="eyebrow">Gateway control</p><h1 id="page-title">Dashboard</h1><p id="page-subtitle" class="muted">Health, activity, and system status at a glance.</p></div>
|
||
<button id="open-create" class="button primary">+ New hosted site</button><button id="check-health" class="button primary hidden">Run certificate check</button><button id="refresh-logs" class="button primary hidden">Refresh logs</button><button id="refresh-view" class="icon-button page-refresh" aria-label="Refresh this page" title="Refresh this page">↻</button>
|
||
</header>
|
||
|
||
<!-- Overview / Dashboard: health, activity, system stats -->
|
||
<section id="dashboard-view" class="dashboard-view" aria-label="Gateway dashboard">
|
||
<div class="metric-grid">
|
||
<button class="metric-card accent-green" data-target="hosted"><span class="metric-icon">↗</span><span class="metric-label">Hosted sites</span><strong id="dash-hosted-total">0</strong><span id="dash-hosted-detail">None configured</span></button>
|
||
<button class="metric-card accent-green" data-target="proxies"><span class="metric-icon">⇌</span><span class="metric-label">Proxy hosts</span><strong id="dash-proxy-total">0</strong><span id="dash-proxy-detail">None configured</span></button>
|
||
<button class="metric-card accent-green" data-target="certificates"><span class="metric-icon">▣</span><span class="metric-label">Certificates</span><strong id="dash-tls-total">0</strong><span id="dash-tls-detail">No TLS domains</span></button>
|
||
</div>
|
||
<div class="metric-strip">
|
||
<button class="metric-chip accent-green" data-target="redirects"><span class="chip-icon">↪</span><span class="chip-copy"><span class="metric-label">Redirect hosts</span><strong id="dash-redirect-total">0</strong></span></button>
|
||
<button class="metric-chip accent-green" data-target="streaming"><span class="chip-icon">⇄</span><span class="chip-copy"><span class="metric-label">Streaming hosts</span><strong id="dash-stream-total">0</strong></span></button>
|
||
<div class="metric-chip" id="dash-attention-chip"><span class="chip-icon" id="dash-attention-icon">◈</span><span class="chip-copy"><span class="metric-label">Needs attention</span><strong id="dash-attention-total">0</strong><small id="dash-attention-detail">No current issues</small></span></div>
|
||
<button class="metric-chip accent-green" data-target="performance"><span class="chip-icon">∿</span><span class="chip-copy"><span class="metric-label">Throughput</span><strong id="dash-throughput-total">0</strong><small id="dash-throughput-detail">requests / min</small></span></button>
|
||
</div>
|
||
<div class="dashboard-columns">
|
||
<section class="dashboard-panel health-panel status-healthy" id="health-panel">
|
||
<div class="panel-heading"><div><p class="eyebrow">Live health</p><h2>Services</h2></div><div class="health-actions"><span id="overall-health" class="health-badge healthy">Healthy</span></div></div>
|
||
<div class="health-grid">
|
||
<div class="health-tile"><span id="gateway-health-dot" class="status-dot running"></span><span class="health-tile-copy"><strong>Gateway</strong><small id="gateway-health-copy">Configuration valid</small></span></div>
|
||
<div class="health-tile"><span id="http-health-dot" class="status-dot running"></span><span class="health-tile-copy"><strong>HTTP · Port 80</strong><small id="http-health-copy">Ready and responding</small></span></div>
|
||
<div class="health-tile"><span id="https-health-dot" class="status-dot inactive"></span><span class="health-tile-copy"><strong>HTTPS · Port 443</strong><small id="https-health-copy">Not configured</small></span></div>
|
||
<div class="health-tile"><span id="storage-health-dot" class="status-dot running"></span><span class="health-tile-copy"><strong>Persistent storage</strong><small id="storage-health-copy">Data directory writable</small></span></div>
|
||
<div class="health-tile"><span id="streaming-health-dot" class="status-dot inactive"></span><span class="health-tile-copy"><strong>Streaming ports</strong><small id="streaming-health-copy">No streaming hosts configured</small></span></div>
|
||
<div class="health-tile"><span id="upstream-health-dot" class="status-dot inactive"></span><span class="health-tile-copy"><strong>Upstreams</strong><small id="upstream-health-copy">No proxy hosts configured</small></span></div>
|
||
</div>
|
||
<p id="health-checked" class="checked-time"><span class="live-dot" id="health-live-dot"></span>Last checked —</p>
|
||
</section>
|
||
<section class="dashboard-panel system-panel">
|
||
<div class="panel-heading"><div><p class="eyebrow">Runtime</p><h2>System</h2></div></div>
|
||
<div class="system-hero-grid" id="dashboard-hero-grid">
|
||
<div class="system-hero-stat" data-hero-stat="cpu"><span class="system-hero-label">CPU</span><strong class="system-hero-value" id="dashboard-hero-cpu-value">—</strong><div class="system-hero-bar"><div class="system-hero-fill" id="dashboard-hero-cpu-fill"></div></div><small class="system-hero-detail" id="dashboard-hero-cpu-detail"></small></div>
|
||
<div class="system-hero-stat" data-hero-stat="memory"><span class="system-hero-label">Memory</span><strong class="system-hero-value" id="dashboard-hero-memory-value">—</strong><div class="system-hero-bar"><div class="system-hero-fill" id="dashboard-hero-memory-fill"></div></div><small class="system-hero-detail" id="dashboard-hero-memory-detail"></small></div>
|
||
<div class="system-hero-stat" data-hero-stat="swap"><span class="system-hero-label">Swap</span><strong class="system-hero-value" id="dashboard-hero-swap-value">—</strong><div class="system-hero-bar"><div class="system-hero-fill" id="dashboard-hero-swap-fill"></div></div><small class="system-hero-detail" id="dashboard-hero-swap-detail"></small></div>
|
||
<div class="system-hero-stat" data-hero-stat="disk"><span class="system-hero-label">Disk</span><strong class="system-hero-value" id="dashboard-hero-disk-value">—</strong><div class="system-hero-bar"><div class="system-hero-fill" id="dashboard-hero-disk-fill"></div></div><small class="system-hero-detail" id="dashboard-hero-disk-detail"></small></div>
|
||
<div class="system-hero-stat" data-hero-stat="network"><span class="system-hero-label">Network</span><strong class="system-hero-value" id="dashboard-hero-network-value">—</strong><div class="system-hero-bar"><div class="system-hero-fill" id="dashboard-hero-network-fill"></div></div><small class="system-hero-detail" id="dashboard-hero-network-detail"></small></div>
|
||
<div class="system-hero-stat" data-hero-stat="uptime"><span class="system-hero-label">Uptime</span><strong class="system-hero-value" id="dashboard-hero-uptime-value">—</strong><div class="system-hero-bar"><div class="system-hero-fill" id="dashboard-hero-uptime-fill"></div></div><small class="system-hero-detail" id="dashboard-hero-uptime-detail"></small></div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
<div id="dashboard-jobs-slot" class="dashboard-jobs-slot"></div>
|
||
<div class="dashboard-columns lower" id="dashboard-lower-columns">
|
||
<section class="dashboard-panel attention-panel" id="attention-panel">
|
||
<div class="panel-heading"><div><p class="eyebrow">Action required</p><h2>Needs attention</h2></div></div>
|
||
<div id="attention-list" class="dashboard-list"><div class="all-clear"><span class="status-dot running"></span><span>Everything looks good — no issues to review.</span></div></div>
|
||
</section>
|
||
<section class="dashboard-panel">
|
||
<div class="panel-heading"><div><p class="eyebrow">Recent activity</p><h2>Recent activity</h2></div><button class="text-button" data-view="logs">View all logs →</button></div>
|
||
<div id="activity-list" class="dashboard-list"><p class="quiet-state">No recent activity.</p></div>
|
||
</section>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Certificates view -->
|
||
<section id="certificates-view" class="feature-view hidden">
|
||
<p id="cert-last-checked" class="certificate-status muted checked-time">Last checked —</p>
|
||
<div class="feature-summary">
|
||
<div><strong id="cert-healthy">0</strong><span>Healthy</span></div><div><strong id="cert-30">0</strong><span>Within 30 days</span></div><div><strong id="cert-7">0</strong><span>Within 7 days</span></div><div><strong id="cert-warning">0</strong><span>Needs attention</span></div><div><strong id="cert-pending">0</strong><span>Not detected</span></div>
|
||
</div>
|
||
<div class="diagnostic-section-heading"><p class="eyebrow">Certificate inventory</p><h2>Certificates</h2><p class="muted">Managed and uploaded certificates assigned to configured domains.</p></div><div id="certificate-list" class="data-list diagnostic-list"><p class="quiet-state">Loading certificates…</p></div>
|
||
<section class="dashboard-panel readiness-panel"><div class="panel-heading"><div><p class="eyebrow">Guided diagnostics</p><h2>Domain readiness</h2><p class="muted">DNS, listener, TLS, and upstream checks for every configured domain.</p></div></div><div id="readiness-list" class="dashboard-list diagnostic-list"><p class="quiet-state">Checking configured domains…</p></div></section><section class="dashboard-panel"><div class="panel-heading"><div><p class="eyebrow">Certificate health</p><h2>Renewal thresholds</h2><p class="muted">Control when a certificate is flagged as renewing soon, critical, or stale on this page and the dashboard.</p></div></div><form id="health-settings-form" class="settings-form"><label>Renewing-soon warning<input name="warningDays" type="number" min="8" max="120" value="30"><small>Days remaining before a certificate is highlighted.</small></label><label>Critical warning<input name="criticalDays" type="number" min="1" max="119" value="7"><small>Must be lower than the renewing-soon threshold.</small></label><label>Stale health data<input name="staleMinutes" type="number" min="2" max="1440" value="10"><small>Minutes before a displayed check is considered old.</small></label><div class="dialog-actions"><button class="button primary">Save health settings</button></div></form></section>
|
||
</section>
|
||
|
||
<!-- Logs view: access log table + gateway event/activity log -->
|
||
<section id="logs-view" class="feature-view hidden">
|
||
<section class="dashboard-panel">
|
||
<div class="panel-heading"><div><p class="eyebrow">Access logs</p><h2>Access requests</h2><p class="muted">Requests handled by configured domains. Sensitive headers are never displayed.</p></div></div>
|
||
<div class="log-filters"><label>Domain<select id="log-host"><option value="">All domains</option></select></label><label>Response status<select id="log-status"><option value="">All responses</option><option value="2">Successful · 2xx</option><option value="3">Redirects · 3xx</option><option value="4">Client errors · 4xx</option><option value="5">Server errors · 5xx</option></select></label></div>
|
||
<p id="log-summary" class="muted feature-note">No requests in the current view. <span id="log-last-checked">Not checked yet.</span></p>
|
||
<div class="table-wrap log-table-wrap"><table class="log-table"><thead><tr><th>Time</th><th>Domain</th><th>Request</th><th>Status</th><th>Duration</th></tr></thead><tbody id="log-rows"></tbody></table></div>
|
||
</section>
|
||
<section class="dashboard-panel log-activity"><div class="panel-heading"><div><p class="eyebrow">Gateway events</p><h2>Activity and errors</h2><p class="muted">Configuration, certificate, and health events recorded by Site Gateway.</p></div></div><div class="event-filters"><label>Severity<select id="event-severity"><option value="">All severities</option><option value="ok">Normal</option><option value="warning">Warnings</option><option value="error">Errors</option></select></label><label>Category<select id="event-category"><option value="">All categories</option><option value="configuration">Configuration</option><option value="certificate">Certificates / TLS</option><option value="health">Upstream health</option><option value="authentication">Authentication</option><option value="backup">Backups</option><option value="system">System</option></select></label></div><div id="gateway-log-list" class="dashboard-list event-list diagnostic-list"></div></section>
|
||
</section>
|
||
|
||
<!-- Performance view: request trend chart + per-domain throughput table -->
|
||
<section id="performance-view" class="feature-view hidden">
|
||
<section class="dashboard-panel">
|
||
<div class="panel-heading"><div><p class="eyebrow">Trend</p><h2 id="performance-trend-title">Requests · last 6 hours</h2></div></div>
|
||
<div class="log-filters"><label>Domain<select id="performance-host"><option value="">All domains</option></select></label><label>Range<select id="performance-range"><option value="1">Last hour</option><option value="3">Last 3 hours</option><option value="6" selected>Last 6 hours</option><option value="12">Last 12 hours</option><option value="24">Last 24 hours</option><option value="72">Last 3 days</option><option value="168">Last 7 days</option></select></label></div>
|
||
<p id="performance-summary" class="muted feature-note">No requests recorded yet. <span id="performance-last-checked">Not checked yet.</span></p>
|
||
<p class="chart-axis-unit">Requests</p>
|
||
<div class="performance-sparkline-wrap">
|
||
<svg id="performance-sparkline" class="performance-sparkline" viewBox="0 0 600 140" preserveAspectRatio="none" aria-label="Request volume trend"></svg>
|
||
<div id="performance-tooltip" class="performance-tooltip hidden" aria-hidden="true"></div>
|
||
<div id="performance-sparkline-labels" class="performance-sparkline-labels"></div>
|
||
</div>
|
||
</section>
|
||
<section class="dashboard-panel">
|
||
<div class="panel-heading"><div><p class="eyebrow">Per-route</p><h2>Throughput by domain</h2></div></div>
|
||
<p class="muted feature-note">Requests, latency, bandwidth, and unique visitors for each configured domain, over the last 24 hours.</p>
|
||
<div class="table-wrap performance-table-wrap"><table class="performance-table"><thead><tr><th>Domain</th><th>Last hour</th><th>Last 24h</th><th>Avg. response</th><th>p95 response</th><th>Data transferred</th><th>Unique visitors</th><th>Top paths</th></tr></thead><tbody id="performance-rows"></tbody></table></div>
|
||
</section>
|
||
</section>
|
||
|
||
<!-- Administration: tabbed panels (System / Users / Groups / Audit log / Logs & Retention /
|
||
Gateway defaults / Backup & restore / Danger Zone). The System tab, like Groups,
|
||
Audit log, and Logs & Retention, is inserted dynamically by features.js's
|
||
renderSystemPanel(). -->
|
||
<section id="users-view" class="feature-view hidden">
|
||
<div class="admin-tabs"><button data-admin-tab="system">System</button><button class="tab-active" data-admin-tab="users">Users</button><button data-admin-tab="groups">Groups</button><button data-admin-tab="defaults">Gateway defaults</button><button data-admin-tab="audit">Audit log</button><button data-admin-tab="backups">Backup & restore</button><button data-admin-tab="retention">Logs & Retention</button><button data-admin-tab="api">API Access</button><button data-admin-tab="danger" class="danger-tab">Danger Zone</button></div>
|
||
<!-- Users tab -->
|
||
<section data-admin-panel="system" class="hidden settings-panel"></section><section data-admin-panel="api" class="hidden settings-panel"></section><section data-admin-panel="users">
|
||
<div id="user-summary" class="summary user-summary" aria-label="User summary"></div>
|
||
<div id="user-list" class="user-grid"><p class="quiet-state">Loading users…</p></div>
|
||
</section>
|
||
<!-- Groups tab (inserted dynamically by features.js's renderGroups) -->
|
||
<section data-admin-panel="groups" class="hidden settings-panel"><p class="quiet-state padded">Loading groups…</p></section>
|
||
<!-- Audit log tab -->
|
||
<section data-admin-panel="audit" class="hidden settings-panel"><p class="quiet-state padded">Loading audit log…</p></section>
|
||
<!-- Logs & Retention tab -->
|
||
<section data-admin-panel="retention" class="hidden settings-panel"><p class="quiet-state padded">Loading retention settings…</p></section>
|
||
<!-- Gateway defaults tab: custom 404/no-response/redirect page + live preview -->
|
||
<section data-admin-panel="defaults" class="hidden settings-panel settings-panel-wide">
|
||
<h2>Default site</h2><p class="muted">Choose what visitors receive when no configured host matches their request.</p>
|
||
<div class="callout"><strong>HTTP fallback</strong><span>This response is used for unknown HTTP hostnames. Unknown HTTPS hostnames are rejected unless a matching certificate and route exist, preventing misleading certificate warnings.</span></div>
|
||
<div class="default-site-layout">
|
||
<form id="default-site-form" class="settings-form"><div class="form-section"><p class="eyebrow">Response</p><label>Response<select name="mode"><option value="themed404">Themed route-not-found page (404)</option><option value="welcome">Gateway ready page (200)</option><option value="abort">No response — close connection</option><option value="redirect">Redirect elsewhere</option><option value="custom">Custom HTML</option></select></label></div><div class="form-section"><p class="eyebrow">Page content</p><label>Page heading<input name="title" maxlength="100" placeholder="Route not found"></label><label>Explanation<textarea name="message" maxlength="500" placeholder="The gateway is responding, but this address has not been configured."></textarea></label></div><div class="form-section"><p class="eyebrow">Redirect behavior</p><label>Redirect destination<input name="redirectUrl" type="url" placeholder="https://www.example.com"></label><label>Redirect code<select name="redirectCode"><option>302</option><option>301</option><option>307</option><option>308</option></select></label><label class="check-control"><input name="preservePath" type="checkbox" checked><span>Preserve the requested path and query</span></label></div><div class="form-section form-section-wide"><p class="eyebrow">Custom response</p><label>Custom HTML<textarea name="customHtml" class="code-input" placeholder="<!doctype html>..."></textarea><small>Administrator-authored HTML only. Used when Custom HTML is selected.</small></label></div><div class="dialog-actions"><button class="button primary">Save & apply</button></div><p id="default-error" class="error"></p></form>
|
||
<div class="default-site-preview">
|
||
<p class="eyebrow">Live preview</p>
|
||
<div class="default-site-preview-frame-wrap"><iframe id="default-site-preview-frame" title="Default site preview" sandbox=""></iframe></div>
|
||
<p id="default-site-preview-note" class="quiet-state hidden"></p>
|
||
<small id="default-site-preview-caption">Updates as you type. Scripts do not run in this preview.</small>
|
||
</div>
|
||
|
||
</div>
|
||
</section>
|
||
<!-- Backup & restore tab -->
|
||
<section data-admin-panel="backups" class="hidden settings-panel"><div class="panel-heading"><div><h2>Backup & restore</h2><p class="muted">Create a copy, restore a previous version, or schedule automatic backups.</p></div><div class="row-actions"><button id="import-backup" class="button secondary">Import backup</button><button id="create-backup" class="button primary">Create backup</button></div></div><input id="backup-upload" type="file" accept=".sgbackup,application/zip" hidden><form id="backup-settings-form" class="settings-form backup-settings"><div class="form-section form-section-wide"><p class="eyebrow">Scheduled backups</p><div class="form-grid"><label class="check-control"><input name="enabled" type="checkbox"><span>Enable scheduled backups</span></label><label>Backup type<select name="type"><option value="complete">Complete (Recommended)</option><option value="configuration">Configuration only</option></select><small id="backup-type-help">Complete backups include configuration, uploaded Hosted Site files, icons, default-site assets, and certificate storage. Verify the file count after creation.</small></label><p id="backup-config-only-warning" class="callout hidden" style="grid-column:1/-1"><strong>Heads up</strong><span>Scheduled backups are set to Configuration only — they will not include your Hosted Site files, icons, or certificates. Switch to Complete if you want scheduled backups to protect those too.</span></p><label>Schedule<select name="frequency"><option value="daily">Daily</option><option value="weekly">Weekly</option><option value="monthly">Monthly</option></select></label><label>Hour<select name="hour"></select></label><label>Keep<input name="retention" type="number" min="1" max="100" value="7"></label><label class="check-control"><input name="includeLogs" type="checkbox"><span>Include logs</span></label></div></div><div class="form-section form-section-wide"><p class="eyebrow">Encryption</p><div class="form-grid encryption-grid"><label class="backup-password-field"><span class="field-label">Backup encryption password <span class="optional">Optional</span></span><input name="backupPassword" type="password" autocomplete="new-password"><small>Used for manually created backups and required when restoring an encrypted archive. It is not stored by Site Gateway.</small></label><label class="check-control encryption-toggle"><input name="encrypt" type="checkbox"><span>Encrypt scheduled backups<small>Uses the container’s <code>BACKUP_PASSWORD</code> value. Enable only after configuring that value.</small></span></label></div></div><div class="dialog-actions"><button class="button primary">Save schedule</button></div></form><div class="callout"><strong>Storage guidance</strong><span id="backup-path">Backups are stored in /data/backups. Mount /backups separately to protect against appdata disk failure.</span><span id="backup-summary" class="backup-summary"></span></div><div class="dashboard-panel backup-stored-section"><div class="panel-heading"><div><p class="eyebrow">On disk</p><h2>Stored backups</h2><p class="muted">The actual backup files currently sitting in /data/backups. Download, Restore, and Delete here act on these files directly — deleting one here is permanent and removes it from disk, not just from this list.</p></div></div><div id="backup-list" class="data-list"></div></div><!-- Gateway diagnostics / support report -- hidden from view per admin request (v0.11.109). Re-enable by deleting this comment wrapper (markup unchanged); backend endpoint /api/support-report left intact. <div class="dashboard-panel"><div class="panel-heading"><div><p class="eyebrow">Troubleshooting & support</p><h2>Gateway diagnostics</h2><p class="muted">Run checks and download a redacted report when you need to investigate a gateway issue.</p></div><button id="download-support" class="button secondary admin-only">Download support report</button></div><p class="muted feature-note">The report includes version, configuration health, certificate readiness, upstream checks, and recent events. Passwords, private keys, session secrets, cookies, and certificate contents are excluded.</p></div> --></section>
|
||
<!-- Danger Zone tab -->
|
||
<section data-admin-panel="danger" class="hidden settings-panel danger-zone"><h2>Danger Zone</h2><p class="muted">These actions can permanently remove Site Gateway data. Review each warning carefully before continuing.</p><div class="danger-card"><p class="eyebrow">Restore defaults</p><h3>Reset gateway preferences</h3><p>Restore default site behavior, backup scheduling, certificate thresholds, and interface preferences. Your users, routes, certificates, logs, and backups remain intact.</p><button id="restore-defaults" class="button secondary">Restore default settings</button></div><div class="danger-card destructive"><p class="eyebrow">Permanent action</p><h3>Factory reset</h3><p>Deletes all Site Gateway data under <code>/data</code>, including users, routes, certificates, logs, backups, and settings. Docker-mounted files outside <code>/data</code> are not affected. The container restarts at first-install setup.</p><form id="factory-reset-form" class="danger-form"><label>Administrator username<input name="username" autocomplete="username" required></label><label>Administrator password<input name="password" type="password" autocomplete="current-password" required></label><label>Type <strong>FACTORY RESET</strong> to confirm<input name="confirmation" required autocomplete="off"></label><p id="factory-reset-error" class="error"></p><div class="danger-actions"><button class="button secondary" type="button" id="factory-reset-cancel">Cancel</button><button class="button danger" type="submit">Erase all data and reset</button></div></form></div></section>
|
||
</section>
|
||
|
||
<!-- Account view: profile, change password, MFA -->
|
||
<section id="account-view" class="feature-view hidden">
|
||
<section class="dashboard-panel">
|
||
<div class="panel-heading"><div><p class="eyebrow">Profile</p><h2>Your account</h2></div></div>
|
||
<dl class="system-grid">
|
||
<div class="system-tile"><dt>Display name</dt><dd id="account-display-name">—</dd></div>
|
||
<div class="system-tile"><dt>Username</dt><dd id="account-username">—</dd></div>
|
||
<div class="system-tile"><dt>Role</dt><dd id="account-role">—</dd></div>
|
||
</dl>
|
||
</section>
|
||
<section class="dashboard-panel">
|
||
<div class="panel-heading"><div><p class="eyebrow">Password</p><h2>Change your password</h2></div></div>
|
||
<form id="account-password-form" class="settings-form">
|
||
<label>Current password<input name="currentPassword" type="password" autocomplete="current-password" required></label>
|
||
<label>New password<input name="newPassword" type="password" minlength="8" autocomplete="new-password" required></label>
|
||
<label style="grid-column:1/-1;max-width:calc(50% - 9px)">Confirm new password<input name="confirmPassword" type="password" minlength="8" autocomplete="new-password" required></label>
|
||
<p id="account-password-error" class="error" role="alert"></p>
|
||
<div class="dialog-actions"><button class="button primary" type="submit">Change password</button></div>
|
||
</form>
|
||
</section>
|
||
<section class="dashboard-panel">
|
||
<div class="panel-heading"><div><p class="eyebrow">Security</p><h2>Two-factor authentication</h2></div><span id="account-mfa-status" class="status-pill"><span class="status-dot inactive"></span>Off</span></div>
|
||
<p class="muted feature-note">Require a 6-digit code from an authenticator app, in addition to your password, when signing in.</p>
|
||
<div id="account-mfa-actions" class="dialog-actions">
|
||
<button id="account-mfa-enable" class="button primary">Enable two-factor authentication</button>
|
||
<button id="account-mfa-disable" class="button secondary danger-text hidden">Disable two-factor authentication</button>
|
||
<button id="account-mfa-recovery" class="button secondary hidden">Regenerate recovery codes</button>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
|
||
<!-- MFA setup / password-reconfirm / recovery-codes dialogs -->
|
||
<dialog id="mfa-setup-dialog" class="setup-dialog">
|
||
<div class="dialog-card">
|
||
<p class="eyebrow">Two-factor authentication</p>
|
||
<h1>Scan this code</h1>
|
||
<p class="muted">Scan with an authenticator app (Google Authenticator, 1Password, Authy, etc.), or enter the key manually.</p>
|
||
<div id="mfa-setup-qr" class="mfa-qr"></div>
|
||
<p class="muted">Manual entry key: <code id="mfa-setup-secret"></code></p>
|
||
<form id="mfa-setup-confirm-form">
|
||
<label>Enter the 6-digit code to confirm<input name="code" autocomplete="one-time-code" inputmode="numeric" maxlength="6" required></label>
|
||
<p id="mfa-setup-error" class="error" role="alert"></p>
|
||
<div class="dialog-actions"><button type="button" id="mfa-setup-cancel" class="button secondary">Cancel</button><button class="button primary" type="submit">Confirm and enable</button></div>
|
||
</form>
|
||
</div>
|
||
</dialog>
|
||
<dialog id="mfa-password-dialog" class="setup-dialog">
|
||
<form id="mfa-password-form" class="dialog-card">
|
||
<p class="eyebrow" id="mfa-password-title">Confirm your password</p>
|
||
<h1 id="mfa-password-heading">Confirm it's you</h1>
|
||
<label>Current password<input name="password" type="password" autocomplete="current-password" required></label>
|
||
<p id="mfa-password-error" class="error" role="alert"></p>
|
||
<div class="dialog-actions"><button type="button" id="mfa-password-cancel" class="button secondary">Cancel</button><button class="button primary" type="submit">Continue</button></div>
|
||
</form>
|
||
</dialog>
|
||
<dialog id="mfa-recovery-dialog" class="setup-dialog">
|
||
<div class="dialog-card">
|
||
<p class="eyebrow">Save these now</p>
|
||
<h1>Your recovery codes</h1>
|
||
<p class="muted">Each code can be used once to sign in if you lose access to your authenticator app. Store them somewhere safe — they won't be shown again.</p>
|
||
<pre id="mfa-recovery-codes" class="mfa-recovery-codes"></pre>
|
||
<div class="dialog-actions"><button type="button" id="mfa-recovery-done" class="button primary">I've saved these codes</button></div>
|
||
</div>
|
||
</dialog>
|
||
|
||
<!-- Shared summary/indicator bar (Hosted, Proxy, Streaming, Redirect views) -->
|
||
<section id="management-summary" class="summary hidden" aria-label="Site summary"><div><span id="running-dot" class="status-dot inactive"></span><strong id="running-count">0</strong><span id="running-label">No sites running</span></div><div><span id="disabled-dot" class="status-dot inactive"></span><strong id="disabled-count">0</strong><span id="disabled-label">No disabled sites</span></div><div><span id="error-dot" class="status-dot inactive"></span><strong id="error-count">0</strong><span id="error-label">No issues</span></div><div class="port-note">Ports <strong id="port-range">9000–9099</strong></div></section>
|
||
<!-- Streaming Hosts view -->
|
||
<section id="streaming-view" class="feature-view hidden"><div id="stream-list" class="site-grid"></div><section id="stream-empty" class="empty hidden"><div class="empty-icon">⇄</div><h2>Create your first streaming host</h2><p>Forward raw TCP or UDP traffic on a specific port straight to another host and port — no domain, no HTTPS.</p><button class="button primary create-trigger">Create a streaming host</button></section></section>
|
||
<!-- Redirect Hosts view -->
|
||
<section id="redirects-view" class="feature-view hidden"><div id="redirect-list" class="site-grid"></div><section id="redirect-empty" class="empty hidden"><div class="empty-icon">↪</div><h2>Create your first redirect</h2><p>Send an old domain to a new destination while preserving its path if you choose.</p><button class="button primary create-trigger">Create a redirect host</button></section></section>
|
||
<!-- Access Lists view (content is entirely JS-rendered by features.js) -->
|
||
<section id="access-view" class="feature-view hidden"><div id="access-list" class="data-list"></div></section>
|
||
<!-- Documentation view -->
|
||
<section id="documentation-view" class="feature-view hidden docs"><div class="docs-intro"><p class="eyebrow">Site Gateway manual</p><h2>Every section, every field, explained</h2><p>A complete reference for the Dashboard, Hosted Sites, Proxy Hosts, Redirect Hosts, Streaming Hosts, Certificates, Access Lists, My Account & two-factor authentication, Administration, Performance monitoring, and every advanced control between them. Start with an area’s overview, then use its field reference when you need to know what one specific setting does.</p><div class="docs-search-panel"><label class="doc-search"><span>Search the complete manual</span><input id="doc-search" type="search" placeholder="Search “upstream TLS”, “Minecraft”, “CIDR”, “backup”, or any field name"></label><small>Searches every heading, field name, and explanation on this page.</small></div></div><div class="docs-layout"><aside class="docs-nav" aria-label="Documentation sections"><p class="eyebrow">Contents</p><button data-doc-jump="introduction">Introduction</button><button data-doc-jump="getting">Getting started</button><button data-doc-jump="dashboard">Dashboard</button><button data-doc-jump="hosted">Hosted Sites</button><button data-doc-jump="proxy">Proxy Hosts</button><button data-doc-jump="redirect">Redirect Hosts</button><button data-doc-jump="streaming">Streaming Hosts</button><button data-doc-jump="certificates">Certificates</button><button data-doc-jump="access lists overview">Access Lists</button><button data-doc-jump="users">Users & Groups</button><button data-doc-jump="my account">My Account</button><button data-doc-jump="system">System</button><button data-doc-jump="api access">API Access</button><button data-doc-jump="defaults">Gateway Defaults</button><button data-doc-jump="backup">Backup & Restore</button><button data-doc-jump="retention">Logs & Retention</button><button data-doc-jump="danger">Danger Zone</button><button data-doc-jump="access logs">Logs</button><button data-doc-jump="performance throughput">Performance</button><button data-doc-jump="common">Common Controls</button><button data-doc-jump="icons">Icons</button><button data-doc-jump="troubleshooting">Troubleshooting</button></aside><div id="docs-content"><article data-doc="introduction why built philosophy caddy novice expert overview"><p class="eyebrow">Introduction</p><h2>Why Site Gateway exists</h2><p>Reverse proxies often expose powerful settings without explaining what they change. Site Gateway provides a visual, Caddy-powered control plane for static sites, proxy routes, redirects, raw TCP/UDP streams, HTTPS, health checks, access control, and recovery — covering everything from a single ZIP upload to a full homelab of proxied applications.</p><h3>How this manual is organized</h3><p>Each area below has an <strong>overview</strong> article (what it’s for and how to configure the common case) followed by a <strong>field reference</strong> article covering every advanced control, one at a time. Use the sidebar to jump straight to a section, or search for any field name, error message, or setting — search matches headings, body text, and field names together.</p><h3>Novice path</h3><p>Create one route, test it locally, then add a domain and TLS. Keep defaults until you have a reason to change them.</p><h3>Expert note</h3><p>Configuration is stored in SQLite under <code>/data</code> and every generated Caddy configuration is validated before reload — if validation fails, the previous working configuration stays active and the failure is explained in Gateway Events.</p></article><article data-doc="getting started install first login setup administrator"><p class="eyebrow">Getting started</p><h2>From installation to your first route</h2><p>Install the container with persistent <code>/data</code> storage, open the management port (<code>8080</code> by default), and complete the administrator setup screen shown on first launch. Before publishing public domains, confirm DNS already points to this server and that ports 80 and 443 are free for Site Gateway to use.</p><h3>Choosing your first route type</h3><ul><li><strong>Hosted Site:</strong> you have static files (a ZIP or a single <code>index.html</code>) and want Site Gateway to serve them directly.</li><li><strong>Proxy Host:</strong> you already have an application running somewhere — another container, a LAN device, another server — and want a domain and HTTPS in front of it.</li><li><strong>Redirect Host:</strong> you want a domain to simply forward visitors to a different address.</li><li><strong>Streaming Host:</strong> you need to forward a raw TCP or UDP port (SSH, Minecraft, a game server) with no domain and no HTTPS involved.</li></ul><h3>Example</h3><p>Publish a ZIP on a direct port first so you can confirm it works over LAN, then add <code>www.example.com</code> once DNS and port forwarding are ready and Automatic HTTPS can issue a certificate.</p></article><article data-doc="dashboard overview metric strip live health runtime system scheduled jobs needs attention recent activity public ip throughput cpu memory swap disk network uptime hero panel resource live"><p class="eyebrow">Dashboard</p><h2>What each panel is telling you</h2><p>The Dashboard is a glance-and-go summary — every number and status on it links back to a full section elsewhere, so you never have to act on the Dashboard itself.</p><h3>Metric strip</h3><p>Counts of Hosted Sites, Proxy Hosts, and Certificates needing attention, plus a <strong>Throughput</strong> chip showing live requests in the last minute across every configured domain — click it to open the full Performance tab.</p><h3>Live Health</h3><ul><li><strong>Reverse proxy / Certificates / Persistent storage:</strong> the original health tiles.</li><li><strong>Streaming ports:</strong> how many enabled Streaming Hosts are actually bound and listening, versus configured.</li><li><strong>Upstreams:</strong> how many Proxy Hosts are currently passing their health check.</li></ul><h3>Runtime & System</h3><p>A live resource panel — CPU, Memory, Swap, Disk, Network, and Uptime — reading this container’s own cgroup v2 stats rather than host-wide numbers, so the percentages reflect what’s actually assigned to Site Gateway (CPU quota or pinned cores, an explicit memory limit if one is set, and so on), not the whole machine it happens to be running on. It refreshes automatically every few seconds while the Dashboard is open, including the live-requests count in the Throughput chip above it. This is the exact same panel shown on Administration → System, so the two can never disagree — what changed there updates here too. Version, database, and Public IP facts (rechecked automatically every 60 minutes) live on the System page now rather than the Dashboard; see that section below.</p><h3>Scheduled Jobs</h3><p>Background jobs shown as status tiles — proxy health checks, backups, log pruning, certificate/domain readiness checks, and the Public IP check — each showing its schedule and last result.</p><h3>Needs Attention</h3><p>Lists anything that needs a decision (a failing health check, an expiring certificate). Clicking an item jumps straight to it. The Configuration drift item is the exception — it has its own inline <strong>Resync now</strong> button so you can clear it without leaving the Dashboard. That button is administrator-only; a Standard User or Viewer sees the same drift warning but resolves it by asking an administrator, rather than a button that would fail for them. When nothing needs attention, the panel collapses to a single all-clear banner instead of an empty list.</p><h3>Recent Activity</h3><p>The last several configuration and operational events, with relative timestamps (“4 hours ago”). This is a shortcut, not a separate log — the same events are searchable in full under Logs → Gateway Events.</p></article><article data-doc="hosted sites overview static zip index upload configure create name domain port tls hsts"><p class="eyebrow">Hosted Sites</p><h2>Overview: publish a static website</h2><p>A Hosted Site serves files you upload directly — no separate application or container required. Use it for a static site, a single-page app build, documentation, or anything that’s just HTML/CSS/JS.</p><h3>Creating a Hosted Site</h3><ul><li><strong>Name:</strong> the label shown in Site Gateway; it doesn’t need to match the domain.</li><li><strong>Primary domain</strong> <span class="optional">Optional</span>: leave blank for port-only LAN access, or set a hostname to serve the same files there once DNS is ready.</li><li><strong>Additional domains:</strong> one alias per line — every alias serves the same files under the same TLS settings as the primary domain.</li><li><strong>Port:</strong> the direct LAN port this site answers on. Must fall inside the configured port range (shown as a hint on the form, default <code>9000–9099</code>) and not already be used by another site.</li><li><strong>TLS:</strong> <em>Automatic public HTTPS</em> (default, requires a working domain and open ports 80/443), <em>Internal HTTPS for trusted local devices</em>, or <em>HTTP only</em>.</li><li><strong>Enable HSTS after HTTPS is verified:</strong> only turn this on once you’ve confirmed HTTPS works for every client — HSTS tells browsers to refuse HTTP entirely for this domain going forward, and that’s hard to undo quickly.</li><li><strong>Website files:</strong> upload a ZIP whose root contains <code>index.html</code> (a single top-level folder inside the ZIP is automatically flattened), or upload a bare <code>index.html</code> file directly. Up to 250 MB.</li></ul><h3>Updating an existing site</h3><p>Use <strong>Replace files</strong> from the card’s menu to upload a new ZIP or HTML file without recreating the site or losing its domain/TLS/health settings. Use <strong>Domain & TLS</strong> to change the domain, TLS mode, or any advanced setting below.</p><h3>Expected behavior</h3><p>Files are served immediately on the chosen port and, once configured, through the domain as well.</p></article><article data-doc="hosted sites advanced access list compression headers hsts subdomains custom caddy configuration health monitor"><p class="eyebrow">Hosted Sites</p><h2>Advanced options and health monitoring</h2><p>Expand <strong>Advanced options</strong> on the create or edit form for these controls:</p><ul><li><strong>Access List:</strong> applies a reusable network or login restriction before any visitor reaches this site. Choose <em>Public — no Access List</em> to leave it open.</li><li><strong>Compression:</strong> <em>Automatic zstd + gzip</em> (default), <em>gzip only</em>, or <em>Off</em>.</li><li><strong>Request headers</strong> / <strong>Response headers:</strong> one <code>Name: value</code> pair per line, up to 30 each. Request headers are added before Caddy processes the request; response headers are added to what visitors receive.</li><li><strong>Apply HSTS to subdomains:</strong> adds <code>includeSubDomains</code> to the HSTS header; only takes effect when HSTS itself is on and TLS isn’t HTTP only.</li><li><strong>Custom Caddy configuration:</strong> expert-only raw Caddyfile lines appended to this site’s block, validated before reload. Lines that would touch the global config block, the admin API, storage, or <code>import</code>/<code>persist_config</code> directives are rejected outright, since those could affect every other route on the gateway.</li></ul><h3>Monitoring this site</h3><p>Hosted Sites can be health-checked the same way Proxy Hosts are: a <strong>Health-check path</strong> (default <code>/</code>), <strong>method</strong> (GET or HEAD), <strong>expected status</strong> (a code, list, or range such as <code>200</code>, <code>200,204</code>, or <code>200-499</code>), a <strong>timeout</strong> in seconds (1–60, default 4), up to 3 <strong>retries</strong>, and a <strong>Monitor this site</strong> toggle. Turning monitoring off shows the card as “Monitoring paused” instead of running a periodic check.</p></article><article data-doc="proxy hosts overview jellyfin vaultwarden plex forward upstream configure name domain target tls"><p class="eyebrow">Proxy Hosts</p><h2>Overview: connect a local application</h2><p>A Proxy Host puts a domain and HTTPS in front of something already running elsewhere — another container, a LAN device, or a remote service.</p><h3>Creating a Proxy Host</h3><ul><li><strong>Name:</strong> the label shown in Site Gateway.</li><li><strong>Primary domain</strong> and <strong>Additional domains:</strong> same behavior as Hosted Sites.</li><li><strong>Forward to:</strong> the upstream address, e.g. <code>http://192.168.1.20:8123</code>. Use the container name, LAN address, or application URL — no path or query string beyond a trailing slash.</li><li><strong>Upstream pool</strong> <span class="optional">Optional</span>: found under Advanced options — additional <code>http(s)://</code> targets, one per line, up to 10. When present, Caddy distributes requests across every healthy target instead of using the single Forward-to address alone, and the Advanced options’ Load balancing setting controls how.</li><li><strong>TLS:</strong> Automatic public HTTPS, Internal HTTPS, <em>Custom uploaded certificate</em>, or HTTP only.</li><li><strong>Enable HSTS after HTTPS is verified.</strong></li></ul><h3>Example</h3><p>For Jellyfin at <code>192.168.1.20:8096</code>, use domain <code>jellyfin.example.com</code> and forward target <code>http://192.168.1.20:8096</code>. Site Gateway checks the upstream continuously and Caddy manages an eligible public HTTPS certificate automatically.</p></article><article data-doc="proxy hosts advanced access list health expected status timeout retries compression block common exploits security custom locations headers upstream tls server name insecure hsts caddy configuration upstream pool load balancing round robin least connections ip hash sticky sessions"><p class="eyebrow">Proxy Hosts</p><h2>Advanced options, field by field</h2><p>Most applications only need a domain, Forward-to target, and TLS choice. These controls are for applications with unusual paths, authentication needs, response codes, headers, or performance requirements.</p><ul><li><strong>Access List:</strong> applies reusable login and network rules before the upstream is reached.</li><li><strong>Health-check path</strong> and <strong>method:</strong> the request Site Gateway makes when checking this application (path default <code>/</code>; GET or HEAD).</li><li><strong>Expected status:</strong> accepts a single code, a comma list, or a range — <code>200</code>, <code>200,204</code>, or <code>200-399</code>.</li><li><strong>Timeout in seconds:</strong> 1–60, default 4.</li><li><strong>Monitor this upstream:</strong> when off, the status shows “Monitoring paused” and no periodic request is made.</li><li><strong>Compression:</strong> Automatic zstd + gzip, gzip only, or Off.</li><li><strong>Block common exploits:</strong> when enabled, Site Gateway checks the request’s URL path against one fixed, built-in pattern and responds 403 before the request ever reaches the upstream if it matches. The pattern currently checks the path for: path traversal sequences (<code>../</code> or <code>..\</code>); direct requests for <code>/etc/passwd</code>; WordPress probing (<code>/wp-login.php</code>, <code>/wp-admin/</code>, <code>/xmlrpc.php</code>); exposed config/VCS paths (<code>/.env</code>, <code>/.git/</code>, <code>/.aws/</code>); exposed PHPUnit test endpoints (<code>/vendor/phpunit</code>, <code>/phpunit/</code>); the literal strings <code>eval(</code> and <code>base64_decode(</code>; a SQL <code>UNION SELECT</code> sequence; and <code><script</code>. Matching is case-insensitive. <strong>Important:</strong> this only inspects the URL path — it does not inspect the query string, request body, cookies, or headers, so a SQL-injection or XSS payload sent as a query parameter (e.g. <code>?id=1 UNION SELECT...</code>) is not caught by this toggle. This is a small, fixed, path-only ruleset, not a full web application firewall — it will not catch every attack, cannot currently be extended with custom patterns, and should not replace keeping the upstream application itself patched.</li><li><strong>Custom Locations:</strong> send specific paths to a different upstream. One per line: <code>/api/* | http://192.168.1.20:3001 | strip</code> or <code>preserve</code> — <em>strip</em> removes the matched prefix before forwarding, <em>preserve</em> keeps it. Up to 20 entries.</li><li><strong>Request headers</strong> / <strong>Response headers:</strong> same <code>Name: value</code>-per-line format as Hosted Sites.</li><li><strong>Upstream TLS server name:</strong> an optional SNI name expected by the upstream certificate. Only meaningful when every upstream target uses <code>https://</code> — the field is disabled otherwise.</li><li><strong>Ignore upstream TLS certificate errors:</strong> use only for a trusted internal HTTPS service with a self-signed or hostname-mismatched certificate; also requires an HTTPS upstream to take effect.</li><li><strong>Apply HSTS to subdomains.</strong></li><li><strong>Custom Caddy configuration:</strong> same validation rules as Hosted Sites’ custom configuration. NGINX syntax is not supported — Site Gateway generates Caddyfile syntax.</li></ul><li><strong>Upstream pool:</strong> additional <code>http(s)://</code> targets, one per line, up to 10 — the same field described in the Overview, now editable here too on an existing Proxy Host, not just when first creating it. Once Upstream pool has any entries, Caddy uses only those targets — the single Forward-to address above is not automatically added to the pool alongside them, so include it as one of the pool lines too if it should keep receiving traffic.</li><li><strong>Load balancing:</strong> only takes effect once Upstream pool has 2 or more entries — Forward to is never counted toward this, per the note above. <em>Random</em> (Caddy’s own default) picks a target at random per request; <em>Round Robin</em> rotates through targets evenly; <em>Least Connections</em> sends each request to whichever target currently has the fewest active requests; <em>IP Hash</em> sends a given client’s requests to the same target each time (sticky sessions), useful for applications that keep session state on one backend. This only affects which upstream Caddy sends a request to — it does not change the separate Health-check settings below, which only drive Site Gateway’s own dashboard status.</li><h3>How to verify a change</h3><p>Save one setting at a time, watch the card’s upstream status line, check Access Logs, and compare against a direct request to the application. If Caddy rejects a custom configuration, Site Gateway keeps the last known-good configuration active and shows the rejection reason.</p></article><article data-doc="proxy custom certificate upload pem private key mismatch"><p class="eyebrow">Proxy Hosts</p><h2>Custom uploaded certificates</h2><p>Choosing <strong>Custom uploaded certificate</strong> as the TLS mode reveals two file fields: <strong>Certificate PEM</strong> and <strong>Private key PEM</strong>. Both are required together — Site Gateway verifies the private key’s public key actually matches the certificate before accepting the pair, and confirms the certificate’s Subject Alternative Names cover every domain configured on that Proxy Host. A certificate that doesn’t cover every alias is rejected rather than silently applied to only some of them.</p><p>Accepted files are stored under <code>/data/certificates/custom</code> and included in complete backups. Replacing a custom certificate later works the same way — upload both files again through the same field.</p></article><article data-doc="redirect hosts overview permanent temporary path query preserve configure"><p class="eyebrow">Redirect Hosts</p><h2>Overview: move an address safely</h2><p>A Redirect Host sends visitors from one domain straight to another, with no files hosted and no application behind it.</p><h3>Fields</h3><ul><li><strong>Name.</strong></li><li><strong>Source domain:</strong> the old address people currently use.</li><li><strong>Additional source domains:</strong> further aliases that redirect the same way.</li><li><strong>Destination:</strong> a complete <code>http://</code> or <code>https://</code> URL to send visitors to.</li><li><strong>Redirect type:</strong> <code>302</code> Temporary (default), <code>301</code> Permanent, <code>307</code> Temporary — preserve method, <code>308</code> Permanent — preserve method.</li><li><strong>TLS:</strong> Automatic HTTPS, HTTP only, or Internal HTTPS — this governs the source domain, not the destination.</li><li><strong>Preserve path and query</strong> (on by default): when enabled, <code>old.example.com/library?id=2</code> becomes <code>new.example.com/library?id=2</code> instead of always landing on the destination’s root.</li><li><strong>Access List</strong> <span class="optional">Optional</span>: protects the source domain the same way it protects Hosted Sites and Proxy Hosts.</li></ul><h3>Choosing a redirect type</h3><p>Use 301 or 308 only when the move is meant to be permanent — browsers and search engines cache these aggressively. Use 302 or 307 while you’re still testing the new destination.</p></article><article data-doc="streaming hosts overview tcp udp port forward raw minecraft ssh game server configure port target"><p class="eyebrow">Streaming Hosts</p><h2>Overview: raw TCP/UDP port forwarding</h2><p>A Streaming Host forwards a raw TCP or UDP port straight to another host and port — there’s no domain, no HTTPS, and no HTTP layer involved at all, unlike every other route type in Site Gateway. Use it for services that speak their own protocol directly over a port: SSH, a Minecraft or other game server, a VPN endpoint, or any similar TCP/UDP service.</p><h3>The one prerequisite: publish the port first</h3><p>Streaming Hosts listen for connections from <em>inside</em> the Site Gateway container itself — they are not routed through Caddy the way domains are. That means the incoming port must already be published on the container (added to your <code>docker-compose.yaml</code> or Unraid port mappings, with the container recreated) <strong>before</strong> you create the matching Streaming Host in the UI, or nothing outside the container will ever reach it.</p><h3>Example docker-compose port mapping</h3><p>To forward SSH (22) and a Minecraft server (25565):</p><p><code>ports:<br> - "22:22/tcp"<br> - "25565:25565/tcp"</code></p><h3>Creating a Streaming Host</h3><ul><li><strong>Name.</strong></li><li><strong>Incoming port:</strong> 1–65535. Cannot be the admin port, 80, 443, a port already inside the Hosted Sites port range, or a port already used by another Streaming Host.</li><li><strong>Forward to:</strong> a plain <code>host:port</code> address, e.g. <code>192.168.1.20:25565</code>. No <code>http://</code> — this is a raw socket forward, not a web address.</li><li><strong>TCP</strong> / <strong>UDP:</strong> enable one or both. At least one must stay checked.</li><li><strong>Monitor this target:</strong> runs a TCP reachability probe against the forward address every check cycle, even for UDP-only streams (UDP itself has no reliable way to “ping” a service).</li></ul></article><article data-doc="streaming hosts how it works tcp relay udp relay session multiplex health probe examples"><p class="eyebrow">Streaming Hosts</p><h2>How the forwarding actually works</h2><p><strong>TCP</strong> is a full one-to-one relay: every inbound connection gets its own fresh outbound connection to the forward target, and the two are piped together in both directions. Closing or erroring either side tears down the other.</p><p><strong>UDP</strong> is connectionless, so Site Gateway multiplexes many remote clients over one listening port by tracking a short-lived “session” per unique client address, each with its own outbound socket to the forward target. An idle session is cleaned up automatically after about a minute of inactivity.</p><h3>Health monitoring</h3><p>The monitor always performs a plain TCP reachability check against the forward address, with a 4-second timeout — this confirms the target host and port are reachable, not that the specific game or service protocol is fully healthy. Disabling monitoring shows the card as “Monitoring paused” rather than unreachable.</p><h3>Practical examples</h3><ul><li><strong>SSH:</strong> incoming port <code>2222</code>, forward to <code>192.168.1.30:22</code>, TCP only.</li><li><strong>Minecraft:</strong> incoming port <code>25565</code>, forward to <code>192.168.1.20:25565</code>, TCP (and UDP if the specific server/mod needs it — vanilla Minecraft is TCP-only).</li></ul><h3>Editing a Streaming Host</h3><p>Changing the port, target, or protocol checkboxes restarts the listener immediately to apply the change; toggling Enable/Disable does the same.</p></article><article data-doc="certificates overview automatic https prerequisites dns ports check now readiness"><p class="eyebrow">Certificates</p><h2>Overview: automatic HTTPS prerequisites</h2><p>For Automatic HTTPS to succeed, a domain must resolve to your public address, inbound ports 80 and 443 must reach Site Gateway, and no other service (including another reverse proxy) can already own those ports on this host.</p><h3>Domain readiness checks</h3><ul><li><strong>DNS:</strong> confirms the domain actually resolves, and to what address.</li><li><strong>HTTP / HTTPS:</strong> confirms Caddy itself is listening on ports 80/443 inside the container — this does not by itself prove the internet can reach you, only that the gateway is ready to answer if it can.</li><li><strong>TLS:</strong> reflects the certificate status for that domain (healthy, renewing soon, critical, expired, or “Waiting for Caddy,” meaning issuance hasn’t completed yet — not that anything is broken).</li><li><strong>Upstream</strong> (Proxy Hosts only): the latest health-check result for that route.</li></ul><h3>Check now</h3><p>Forces an immediate re-check of every certificate, domain readiness result, and health probe instead of waiting for the periodic background check. Administrator-only — the button is hidden for Standard Users and Viewers, who see the same readiness and certificate results but can’t force an early check.</p><h3>Novice workflow</h3><p>Confirm DNS, forward ports 80 and 443, stop any competing proxy, then run Check now. Don’t troubleshoot an upstream application until the domain and HTTPS checks themselves are healthy.</p><h3>Certificate health thresholds</h3><p>See Certificates → Field reference for what Renewing-soon warning, Critical warning, and Stale health data actually control.</p></article><article data-doc="certificates field reference issuer expiration warning critical thresholds serial fingerprint"><p class="eyebrow">Certificates</p><h2>Field reference and thresholds</h2><p>Expanding a certificate’s details shows: status, valid-from date, issuer, every domain it covers, serial number, SHA-256 fingerprint, and when it was last detected — deliberately excluding private key material.</p><h3>Status thresholds</h3><p>Set from Administration → Security & Health:</p><ul><li><strong>Renewing-soon warning:</strong> days remaining before a certificate is flagged as renewing soon (8–120, default 30).</li><li><strong>Critical warning:</strong> days remaining before it’s flagged critical; must stay lower than the renewing-soon threshold (default 7).</li><li><strong>Stale health data:</strong> minutes before a displayed check is considered old and worth re-running (2–1440, default 10).</li></ul><p>A status of <strong>mismatch</strong> means a custom certificate was uploaded for that route but its coverage doesn’t include every configured domain — upload a replacement covering all of them.</p></article><article data-doc="access lists overview protect lan authentication networks denied login groups assignment group enabled disabled toggle"><p class="eyebrow">Access Lists</p><h2>Overview: protect a route</h2><p>An Access List restricts who can reach a Hosted Site, Proxy Host, or Redirect Host, by network address, by login, or both. Assign a saved list from that route’s Advanced options.</p><h3>Fields</h3><ul><li><strong>Name.</strong></li><li><strong>Allowed networks:</strong> one IP, CIDR range, or the literal <code>private_ranges</code> per line. When set, every network <em>not</em> listed is denied — this is an allow-list, not a suggestion.</li><li><strong>Denied networks</strong> <span class="optional">Optional</span>: evaluated <em>before</em> allowed networks and logins, so a denied entry always wins even if it would otherwise be allowed.</li><li><strong>Logins:</strong> add one or more username/password pairs directly on the Access List (passwords need at least 8 characters; leaving a password blank while editing an existing login keeps it unchanged).</li><li><strong>Groups:</strong> alternatively, let members of a Group (Administration → Users & Groups) authenticate with their own Site Gateway username and password instead of a separate Access-List-only login. A disabled Group (Administration → Users & Groups) stops granting access immediately, even though it still appears assigned here — see Users & Groups for details. Assigning Groups is an administrator-only action: a Standard User can still fully manage everything else on an Access List, but this field is replaced with a note pointing them to an administrator, since Groups themselves live entirely inside Administration.</li></ul><p>At least one rule — a network rule, a denied-network rule, or a login — is required before the list can be saved.</p><h3>Assigning and inspecting</h3><p>Use a route’s Advanced options to assign an Access List to it, or use the Access List’s own <strong>View assigned hosts</strong> menu action to see everywhere it’s currently in use. An Access List can’t be deleted while anything still references it — unassign it from every host first.</p></article><article data-doc="users groups overview administrator standard viewer role reset password archive audit group enable disable toggle access"><p class="eyebrow">Users & Groups</p><h2>Overview: control who can change the gateway</h2><p>Every account has a role. Roles work on two independent questions: can this account see and use <strong>Administration</strong> (System, Users, Groups, Backups, API Access, Logs & Retention, Danger Zone) at all, and can it <em>change</em> Hosted Sites, Proxy Hosts, Redirect Hosts, Streaming Hosts, and Access Lists (everyone who’s signed in can already view those, along with the Dashboard, Certificates, Performance, and Logs pages).</p><ul><li><strong>Administrator:</strong> the only role with any access to Administration — System, Users, Groups, Backups, API Access, Logs & Retention, and Danger Zone are entirely invisible to the other two roles, not merely read-only. Also the only role that can run a certificate/domain check, resync gateway configuration after drift, or assign Groups to an Access List.</li><li><strong>Standard User:</strong> can create, edit, delete, and enable/disable Hosted Sites, Proxy Hosts, Redirect Hosts, Streaming Hosts, and Access Lists — including a list’s network rules and login credentials. Cannot open Administration in any form, and within an Access List, cannot assign Groups (that control is replaced with a note pointing to an administrator), since Groups are themselves an Administration-only concept.</li><li><strong>Viewer:</strong> read-only across everything a Standard User can see — Dashboard, Hosted Sites, Proxy Hosts, Redirect Hosts, Streaming Hosts, Access Lists, Certificates, Performance, and Logs — with no create, edit, delete, or toggle controls anywhere. Like Standard, Administration is entirely out of view for a Viewer, not just non-editable.</li></ul><h3>Creating a user</h3><ul><li><strong>Display name.</strong></li><li><strong>Username:</strong> 3–64 characters, letters/numbers/<code>._-</code>, must be unique.</li><li><strong>Role.</strong></li><li><strong>Temporary password:</strong> at least 8 characters — share it securely and have the person change it after their first sign-in.</li></ul><h3>Managing an existing user</h3><p>An administrator can change a user’s role from the card’s role dropdown, reset their password, archive them (a reversible soft-disable distinct from deleting), or delete them outright. Delete and Change icon live under the card’s “•••” menu, matching Hosted Sites and Proxy Hosts; role, password reset, and archive stay as visible buttons. You cannot disable, archive, delete, or change the role of your own account — another administrator has to do that — and Site Gateway always keeps at least one active Administrator, refusing any action that would leave zero. Resetting a user’s password here does not disable their two-factor authentication if they have it enabled — see My Account for how 2FA works. If a user is locked out of their own 2FA (lost authenticator, no recovery codes left), an administrator can disable it for them from the card’s “•••” menu — <strong>Disable 2FA</strong> appears there only when that user currently has 2FA enabled. This clears their authenticator, secret, and any unused recovery codes; they can set 2FA up again afterward if they choose to. The action is logged to the Audit log, since it removes a security control from someone else’s account.</p><h3>Groups</h3><p>A Group is simply a named set of users. Its only purpose is authentication: assign a Group to an Access List so its members can log in with their own Site Gateway credentials instead of a separate Access-List-only login.</p><p>Every Group has its own enable/disable toggle, separate from deleting it. <strong>Disabling a Group immediately stops it from granting access through any Access List it’s assigned to</strong> — the assignment itself is untouched and still shows as assigned, but its members can no longer authenticate through it until the Group is re-enabled. This is a common surprise: disabling a Group is not the same as archiving it for later cleanup, it has an immediate access-control effect. If the Group is currently assigned to at least one enabled Access List, disabling it now asks for confirmation first, naming exactly which Access List(s) will stop authenticating that Group’s members — the same pattern already used when disabling an Access List that protects active hosts.</p><h3>Audit history</h3><p>Administration includes an Audit log — a searchable, filterable, immutable record of who did what, filterable by outcome (success/failed) and free-text search across the user, action, and target. Audit entries can’t be edited or deleted.</p></article><article data-doc="my account profile password change two-factor authentication mfa totp authenticator recovery codes self-service"><p class="eyebrow">My Account</p><h2>Managing your own profile, password, and two-factor authentication</h2><p>My Account is available to every role — Administrator, Standard User, and Viewer alike — and only ever affects your own account. It sits in the sidebar next to Administration and Documentation.</p><h3>Profile</h3><p>Shows your display name, username, and role. These are read-only here; an administrator changes them from Administration → Users.</p><h3>Changing your password</h3><p>Enter your current password once, then your new password twice. This works for every role — previously, only an administrator could change a user’s password (Administration → Users → Reset password), which meant every routine password change had to go through an admin. My Account closes that gap for everyone’s own account.</p><h3>Two-factor authentication (2FA)</h3><p>Optional and off by default for every account. When enabled, signing in requires your password plus a 6-digit code from an authenticator app (or a recovery code), using the standard TOTP algorithm (RFC 6238) — any authenticator app works, not a Site Gateway-specific one.</p><h3>Enabling 2FA</h3><ol><li>Choose Enable two-factor authentication.</li><li>Scan the QR code with your authenticator app, or enter the shown key manually.</li><li>Enter the 6-digit code it generates to confirm setup.</li><li>Save the 10 recovery codes shown immediately afterward — each works once, and this is the only time they’re shown in full.</li></ol><h3>Signing in with 2FA enabled</h3><p>After your username and password are accepted, you’re prompted for a code. Enter the current 6-digit code from your authenticator app, or one of your unused recovery codes if you don’t have the app available. A wrong code shows an inline error without sending you back to the username/password screen.</p><h3>Disabling 2FA, or regenerating recovery codes</h3><p>Both require re-entering your current password as confirmation. Regenerating recovery codes immediately invalidates the previous set.</p><p>An administrator resetting your password does not disable your 2FA — you’ll still need your authenticator app or a recovery code at your next sign-in. Only you can turn off your own 2FA, from My Account.</p></article><article data-doc="system environment integrations docker socket backup_password encryption acme_email security status admin_password session_secret storage disk usage scheduled jobs gateway sync resync reload restart restart policy cpu memory swap network throughput hero panel resource live data_dir_limit_gb quota pinned cores uptime caddy version database public ip"><p class="eyebrow">Administration</p><h2>System</h2><p>A read-only operations and diagnostics page — what’s configured, what’s running, and what this deployment can do. The only things you can actually change here are the Docker container-picker toggle and the action buttons; everything else is status.</p><h3>Live resource panel</h3><p>The same hero panel shown on the Dashboard — CPU, Memory, Swap, Disk, Network, and Throughput — reading this container’s own cgroup v2 stats (not host-wide numbers), so pinning or limiting the container changes what these percentages are measured against. CPU is measured against a real <code>--cpus</code> quota when one is set, pinned cores (<code>--cpuset-cpus</code>) when there’s no quota, or the host’s full core count as a last resort — the detail line under the number says which one applies. Swap only shows a percentage when the container has a real <code>--memory-swap</code> limit; otherwise it shows bytes in use with "Unlimited — shares host swap" rather than implying a cap that isn’t there. Disk compares against the full <code>/data</code> volume by default, or against an assigned allowance if <code>DATA_DIR_LIMIT_GB</code> is set on the container — useful when <code>/data</code> lives on a share or pool with other things on it, since the percentage then reflects what Site Gateway itself has written, not the whole volume’s usage; going over that assigned allowance shows over 100% and turns red rather than silently capping. The panel refreshes automatically every few seconds while this tab is open, and every signed-in user can see it, not just administrators.</p><h3>Environment & integrations</h3><p>Shows whether the Docker socket is mounted (and, if so, the toggle that lets Proxy and Streaming Hosts pick a running container as their target — moved here from Gateway Defaults) and whether <code>BACKUP_PASSWORD</code> is configured on the container (the actual "Encrypt scheduled backups" toggle it enables stays on the Backup & Restore page, next to the setting it drives).</p><p>If you’ve mounted <code>/var/run/docker.sock</code> and this still shows "not detected," it’s almost always a permissions issue rather than a missing mount. The socket is typically owned <code>root:docker</code> on the host with mode 660, but Site Gateway runs as the unprivileged <code>PUID</code>/<code>PGID</code> you set, not root. The container’s entrypoint handles this automatically at startup — while still root, it reads the socket’s actual group off the mount and adds the app user to it before dropping privileges — so a fresh container start after adding the mount should be enough; this detection only runs once at boot, so an existing running container needs to be restarted (not just have its mount added) to pick it up.</p><h3>Security status</h3><p>Read-only flags for whether <code>ADMIN_PASSWORD</code> or <code>SESSION_SECRET</code> are still on their built-in defaults, and whether <code>ACME_EMAIL</code> is set.</p><h3>Gateway sync</h3><p>Current configuration-drift status and a persistent <strong>Resync now</strong> control — the same action available inline on the Dashboard’s Needs Attention list, always available here too.</p><h3>Scheduled jobs and storage</h3><p>A table of background jobs with their schedule and last result, and a per-folder breakdown of disk usage under <code>/data</code> (sites, backups, certificates, logs, database) with total used and remaining capacity.</p><h3>Version</h3><p>Site Gateway and Caddy versions, container uptime, database engine/status/size, and the gateway’s current Public IP (rechecked automatically every 60 minutes, prep work for future Dynamic DNS support) — alongside the dashboard access URL, data directory, and configured site port range that were already here. These moved from the Dashboard when its Runtime/System panel became the live resource hero panel above, so operational and version facts about the deployment now live in one place instead of being split across two pages. Uptime here refreshes on the same few-second cadence as the hero panel above it, rather than only when this page’s other details are refetched.</p><h3>Reload & restart</h3><p><strong>Reload gateway config</strong> re-applies the current configuration to Caddy with no downtime and is always available. <strong>Restart application</strong> stops and restarts the whole application — it only becomes available when the Docker socket is mounted and the container’s own restart policy (checked via the Docker Engine API) is <code>always</code>, <code>unless-stopped</code>, or <code>on-failure</code>; without a qualifying restart policy, the container would not come back on its own, so the button stays disabled with an explanation instead.</p></article><article data-doc="api access tokens programmatic scope full read-only revoke expire bearer"><p class="eyebrow">Administration</p><h2>API Access</h2><p>Issue bearer tokens so scripts and integrations can call the Site Gateway API without signing in interactively. A token acts as the administrator who issued it — it can do anything that administrator could do through the API, scoped as described below — and its full value is shown to you only once, at creation.</p><h3>Creating a token</h3><p>Use the <strong>Create token</strong> button (same shared button used to create users, groups, and hosts elsewhere in Administration). You’ll re-enter your own administrator username and password to confirm, then set:</p><ul><li><strong>Token name:</strong> a label to tell tokens apart later — name it after what will use it (for example, “Home Assistant integration”).</li><li><strong>Scope:</strong> <strong>Full access</strong> can read and change configuration; <strong>Read-only</strong> is restricted to GET requests only.</li><li><strong>Expires after:</strong> optional — a number of days after which the token stops working on its own. Leave it empty for a token that never expires.</li></ul><p>After creation, the token’s full value is displayed exactly once in a “copy it now” dialog — only its hash is stored, so if you close that dialog without copying it, the value can’t be retrieved again and you’ll need to issue a new token.</p><h3>The token list</h3><p>Tokens are shown as cards, the same tile layout used for Hosted Sites and Groups, with a status bar above summarizing how many are active, revoked, full-access, and read-only at a glance — the Full access/Read-only counts only tally active tokens, so they stay consistent with the Active/Revoked split next to them. Each card shows the token’s name, its prefix (enough to identify it without exposing the full secret), its scope, who issued it and when, and when it was last used (or “Never used”) plus its expiration date if one was set. Each card also has a “•••” menu, matching Hosted Sites, Groups, and every other tile — <strong>Change icon</strong> lives there, and <strong>Revoke token</strong> too when the token is still active.</p><h3>Revoking a token</h3><p>Revoking is permanent — Site Gateway does not support re-enabling a revoked token, and there is no way to delete one outright. If you need it again, issue a new one. A token is also revoked automatically, along with every other token the same administrator issued, if that administrator’s account is disabled or their password is changed — this keeps a token from outliving the credentials that vouched for it.</p><p>Deleting a revoked token isn’t supported on purpose: even though it can no longer authenticate, its record (who issued it, when, its scope, and when it was cut off) stays part of the accountability trail, the same reasoning behind the Audit log never allowing entries to be edited or deleted. If the list gets long, use the <strong>Hide revoked</strong> toggle in the token summary bar to focus on active tokens without losing that history.</p></article><article data-doc="gateway defaults default site welcome themed 404 abort redirect custom html unknown hostnames live preview real time"><p class="eyebrow">Administration</p><h2>Gateway Defaults: handling unknown addresses</h2><p>Controls what happens when a visitor reaches Site Gateway on HTTP using a hostname that isn’t configured. (Unknown HTTPS hostnames are always rejected outright, regardless of this setting, since serving anything else would need a certificate Site Gateway doesn’t have — and issuing a misleading one would be worse.)</p><h3>Response modes</h3><ul><li><strong>Themed route-not-found page (404)</strong> — the safest public default.</li><li><strong>Gateway ready page (200)</strong> — useful while confirming HTTP routing during initial setup.</li><li><strong>No response — close connection.</strong></li><li><strong>Redirect elsewhere:</strong> set a destination URL, redirect code, and whether to preserve the requested path and query.</li><li><strong>Custom HTML:</strong> administrator-authored markup, served exactly as written with no sanitization — up to 250,000 characters.</li></ul><p>The themed page’s heading and explanation text are also editable here, independent of which mode is active. It displays the Site Gateway icon and wordmark, matching the branding used throughout the rest of the app.</p><h3>Live preview</h3><h3>Configuration drift detection</h3><p>Every 10 minutes (and once shortly after startup), Site Gateway compares Caddy’s live running configuration against what your saved routes would currently generate. If they disagree — for example after a manual edit outside the app, or a Caddy restart that didn’t pick up the latest reload — a “Configuration drift” item appears on the Dashboard’s Needs Attention list with its own inline “Resync now” button, which simply re-runs the normal save-and-reload path and clears the flag; it does not change any of your saved settings. The first time drift is detected, it’s also recorded as a Gateway Events entry.</p></article><article data-doc="backup restore schedule retention encryption complete configuration import restore checklist"><p class="eyebrow">Administration</p><h2>Backup & Restore</h2><p>Every place you create a backup — scheduled or the manual “Create backup” dialog — asks for a <strong>Backup type</strong>. A <strong>Configuration only</strong> backup contains a consistent SQLite snapshot of every route, user, group, Access List, and setting, plus a portable JSON export of the same data. A <strong>Complete</strong> backup (the recommended default) adds uploaded Hosted Site files, icons, the default-site page, and certificates (both managed and custom).</p><h3>Scheduled backups</h3><ul><li><strong>Enable scheduled backups.</strong></li><li><strong>Backup type:</strong> Complete (the recommended default) or Configuration only. If scheduled backups are enabled while Configuration only is selected, a warning appears explaining that Hosted Site files, icons, and certificates won’t be included.</li><li><strong>Schedule:</strong> Daily, Weekly, or Monthly, plus the hour of day to run.</li><li><strong>Keep:</strong> how many scheduled backups to retain (1–100, default 7) — older ones beyond this count are deleted automatically after each run.</li><li><strong>Include logs.</strong></li><li><strong>Encrypt scheduled backups:</strong> uses the container’s <code>BACKUP_PASSWORD</code> environment value. This checkbox is disabled with an explanation if <code>BACKUP_PASSWORD</code> isn’t currently set on the container — it can’t be turned on until that value exists. If it was previously enabled and <code>BACKUP_PASSWORD</code> was later removed from the container’s environment, it stays checked but shows a warning instead of failing silently at the next scheduled run — set the variable again (and restart the container) to resolve it.</li></ul><h3>Manual backup, encryption password</h3><p>The optional backup password field on this page is used only for manually created backups and for restoring an encrypted archive — it is never stored by Site Gateway. A manually created backup is saved to <code>/data/backups</code> and appears in the list below; it does not download automatically — use that entry’s Download button when you want a local copy.</p><h3>Restore checklist</h3><ol><li>Download or import the <code>.sgbackup</code> archive (importing just stages the file — restoring is a separate, explicit action).</li><li>Supply its password if it’s encrypted.</li><li>Choose Restore and allow validation to finish.</li><li>Confirm hosts, certificates, and upstream health afterward.</li></ol><p>Site Gateway verifies every file’s checksum and automatically creates a safety backup of the current state before restoring anything. If the restored configuration turns out to be invalid, it automatically rolls back to that safety backup rather than leaving the gateway in a broken state.</p><h3>Configuration safety & updates</h3><p>Site Gateway validates every generated Caddy configuration before reload and keeps the previous working configuration active if validation fails. Container updates are installed by pulling a new pinned image — create a backup first.</p></article><article data-doc="logs retention pruning warning critical stale access activity audit certificate security records"><p class="eyebrow">Administration</p><h2>Logs & Retention</h2><p>Set how many days of Access, Activity, Audit, Certificate, and Security records to keep (7–3650 days each) and whether automatic pruning is enabled. <strong>Prune Now</strong> shows exactly how many records in each category are eligible before you confirm, and <strong>Download Logs</strong> exports them.</p></article><article data-doc="danger zone restore defaults factory reset credentials confirmation countdown"><p class="eyebrow">Administration</p><h2>Danger Zone</h2><p>Two separate, deliberately distinct destructive actions — kept apart so a routine preference correction is never confused with a full rebuild. Both require re-entering your own administrator username and password, typing an exact confirmation phrase, and then confirming a second themed dialog by typing <strong>YES</strong>.</p><h3>Restore Defaults</h3><p>Confirmation phrase: <code>RESTORE DEFAULT</code>. Resets Gateway Defaults, backup schedule settings, and certificate-health thresholds back to their starting values. It does <strong>not</strong> remove any hosts, uploaded files, users, groups, Access Lists, certificates, logs, or backups.</p><h3>Factory Reset</h3><p>Confirmation phrase: <code>FACTORY RESET</code>. Deletes everything under <code>/data</code> — every host of every kind, uploaded files, certificates (managed and custom), logs, backups, icons, users, and settings. Docker-mounted files outside <code>/data</code> are untouched. The container returns to the initial setup screen without needing a manual restart. Because backups themselves live under <code>/data/backups</code>, they’re deleted too — recovery is only possible from a backup taken beforehand and stored elsewhere (downloaded, or on separately mounted storage).</p><h3>When to use a backup instead</h3><p>If you want to undo a recent change while keeping the rest of the installation intact, restore a backup — Factory Reset is not a rollback tool.</p></article><article data-doc="logs access logs gateway events requests response status domain filters severity category redacted"><p class="eyebrow">Logs</p><h2>Access Logs and Gateway Events</h2><h3>Access Logs</h3><p>Every request Caddy handles, with domain, path, response status, latency, and upstream outcome. Filter by host or by status-code range (2xx/3xx/4xx/5xx). Sensitive query-string values — tokens, secrets, passwords, session identifiers, API keys, credentials — are redacted before they’re ever stored, regardless of filter settings.</p><h3>Gateway Events</h3><p>Configuration and operational changes, filterable by severity (Normal/Warnings/Errors) and by category (certificate, health, authentication, backup, configuration, or system).</p><h3>Example</h3><p>Filter Access Logs for a 502, then compare the target address against a direct LAN request to the same upstream to isolate whether the problem is the gateway or the application itself.</p></article><article data-doc="performance throughput trend sparkline per-route domain range last hour last 24h average response time sticky header unconfigured"><p class="eyebrow">Performance</p><h2>Throughput by domain, over time</h2><p>Built entirely from the same request data already collected for Access Logs — no new logging or extra overhead, just a different view of it.</p><h3>Filters</h3><p><strong>Domain</strong> narrows the Trend chart to one host (default: all domains combined). <strong>Range</strong> sets the Trend window — Last hour, 3, 6 (default), 12, or 24 hours, or 3 or 7 days.</p><h3>Trend</h3><p>Request volume over the selected range, bucketed into 15-minute (up to 24h) or hourly (3–7 days) points.</p><h3>Per-route table</h3><p>One row per domain that has received traffic, sorted by 24-hour volume. The column header row stays pinned in place while the table scrolls, so the column each figure belongs to is always visible on longer lists.</p><ul><li><strong>Last hour / Last 24h:</strong> request count for that window.</li><li><strong>Avg. response:</strong> the mean response time across every request to that domain in the last 24 hours — a fixed 24h window regardless of the Range filter above, and a straight average, so a handful of slow outliers can pull it up more than most visitors actually experience.</li><li><strong>Top paths:</strong> a per-domain “View” popout listing the top 10 most-requested paths on that domain in the last 24 hours, with request counts.</li><li><strong>Unconfigured chip:</strong> marks a domain in this table that doesn’t match any currently configured Hosted Site, Proxy Host, Redirect Host, or Streaming Host. This table is built straight from Caddy’s raw access log with no cross-reference against your configured hosts, so it reflects every Host header Caddy has ever seen — including scanning and bot traffic aimed at hostnames that were never set up here. Those requests fall through to the Default Site handler, which only ever serves a static page and never proxies anywhere, so an unconfigured entry here — even with meaningful traffic volume — is not a sign that an unconfigured site is actually being served.</li></ul></article><article data-doc="common interface controls menus three dots edit disable delete enable toggle role gating viewer standard administrator"><p class="eyebrow">Common Interface Controls</p><h2>Menus, toggles, and role gating</h2><p>The same card language is used throughout Hosted Sites, Proxy Hosts, Redirect Hosts, Streaming Hosts, Access Lists, Groups, and Users, so learning one area transfers directly to the next.</p><h3>The three-dot menu</h3><p>Contains actions that change or inspect a card: <strong>Edit</strong> opens the full form, kind-specific extras appear where relevant (Replace files on Hosted Sites, View assigned hosts on Access Lists), and <strong>Delete</strong> removes the record after a confirmation.</p><h3>The toggle switch</h3><p>A slide switch, separate from the menu, turns a route or account on or off without deleting its saved configuration — useful during maintenance or testing when you expect to reuse the exact same settings shortly after.</p><h3>Who can use each control</h3><p>Administrators can manage everything. Standard Users can manage Hosted Sites, Proxy Hosts, Redirect Hosts, Streaming Hosts, and Access Lists, but not Users, Groups, Settings, or Backups. Viewers can inspect information but cannot create, edit, disable, assign, or delete anything — their menus and toggles are hidden entirely. This is enforced on the server independently of what the interface shows, so hiding a button is a convenience, not the actual security boundary.</p><h3>Update notifications</h3><p>While signed in, Site Gateway checks every 60 seconds whether a newer version has been deployed. If so, a small banner appears with Refresh and Dismiss — Refresh reloads the page to pick up the new version; Dismiss hides the banner, but it reappears on the next check if you’re still on the old version. This only matters for a tab left open across a deploy; closing and reopening the tab, or signing in fresh, always loads the current version automatically.</p></article><article data-doc="icons change icon picker search catalog custom upload url fallback initials"><p class="eyebrow">Icons</p><h2>Changing a card’s icon</h2><p>Every Hosted Site, Proxy Host, Redirect Host, Streaming Host, Access List, Group, and User can have its own icon. Open the picker from a card’s icon tile or its “Change icon” menu action.</p><ul><li><strong>Search by service name:</strong> type at least 2 characters (e.g. <code>Jellyfin</code>, <code>Plex</code>) to search a large built-in icon catalog and pick a match.</li><li><strong>Upload a custom image:</strong> PNG, JPEG, WebP, GIF, or SVG, up to 2 MB, stored locally under <code>/data/icons</code>. Uploaded SVGs are checked for embedded scripts or external references before being accepted.</li><li><strong>Image URL:</strong> paste a direct <code>https://</code> image link instead of uploading a file.</li><li><strong>Use two-letter fallback:</strong> clears any icon and reverts to initials derived from the name.</li></ul><p>If a custom icon URL ever stops loading, the card automatically falls back to showing initials instead of a broken image.</p></article><article data-doc="troubleshooting dns ports certificate caddy nginx conflict https not detected"><p class="eyebrow">Troubleshooting</p><h2>When HTTPS is not detected</h2><p>Confirm public DNS actually points to this server, that router/firewall forwarding reaches ports 80 and 443, and that NGINX Proxy Manager or another reverse proxy isn’t still holding those ports. Then check Certificates and Logs → Gateway Events for the specific rejection reason. Site Gateway cannot request a public certificate while another gateway is receiving the ACME challenge on its behalf.</p><h3>A route rejected its configuration</h3><p>Check the error shown on save — it names the specific problem (duplicate address, invalid upstream, malformed custom configuration, certificate/TLS issue) rather than a generic failure, and the previous working configuration stays active while you fix it.</p><h3>A Streaming Host isn’t reachable from outside</h3><p>This is almost always the port not being published on the container yet — see Streaming Hosts → Overview for the docker-compose/Unraid port mapping requirement.</p><h3>A user is locked out after enabling two-factor authentication</h3><p>If they still have an unused recovery code, they can sign in with it in place of the 6-digit code. If not, an administrator can disable that user’s 2FA from Administration → Users — open the locked-out user’s “•••” menu and choose <strong>Disable 2FA</strong>. The user can sign in with just their password afterward and set 2FA up again whenever they’re ready.</p></article></div></div><p id="doc-empty" class="quiet-state hidden">No guide matched that search.</p></section>
|
||
<!-- Hosted Sites / Proxy Hosts view (shared grid + empty state) -->
|
||
<div id="management-view" class="hidden">
|
||
<section id="empty" class="empty hidden">
|
||
<div class="empty-icon">↗</div><h2>Publish your first site</h2>
|
||
<p>Drop in a ZIP containing an <code>index.html</code> and choose a port. That’s it.</p>
|
||
<button class="button primary create-trigger">Create a site</button>
|
||
</section>
|
||
<section id="site-grid" class="site-grid" aria-live="polite"></section>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
|
||
<!-- ================================================================
|
||
Dialogs: create/edit forms for every route type, plus settings,
|
||
confirmation, icon picker, and user management dialogs
|
||
================================================================ -->
|
||
<!-- Create Hosted Site dialog -->
|
||
<dialog id="create-dialog">
|
||
<form id="create-form" class="dialog-card">
|
||
<div class="dialog-heading"><div><p class="eyebrow">New destination</p><h2>Create a site</h2></div></div>
|
||
<label>Site name<input name="name" placeholder="Portfolio" maxlength="80" required></label>
|
||
<label>Port<input name="port" type="number" required><small id="port-help"></small></label>
|
||
<label>Domain <span class="optional">Optional</span><input name="domain" placeholder="www.example.com"><small>Leave blank for port-only LAN access.</small></label><label>Additional domains <span class="optional">Optional</span><textarea name="domains" placeholder="www.example.com example.net"></textarea><small>One alias per line. All domains use the same hosted files and TLS settings.</small></label>
|
||
<label>TLS<select name="tls"><option value="automatic">Automatic public HTTPS</option><option value="internal">Internal HTTPS for trusted local devices</option><option value="http">HTTP only</option></select></label>
|
||
<label class="check-control"><input name="hsts" type="checkbox" value="true"><span>Enable HSTS after HTTPS is verified</span></label>
|
||
<details><summary>Advanced options</summary><div class="details-body"><label>Access List<select name="accessListId"><option value="">Public — no Access List</option></select><small>Reusable network or login protection.</small></label><label>Compression<select name="compression"><option value="automatic">Automatic zstd + gzip</option><option value="gzip">gzip only</option><option value="off">Off</option></select></label><label>Request headers<textarea name="requestHeadersText" placeholder="X-Robots-Tag: noindex"></textarea><small>One Name: value pair per line.</small></label><label>Response headers<textarea name="responseHeadersText" placeholder="X-Frame-Options: SAMEORIGIN"></textarea><small>One Name: value pair per line.</small></label><label class="check-control"><input name="hstsSubdomains" type="checkbox"><span>Apply HSTS to subdomains</span></label><label>Custom Caddy configuration<textarea name="customConfig" class="code-input" placeholder="# Expert use only"></textarea><small>Validated before Caddy reload.</small></label></div></details>
|
||
<label class="dropzone">Website files<input name="files" type="file" accept=".zip,.html,text/html,application/zip" required><span class="upload-icon">⇧</span><strong>Choose a ZIP or index.html</strong><small>ZIP files must contain index.html · Up to 250 MB</small></label>
|
||
<p id="create-error" class="error" role="alert"></p>
|
||
<div class="dialog-actions"><button type="button" class="button secondary close-dialog">Cancel</button><button class="button primary">Create & publish</button></div>
|
||
</form>
|
||
</dialog>
|
||
|
||
|
||
<!-- Create Proxy Host dialog -->
|
||
<dialog id="proxy-dialog">
|
||
<form id="proxy-form" class="dialog-card">
|
||
<div class="dialog-heading"><div><p class="eyebrow">New route</p><h2>Create a proxy host</h2></div></div>
|
||
<label>Name<input name="name" placeholder="Home Assistant" maxlength="80" required></label>
|
||
<label>Primary domain<input name="domain" placeholder="home.example.com" required></label>
|
||
<label>Additional domains <span class="optional">Optional</span><textarea name="domainsText" placeholder="www.home.example.com home.example.net"></textarea><small>One alias per line. All domains use this proxy host’s upstream and TLS settings.</small></label>
|
||
<label>Forward to<input name="target" type="url" placeholder="http://192.168.1.20:8123" required><small>Use the container name, LAN address, or application URL.</small></label>
|
||
|
||
<label>TLS<select name="tls"><option value="automatic">Automatic public HTTPS</option><option value="internal">Internal HTTPS for trusted local devices</option><option value="custom">Custom uploaded certificate</option><option value="http">HTTP only</option></select></label>
|
||
<label class="check-control"><input name="hsts" type="checkbox"><span>Enable HSTS after HTTPS is verified</span></label>
|
||
<div id="custom-certificate-fields"><label>Certificate PEM<input name="certificateFile" type="file" accept=".pem,.crt,application/x-pem-file"></label><label>Private key PEM<input name="privateKeyFile" type="file" accept=".pem,.key,application/x-pem-file"></label><small>Both files are required when installing or replacing a custom certificate.</small></div>
|
||
<details><summary>Advanced options</summary><div class="details-body"><label>Access List<select name="accessListId"><option value="">Public — no Access List</option></select><small>Reusable network or login protection.</small></label><label>Upstream pool <span class="optional">Optional</span><textarea name="upstreamsText" placeholder="http://192.168.1.20:53 http://192.168.1.21:53"></textarea><small>One HTTP/HTTPS target per line. Caddy distributes requests across healthy targets.</small></label><label>Load balancing<select name="lbPolicy"><option value="random">Random (default)</option><option value="round_robin">Round Robin</option><option value="least_conn">Least Connections</option><option value="ip_hash">IP Hash (sticky sessions)</option></select><small>Only takes effect with 2 or more targets in Upstream pool above — Forward to is not counted once Upstream pool has any entries.</small></label><label>Health-check path<input name="healthPath" value="/"></label><label>Health-check method<select name="healthMethod"><option value="GET">GET — retrieve a response</option><option value="HEAD">HEAD — headers only</option></select></label><label>Expected status<input name="healthExpected" value="200-499"><small>Examples: 200, 200,204, or 200-399.</small></label><label>Timeout in seconds<input name="healthTimeoutSeconds" type="number" min="1" max="60" value="4"></label><label class="check-control"><input name="healthEnabled" type="checkbox" checked><span>Monitor this upstream</span></label><label>Compression<select name="compression"><option value="automatic">Automatic zstd + gzip</option><option value="gzip">gzip only</option><option value="off">Off</option></select></label><label class="check-control"><input name="blockCommonExploits" type="checkbox"><span>Block common exploits</span></label><h3>Custom locations <span class="optional">Optional</span></h3><label>Locations<textarea name="customLocationsText" placeholder="/api/* | http://192.168.1.20:3001 | strip /media/* | http://192.168.1.21:8080 | preserve"></textarea><small>One per line: path | destination | strip or preserve.</small></label><h3>Headers and upstream TLS</h3><label>Request headers<textarea name="requestHeadersText" placeholder="X-Forwarded-Host: {host}"></textarea><small>One Name: value pair per line.</small></label><label>Response headers<textarea name="responseHeadersText" placeholder="X-Frame-Options: SAMEORIGIN"></textarea></label><label>Upstream TLS server name<input name="upstreamTlsServerName" placeholder="service.internal"><small>Optional SNI name expected by the upstream certificate.</small></label><label class="check-control"><input name="upstreamTlsInsecure" type="checkbox"><span>Ignore upstream TLS certificate errors</span><small>Use only for a trusted internal HTTPS service with a self-signed or hostname-mismatched certificate.</small></label><label class="check-control"><input name="hstsSubdomains" type="checkbox"><span>Apply HSTS to subdomains</span></label><label>Custom Caddy configuration<textarea name="customConfig" class="code-input" placeholder="# Expert use only"></textarea><small>Validated before Caddy reload. NGINX syntax is not supported.</small></label></div></details>
|
||
<p id="proxy-error" class="error" role="alert"></p>
|
||
<div class="dialog-actions"><button type="button" class="button secondary close-dialog">Cancel</button><button class="button primary">Create & publish</button></div>
|
||
</form>
|
||
</dialog>
|
||
<!-- Create Streaming Host dialog -->
|
||
<dialog id="stream-dialog"><form id="stream-form" class="dialog-card"><div class="dialog-heading"><div><p class="eyebrow">New route</p><h2 id="stream-title">Create a streaming host</h2></div></div><label>Name<input name="name" required placeholder="Minecraft server"></label><label>Incoming port<input name="port" type="number" min="1" max="65535" required placeholder="25565"><small>Must already be published on the container. See Documentation → Streaming hosts.</small></label><label>Forward to<input name="target" required placeholder="192.168.1.20:25565"><small>A host and port, such as 192.168.1.20:25565. No http:// — this is raw TCP/UDP, not a web address.</small></label><div class="form-grid"><label class="check-control"><input name="tcp" type="checkbox" checked><span>TCP</span></label><label class="check-control"><input name="udp" type="checkbox"><span>UDP</span></label></div><label class="check-control"><input name="healthEnabled" type="checkbox" checked><span>Monitor this target</span></label><p id="stream-error" class="error"></p><div class="dialog-actions"><button type="button" class="button secondary close-dialog">Cancel</button><button class="button primary">Create streaming host</button></div></form></dialog>
|
||
<!-- Create Redirect Host dialog -->
|
||
<dialog id="redirect-dialog"><form id="redirect-form" class="dialog-card"><div class="dialog-heading"><div><p class="eyebrow">New route</p><h2>Create a redirect host</h2></div></div><label>Name<input name="name" required placeholder="Old website"></label><label>Source domain<input name="domain" required placeholder="old.example.com"></label><label>Destination<input name="target" type="url" required placeholder="https://new.example.com"></label><label>Redirect type<select name="code"><option value="302">302 · Temporary</option><option value="301">301 · Permanent</option><option value="307">307 · Temporary, preserve method</option><option value="308">308 · Permanent, preserve method</option></select></label><label>TLS<select name="tls"><option value="automatic">Automatic HTTPS</option><option value="http">HTTP only</option><option value="internal">Internal HTTPS</option></select></label><label class="check-control"><input name="preservePath" type="checkbox" checked><span>Preserve path and query</span></label><p id="redirect-error" class="error"></p><div class="dialog-actions"><button type="button" class="button secondary close-dialog">Cancel</button><button class="button primary">Create redirect</button></div></form></dialog>
|
||
<!-- Create Access List dialog -->
|
||
<dialog id="access-dialog"><form id="access-form" class="dialog-card"><div class="dialog-heading"><div><p class="eyebrow">Reusable protection</p><h2>Create an Access List</h2></div></div><label>Name<input name="name" required placeholder="LAN and family"></label><label>Allowed networks<textarea name="networks" placeholder="private_ranges 192.168.50.0/24"></textarea><small>When supplied, every other network is denied. Use one IP, CIDR range, or private_ranges per line.</small></label><label>Denied networks <span class="optional">Optional</span><textarea name="deniedNetworks" placeholder="203.0.113.0/24"></textarea><small>These rules are evaluated before allowed networks and logins.</small></label><div id="access-credential-editor" class="credential-editor"></div><div id="access-assignment-summary" class="callout hidden"></div><p id="access-error" class="error"></p><div class="dialog-actions"><button type="button" class="button secondary close-dialog">Cancel</button><button class="button primary">Save Access List</button></div></form></dialog>
|
||
|
||
|
||
<!-- Edit (Domain & TLS / Edit proxy host) settings dialog, shared by Hosted & Proxy -->
|
||
<dialog id="settings-dialog">
|
||
<form id="settings-form" class="dialog-card">
|
||
<div class="dialog-heading"><div><p class="eyebrow">Gateway settings</p><h2 id="settings-title">Edit route</h2></div></div>
|
||
<label id="settings-name-wrap">Name<input name="name" maxlength="80"></label>
|
||
<label>Primary domain<input name="domain" placeholder="www.example.com"></label>
|
||
<label>Additional domains <span class="optional">Optional</span><textarea name="domainsText" placeholder="www.example.com example.net"></textarea><small>One alias per line. All domains use the same route and TLS settings.</small></label>
|
||
<label id="settings-target-wrap">Forward to<input name="target" type="url" placeholder="http://192.168.1.20:3000"></label>
|
||
<label>TLS<select name="tls"><option value="automatic">Automatic public HTTPS</option><option value="internal">Internal HTTPS for trusted local devices</option><option value="custom">Custom uploaded certificate</option><option value="http">HTTP only</option></select></label>
|
||
<label class="check-control"><input name="hsts" type="checkbox"><span>Enable HSTS after HTTPS is verified</span></label>
|
||
<div class="custom-certificate-fields"><label>Certificate PEM<input name="certificateFile" type="file" accept=".pem,.crt,application/x-pem-file"></label><label>Private key PEM<input name="privateKeyFile" type="file" accept=".pem,.key,application/x-pem-file"></label><small>Both files are required when installing or replacing a custom certificate.</small></div>
|
||
<details id="settings-hosted-advanced"><summary>Advanced options</summary><div class="details-body"><label>Access List<select name="accessListId"><option value="">Public — no Access List</option></select><small>Reusable network or login protection.</small></label><label>Compression<select name="compression"><option value="automatic">Automatic zstd + gzip</option><option value="gzip">gzip only</option><option value="off">Off</option></select></label><label>Request headers<textarea name="requestHeadersText" placeholder="Name: value"></textarea></label><label>Response headers<textarea name="responseHeadersText" placeholder="Name: value"></textarea></label><label class="check-control"><input name="hstsSubdomains" type="checkbox"><span>Apply HSTS to subdomains</span></label><label>Custom Caddy configuration<textarea name="customConfig" class="code-input"></textarea></label></div></details>
|
||
<details id="settings-advanced"><summary>Advanced options</summary><div class="details-body"><label>Access List<select name="accessListId"><option value="">Public — no Access List</option></select></label><label>Upstream pool <span class="optional">Optional</span><textarea name="upstreamsText" placeholder="http://192.168.1.20:53 http://192.168.1.21:53"></textarea><small>One HTTP/HTTPS target per line. Caddy distributes requests across healthy targets.</small></label><label>Load balancing<select name="lbPolicy"><option value="random">Random (default)</option><option value="round_robin">Round Robin</option><option value="least_conn">Least Connections</option><option value="ip_hash">IP Hash (sticky sessions)</option></select><small>Only takes effect with 2 or more targets in Upstream pool above — Forward to is not counted once Upstream pool has any entries.</small></label><label>Health-check path<input name="healthPath" value="/"></label><label>Health-check method<select name="healthMethod"><option value="GET">GET — retrieve a response</option><option value="HEAD">HEAD — headers only</option></select></label><label>Expected status<input name="healthExpected" value="200-499"></label><label>Timeout in seconds<input name="healthTimeoutSeconds" type="number" min="1" max="60" value="4"></label><label class="check-control"><input name="healthEnabled" type="checkbox" checked><span>Monitor this upstream</span></label><label>Compression<select name="compression"><option value="automatic">Automatic zstd + gzip</option><option value="gzip">gzip only</option><option value="off">Off</option></select></label><label class="check-control"><input name="blockCommonExploits" type="checkbox"><span>Block common exploits</span></label><label>Custom locations<textarea name="customLocationsText" placeholder="/api/* | http://192.168.1.20:3001 | strip"></textarea><small>One per line: path | destination | strip or preserve.</small></label><label>Request headers<textarea name="requestHeadersText" placeholder="Name: value"></textarea></label><label>Response headers<textarea name="responseHeadersText" placeholder="Name: value"></textarea></label><label>Upstream TLS server name<input name="upstreamTlsServerName"><small>Optional SNI name expected by the upstream certificate.</small></label><label class="check-control"><input name="upstreamTlsInsecure" type="checkbox"><span>Ignore upstream TLS certificate errors</span><small>Use only for a trusted internal HTTPS service with a self-signed or hostname-mismatched certificate.</small></label><label class="check-control"><input name="hstsSubdomains" type="checkbox"><span>Apply HSTS to subdomains</span></label><label>Custom Caddy configuration<textarea name="customConfig" class="code-input"></textarea></label></div></details>
|
||
<p id="settings-error" class="error" role="alert"></p>
|
||
<div class="dialog-actions"><button type="button" class="button secondary close-dialog">Cancel</button><button class="button primary">Save & apply</button></div>
|
||
</form>
|
||
</dialog>
|
||
|
||
|
||
<!-- Delete confirmation dialog -->
|
||
<dialog id="confirm-dialog">
|
||
<form method="dialog" class="dialog-card compact"><h2 id="confirm-title">Delete this site?</h2><p id="confirm-copy" class="muted">Its uploaded files will be permanently removed.</p><div class="dialog-actions"><button value="cancel" class="button secondary">Cancel</button><button value="confirm" class="button danger">Delete</button></div></form>
|
||
</dialog>
|
||
<!-- Icon picker dialog (search / upload / URL) -->
|
||
<dialog id="icon-dialog" class="icon-dialog">
|
||
<form method="dialog" class="dialog-card icon-picker">
|
||
<div class="dialog-heading"><div><p class="eyebrow">Appearance</p><h2>Choose an icon</h2></div></div>
|
||
<p class="muted">Search Dashboard Icons. Selected icons are validated and stored locally in <code>/data/icons</code>.</p>
|
||
<label>Search icons<input id="icon-search" type="search" placeholder="Jellyfin" autocomplete="off"></label>
|
||
<label>Upload a custom icon<input id="icon-upload" type="file" accept="image/png,image/jpeg,image/webp,image/gif,image/svg+xml"><small>PNG, JPEG, WebP, GIF, or SVG · up to 2 MB. Stored locally in <code>/data/icons</code>.</small></label>
|
||
<label>Or use an image URL <span class="optional">Optional</span><input id="icon-url" type="url" placeholder="https://example.com/icon.png"><small>Use a trusted HTTPS URL. The two-letter fallback remains available.</small></label>
|
||
<div id="icon-results" class="icon-results" aria-live="polite"><p class="quiet-state">Enter at least two characters to search.</p></div>
|
||
<p id="icon-error" class="error" role="alert"></p>
|
||
<div class="dialog-actions"><button id="save-icon-url" type="button" class="button secondary">Save URL</button><button id="reset-icon" value="none" class="button secondary">Use two-letter fallback</button><button value="cancel" class="button secondary">Cancel</button></div>
|
||
</form>
|
||
</dialog>
|
||
<!-- Create/edit user dialog -->
|
||
<dialog id="user-dialog">
|
||
<form id="user-form" class="dialog-card">
|
||
<div class="dialog-heading"><div><p class="eyebrow">Administration</p><h2>Create a user</h2></div></div>
|
||
<label>Display name<input name="displayName" placeholder="Marvin Wade" maxlength="80" required></label>
|
||
<label>Username<input name="username" placeholder="marvin" minlength="3" maxlength="64" pattern="[A-Za-z0-9][A-Za-z0-9._-]{2,63}" autocomplete="off" required></label>
|
||
<label>Role<select name="role"><option value="standard">Standard User</option><option value="viewer">Viewer</option><option value="administrator">Administrator</option></select><small>Viewer accounts can inspect gateway data. Standard Users and Administrators retain their assigned management capabilities.</small></label>
|
||
<label>Temporary password<input name="password" type="password" minlength="8" autocomplete="new-password" required><small>At least 8 characters. Share it securely.</small></label>
|
||
<p id="user-error" class="error" role="alert"></p>
|
||
<div class="dialog-actions"><button type="button" class="button secondary close-dialog">Cancel</button><button class="button primary">Create user</button></div>
|
||
</form>
|
||
</dialog>
|
||
<!-- Reset user password dialog -->
|
||
<dialog id="password-dialog">
|
||
<form id="password-form" class="dialog-card">
|
||
<div class="dialog-heading"><div><p class="eyebrow">Credentials</p><h2 id="password-title">Reset password</h2></div></div>
|
||
<label>New password<input name="password" type="password" minlength="8" autocomplete="new-password" required><small>At least 8 characters.</small></label>
|
||
<p id="password-error" class="error" role="alert"></p>
|
||
<div class="dialog-actions"><button type="button" class="button secondary close-dialog">Cancel</button><button class="button primary">Save password</button></div>
|
||
</form>
|
||
</dialog>
|
||
|
||
<!-- Hidden file input used by the "Replace files" hosted-site action, toast, and
|
||
update-available banner -->
|
||
<input id="replace-files" type="file" accept=".zip,.html,text/html,application/zip" hidden>
|
||
<div id="toast" class="toast" role="status"></div>
|
||
<div id="update-banner" class="update-banner hidden" role="status"><span>A new version of Site Gateway is available.</span><div class="update-banner-actions"><button id="update-banner-refresh" class="button primary">Refresh</button><button id="update-banner-dismiss" class="text-button">Dismiss</button></div></div>
|
||
<!-- App scripts: core (app.js) then extended views/admin (features.js) -->
|
||
<script src="/app.js?v=0.16.47" defer></script><script src="/features.js?v=0.16.47" defer></script><script src="/select-enhance.js?v=0.16.47" defer></script>
|
||
</body>
|
||
</html>
|