Files
site-gateway/src/public/index.html
T

436 lines
106 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="theme-color" content="#0b1220">
<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.11.117">
</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">
<label class="theme-control" for="theme-select"><span>Theme</span><select id="theme-select" aria-label="Color theme"><option value="system">System</option><option value="dark">Dark</option><option value="light">Light</option></select></label>
<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>
</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-blue" 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-blue" 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-amber" 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-purple" 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-blue" 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><button id="refresh-health" class="icon-button" aria-label="Refresh health checks" title="Refresh health checks"></button></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>
<dl class="system-grid">
<div class="system-tile"><dt>Uptime</dt><dd id="system-uptime"></dd></div>
<div class="system-tile"><dt>Memory</dt><dd id="system-memory"></dd></div>
<div class="system-tile"><dt>Site Gateway data</dt><dd id="system-data"></dd><small>Used by sites and configuration</small></div>
<div class="system-tile"><dt>Storage available</dt><dd id="system-disk"></dd><small>Available on the /data volume</small></div>
<div class="system-tile"><dt>Site Gateway</dt><dd id="system-app-version"></dd></div>
<div class="system-tile"><dt>Caddy</dt><dd id="system-caddy-version"></dd></div>
<div class="system-tile"><dt>Database</dt><dd id="system-database"></dd><small id="system-database-detail">SQLite storage</small></div>
<div class="system-tile"><dt>Public IP</dt><dd id="system-public-ip"></dd><small id="system-public-ip-detail">Not yet checked</small></div>
</dl>
</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>
<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-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, error rate, and average response time for each configured domain.</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></tr></thead><tbody id="performance-rows"></tbody></table></div>
</section>
</section>
<!-- Administration: tabbed panels (Users / Groups / Audit log / Logs & Retention /
Gateway defaults / Backup & restore / Danger Zone) -->
<section id="users-view" class="feature-view hidden">
<div class="admin-tabs"><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="danger" class="danger-tab">Danger Zone</button></div>
<!-- Users tab -->
<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) — configuration + hosted files</option><option value="configuration">Configuration only</option></select><small id="backup-type-help">Configuration only includes settings and metadata, not uploaded Hosted Site files.</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 containers <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 id="backup-list" class="data-list"></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">90009099</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 &amp; two-factor authentication, Administration, Performance monitoring, and every advanced control between them. Start with an areas 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 &amp; Groups</button><button data-doc-jump="my account">My Account</button><button data-doc-jump="defaults">Gateway Defaults</button><button data-doc-jump="backup">Backup &amp; Restore</button><button data-doc-jump="retention">Logs &amp; 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 its 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"><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>Container and version facts, plus a <strong>Public IP</strong> tile showing your gateway's current public address, rechecked automatically every 60 minutes — prep work for future Dynamic DNS support. This is a status fact, not a job you configure.</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. 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 thats just HTML/CSS/JS.</p><h3>Creating a Hosted Site</h3><ul><li><strong>Name:</strong> the label shown in Site Gateway; it doesnt 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>90009099</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 youve confirmed HTTPS works for every client — HSTS tells browsers to refuse HTTP entirely for this domain going forward, and thats 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 cards 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 isnt HTTP only.</li><li><strong>Custom Caddy configuration:</strong> expert-only raw Caddyfile lines appended to this sites 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 (160, 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> 160, 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 requests 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>&lt;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> (Caddys 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 clients 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 Gateways own dashboard status.</li><h3>How to verify a change</h3><p>Save one setting at a time, watch the cards 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 keys public key actually matches the certificate before accepting the pair, and confirms the certificates Subject Alternative Names cover every domain configured on that Proxy Host. A certificate that doesnt 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 destinations 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 youre 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 — theres 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>&nbsp;&nbsp;- "22:22/tcp"<br>&nbsp;&nbsp;- "25565:25565/tcp"</code></p><h3>Creating a Streaming Host</h3><ul><li><strong>Name.</strong></li><li><strong>Incoming port:</strong> 165535. 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 hasnt 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.</p><h3>Novice workflow</h3><p>Confirm DNS, forward ports 80 and 443, stop any competing proxy, then run Check now. Dont 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 certificates 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 (8120, default 30).</li><li><strong>Critical warning:</strong> days remaining before its 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 (21440, default 10).</li></ul><p>A status of <strong>mismatch</strong> means a custom certificate was uploaded for that route but its coverage doesnt 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 routes 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.</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 routes Advanced options to assign an Access List to it, or use the Access Lists own <strong>View assigned hosts</strong> menu action to see everywhere its currently in use. An Access List cant 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 &amp; Groups</p><h2>Overview: control who can change the gateway</h2><p>Every account has a role:</p><ul><li><strong>Administrator:</strong> full access, including Users, Groups, Settings, and Backups.</li><li><strong>Standard User:</strong> can manage Hosted Sites, Proxy Hosts, Redirect Hosts, Streaming Hosts, and Access Lists, but not Users, Groups, Settings, or Backups.</li><li><strong>Viewer:</strong> read-only — can inspect everything but change nothing.</li></ul><h3>Creating a user</h3><ul><li><strong>Display name.</strong></li><li><strong>Username:</strong> 364 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 users role from the cards 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 cards “•••” 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 users 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 cards “•••” 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 elses 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 its 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 Groups 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 cant 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 users password (Administration → Users → Reset password), which meant every routine password change had to go through an admin. My Account closes that gap for everyones 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 theyre shown in full.</li></ol><h3>Signing in with 2FA enabled</h3><p>After your username and password are accepted, youre prompted for a code. Enter the current 6-digit code from your authenticator app, or one of your unused recovery codes if you dont 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 — youll 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="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 isnt configured. (Unknown HTTPS hostnames are always rejected outright, regardless of this setting, since serving anything else would need a certificate Site Gateway doesnt 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 pages 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><p>A preview pane updates as you type, showing the page a visitor would actually see — the same rendering the gateway serves, not an approximation. It only shows something for the modes that actually serve a page (<em>Themed route-not-found page</em>, <em>Gateway ready page</em>, and <em>Custom HTML</em>); for <em>No response</em> and <em>Redirect elsewhere</em>, the pane explains why theres nothing to preview — those modes close the connection or send the visitor elsewhere before any page is ever shown. Scripts do not execute in the preview even when Custom HTML contains them, since the preview runs in a sandboxed frame — this only affects the preview, not what a real visitors browser does with your saved Custom HTML.</p></article><article data-doc="backup restore schedule retention encryption complete configuration import restore checklist"><p class="eyebrow">Administration</p><h2>Backup &amp; Restore</h2><p>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 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>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 wont 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 (1100, 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 containers <code>BACKUP_PASSWORD</code> environment value — enable this only after that value is actually configured, or scheduled runs will fail.</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 entrys 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 its 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 files 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 &amp; 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 &amp; Retention</h2><p>Set how many days of Access, Activity, Audit, Certificate, and Security records to keep (73650 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>, theyre 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 theyre 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 errors average response time"><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 (37 days) points.</p><h3>Per-route table</h3><p>One row per domain that has received traffic, sorted by 24-hour volume:</p><ul><li><strong>Last hour / Last 24h:</strong> request count for that window; a red count after the divider is how many of those responses were 4xx or 5xx. This includes routine noise (expired-token 401s, bots probing by raw IP, scanners hitting invalid hosts) as well as genuine failures — it isnt a health verdict by itself.</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></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 youre 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 cards 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 cards 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 isnt 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 isnt 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 users 2FA from Administration → Users — open the locked-out users “•••” menu and choose <strong>Disable 2FA</strong>. The user can sign in with just their password afterward and set 2FA up again whenever theyre 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. Thats 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><button type="button" class="icon-button close-dialog" aria-label="Close">×</button></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&#10;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><button type="button" class="icon-button close-dialog" aria-label="Close">×</button></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&#10;home.example.net"></textarea><small>One alias per line. All domains use this proxy hosts 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&#10;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&#10;/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><button type="button" class="icon-button close-dialog">×</button></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><button type="button" class="icon-button close-dialog">×</button></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><button type="button" class="icon-button close-dialog">×</button></div><label>Name<input name="name" required placeholder="LAN and family"></label><label>Allowed networks<textarea name="networks" placeholder="private_ranges&#10;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><button type="button" class="icon-button close-dialog" aria-label="Close">×</button></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&#10;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&#10;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><button value="cancel" class="icon-button" aria-label="Close">×</button></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><button type="button" class="icon-button close-dialog" aria-label="Close">×</button></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><button type="button" class="icon-button close-dialog" aria-label="Close">×</button></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.11.118" defer></script><script src="/features.js?v=0.11.114" defer></script>
</body>
</html>