Import Site Gateway app and clean up for standalone release

This commit is contained in:
2026-09-12 17:58:54 +00:00
commit bbeeb633de
24 changed files with 4214 additions and 0 deletions
+276
View File
@@ -0,0 +1,276 @@
<!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">
</head>
<body>
<div id="login" class="login-shell hidden">
<form id="login-form" class="login-card">
<img class="brand-mark product-icon" src="/site-gateway-lockup-approved.png" alt="Site Gateway">
<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>
</div>
<dialog id="setup-dialog" class="setup-dialog">
<form id="setup-form" class="dialog-card setup-card">
<img class="brand-mark product-icon" src="/site-gateway-lockup-approved.png" alt="Site Gateway">
<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>
<div id="dashboard" class="app-shell hidden">
<aside>
<div class="brand"><img class="brand-mark small product-icon" src="/site-gateway-icon-approved.png" alt=""><span>Site Gateway</span></div>
<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="logs">Logs</button>
</nav>
<div class="aside-utilities"><button class="admin-only" data-view="administration">Administration</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="logs">Logs</button>
<button class="admin-only" data-view="administration">Admin</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>
<section id="dashboard-view" class="dashboard-view" aria-label="Gateway dashboard">
<div class="metric-grid">
<button class="metric-card" data-target="hosted"><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" data-target="proxies"><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" data-target="certificates"><span class="metric-label">Certificates</span><strong id="dash-tls-total">0</strong><span id="dash-tls-detail">No TLS domains</span></button>
<div class="metric-card attention"><span class="metric-label">Needs attention</span><strong id="dash-attention-total">0</strong><span id="dash-attention-detail">No current issues</span></div>
</div>
<div class="dashboard-columns">
<section class="dashboard-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-list">
<div><span id="gateway-health-dot" class="status-dot running"></span><span><strong>Gateway</strong><small id="gateway-health-copy">Configuration valid</small></span></div>
<div><span id="http-health-dot" class="status-dot running"></span><span><strong>HTTP · Port 80</strong><small id="http-health-copy">Ready and responding</small></span></div>
<div><span id="https-health-dot" class="status-dot inactive"></span><span><strong>HTTPS · Port 443</strong><small id="https-health-copy">Not configured</small></span></div>
<div><span id="storage-health-dot" class="status-dot running"></span><span><strong>Persistent storage</strong><small id="storage-health-copy">Data directory writable</small></span></div>
</div>
<p id="health-checked" class="checked-time">Last checked —</p>
</section>
<section class="dashboard-panel">
<div class="panel-heading"><div><p class="eyebrow">Runtime</p><h2>System</h2></div></div>
<dl class="system-grid">
<div><dt>Uptime</dt><dd id="system-uptime"></dd></div>
<div><dt>Memory</dt><dd id="system-memory"></dd></div>
<div><dt>Site Gateway data</dt><dd id="system-data"></dd><small>Used by sites and configuration</small></div>
<div><dt>Storage available</dt><dd id="system-disk"></dd><small>Available on the /data volume</small></div>
<div><dt>Site Gateway</dt><dd id="system-app-version"></dd></div>
<div><dt>Caddy</dt><dd id="system-caddy-version"></dd></div>
<div><dt>Database</dt><dd id="system-database"></dd><small id="system-database-detail">SQLite storage</small></div>
</dl>
</section>
</div>
<div class="dashboard-columns lower">
<section class="dashboard-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"><p class="quiet-state">Everything looks good.</p></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>
<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">Run a check to inspect configured domains.</p></div></section>
</section>
<section id="logs-view" class="feature-view hidden">
<div class="log-toolbar"><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></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>
<p class="muted feature-note">Recent requests handled by Caddy. Sensitive request headers are never displayed.</p>
<div class="log-section-heading diagnostic-section-heading"><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 class="table-wrap log-table-wrap diagnostic-list"><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 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>
<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="defaults">Gateway defaults</button><button data-admin-tab="backups">Backup & restore</button><button data-admin-tab="security">Security & updates</button><button data-admin-tab="danger" class="danger-tab">Danger Zone</button></div>
<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>
<section data-admin-panel="defaults" class="hidden settings-panel">
<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>
<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>
</section>
<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="configuration">Configuration only</option><option value="complete">Complete — configuration + hosted files</option></select><small id="backup-type-help">Configuration only includes settings and metadata, not uploaded Hosted Site files.</small></label><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 secondary">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></div><div id="backup-list" class="data-list"></div></section>
<section data-admin-panel="security" class="hidden settings-panel"><h2>Security, health & updates</h2><div class="role-callout"><strong>Configuration safety</strong><span>Site Gateway validates generated Caddy configuration before every reload and retains the active configuration when validation fails.</span><strong>Container updates</strong><span>Updates are installed by pulling a new pinned image. Create a backup before changing versions.</span></div><section class="support-panel"><div><p class="eyebrow">Troubleshooting & support</p><h3>Gateway diagnostics</h3><p class="muted">Run checks and download a redacted report when you need to investigate a gateway issue.</p></div><div class="row-actions"><button id="download-support" class="button secondary admin-only">Download support report</button></div><p class="muted support-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></section><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 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>
<section id="redirects-view" class="feature-view hidden"><div id="redirect-list" class="site-grid"></div><section id="redirect-empty" class="empty"><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>
<section id="access-view" class="feature-view hidden"><div id="access-list" class="data-list"></div></section>
<section id="documentation-view" class="feature-view hidden docs"><div class="docs-intro"><p class="eyebrow">Site Gateway manual</p><h2>Simple routing for homelabs and small teams</h2><p>A complete guide to publishing sites, routing applications, securing domains, and recovering safely. Start with the defaults, then use the advanced controls when you understand the trade-offs.</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”, “Plex”, “CIDR”, “backup”, or any field name"></label><small>Searches purpose, fields, examples, troubleshooting, and expert notes.</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="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="certificate">Certificates</button><button data-doc-jump="logs">Logs</button><button data-doc-jump="access">Access Lists</button><button data-doc-jump="administration">Administration</button><button data-doc-jump="backup">Backup & Restore</button><button data-doc-jump="danger">Danger Zone</button><button data-doc-jump="common">Common Controls</button></aside><div id="docs-content">
<article data-doc="introduction why built philosophy caddy novice expert"><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, HTTPS, health checks, access control, and recovery.</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 generated Caddy configuration is validated before reload.</p><h3>Example</h3><p>Publish a ZIP on a direct port first, then add <code>www.example.com</code> after DNS and port forwarding are ready.</p></article>
<article data-doc="getting started install first login"><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 port 8080, and sign in with the administrator credentials supplied to Docker. Before publishing public domains, make sure DNS points to this server and ports 80 and 443 are free.</p></article>
<article data-doc="hosted static zip index upload"><p class="eyebrow">Hosted sites</p><h2>Publish a static website</h2><ol><li>Open Hosted Sites and choose New hosted site.</li><li>Give the site a name and unused direct-access port.</li><li>Upload an index.html or ZIP whose root contains index.html.</li><li>Add a domain only when DNS is ready; choose Automatic HTTPS for public service.</li></ol><p><strong>Expected behavior:</strong> the files are served immediately on the chosen port and, when configured, through the domain.</p></article>
<article data-doc="proxy jellyfin vaultwarden plex forward upstream"><p class="eyebrow">Proxy hosts</p><h2>Connect a local application</h2><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 and Caddy manages eligible public HTTPS certificates automatically.</p></article>
<article data-doc="certificate tls dns ports pending"><p class="eyebrow">Certificates</p><h2>Automatic HTTPS prerequisites</h2><p>The domain must resolve to your public address, inbound ports 80 and 443 must reach Site Gateway, and another proxy cannot own those ports. “Not detected” means Caddy has not yet stored a certificate; review gateway events and DNS before retrying.</p></article>
<article data-doc="diagnostics readiness check now support report expiration"><p class="eyebrow">Health diagnostics</p><h2>Understand what failed</h2><p>Open Certificates and choose <strong>Check now</strong> to test DNS resolution, the gateway listeners, stored certificate coverage, and proxy upstream health. Expand a certificate for its non-secret details. Administrators can download a redacted support report when asking for help; it intentionally excludes credentials, cookies, private keys, secrets, and raw expert configuration.</p></article>
<article data-doc="access list lan authentication"><p class="eyebrow">Access Lists</p><h2>Protect a route</h2><p>Use <code>private_ranges</code> to allow standard LAN address ranges, or enter exact IP/CIDR values one per line. Add a login when the visitor must also authenticate. Assign the saved list from the Proxy Hosts Advanced section.</p></article>
<article data-doc="redirect host permanent temporary path query"><p class="eyebrow">Redirect Hosts</p><h2>Move an address safely</h2><p>Use 301 or 308 only when the move is intended to be permanent; browsers can cache them. Use 302 or 307 while testing. Enable Preserve path and query when <code>old.example.com/library?id=2</code> should become <code>new.example.com/library?id=2</code>.</p></article>
<article data-doc="default site welcome 404 no response custom html"><p class="eyebrow">Default site</p><h2>Handle unknown addresses</h2><p>The themed 404 is the safest public default. Gateway ready confirms HTTP routing during setup, No response closes unmatched HTTP connections, Redirect sends visitors elsewhere, and Custom HTML serves administrator-provided markup. Unknown HTTPS names still require their own valid route and certificate.</p></article>
<article data-doc="backup restore update rollback sqlite certificates"><p class="eyebrow">Backups</p><h2>Back up before an update</h2><p>A configuration backup contains a consistent SQLite snapshot and portable recovery data. A complete backup also contains hosted files, local assets, and certificate storage. Backups are stored in <code>/data/backups</code>; advanced installations can mount separate storage directly at that path.</p></article>
<article data-doc="backup encryption schedule retention restore troubleshooting"><p class="eyebrow">Restore checklist</p><h2>Recover with confidence</h2><ol><li>Download or import the <code>.sgbackup</code> archive.</li><li>Supply its password if it is encrypted.</li><li>Choose Restore and allow validation to finish.</li><li>Confirm hosts, certificates, and upstream health.</li></ol><p>Site Gateway verifies file checksums and creates a pre-restore safety backup. If the restored Caddy configuration is invalid, it attempts to recover the previous state automatically.</p></article>
<article data-doc="logs access logs gateway events requests response status domain filters"><p class="eyebrow">Logs</p><h2>Investigate requests and gateway events</h2><p>Access Logs show domains, paths, response status, latency, and upstream outcomes. Gateway Events record configuration and operational changes.</p><h3>Example</h3><p>Filter for a 502 or failed upstream event, then compare the target address with a direct LAN request.</p></article>
<article data-doc="users roles administrator standard viewer groups permissions audit"><p class="eyebrow">Administration · Users & Groups</p><h2>Control who can change the gateway</h2><p>Administrators manage users, roles, groups, and audit history. Standard Users perform permitted management tasks; Viewers are read-only.</p><h3>Example</h3><p>Create a Viewer for monitoring and a Standard User for routine route changes.</p></article>
<article data-doc="gateway defaults default site restore factory reset danger zone"><p class="eyebrow">Administration · Gateway Defaults & Danger Zone</p><h2>Preferences and destructive actions</h2><p>Gateway Defaults control unknown HTTP responses, backup scheduling, and certificate thresholds. Restore Defaults changes preferences only. Factory Reset deletes all data under <code>/data</code> and returns to initial setup.</p><h3>Example</h3><p>Keep the themed 404 in production and create a complete backup before any factory reset.</p></article>
<article data-doc="proxy advanced access list health expected status compression custom locations headers upstream tls server name hsts caddy configuration five ws"><p class="eyebrow">Proxy Hosts · Advanced options</p><h2>Why the advanced controls exist</h2><p>Most applications work with only a domain, Forward to target, and TLS choice. Advanced options are for applications with unusual paths, authentication boundaries, response codes, headers, certificates, or performance needs.</p><h3>What each control changes</h3><ul><li><strong>Access List:</strong> applies reusable login and network rules before the upstream is reached.</li><li><strong>Health-check path and method:</strong> tells Site Gateway what request to make when checking the application.</li><li><strong>Expected status:</strong> accepts a code, list, or range such as <code>200</code>, <code>200,204</code>, or <code>200-399</code>.</li><li><strong>Timeout and retries:</strong> control how long a check waits and how many additional attempts are made.</li><li><strong>Compression:</strong> controls whether Caddy negotiates gzip or zstd for responses.</li><li><strong>Custom Locations:</strong> sends paths such as <code>/api/*</code> to a different upstream and can strip or preserve the path.</li><li><strong>Request and response headers:</strong> add metadata required by an application or browser.</li><li><strong>Upstream TLS server name:</strong> supplies the SNI name when the upstream certificate expects a hostname.</li><li><strong>Trust an unverified upstream certificate:</strong> permits internal HTTPS with an untrusted certificate; use only on a trusted network.</li><li><strong>HSTS:</strong> tells browsers to use HTTPS for future requests; enable only after HTTPS is reliable.</li><li><strong>Custom Caddy configuration:</strong> an expert escape hatch for supported Caddy directives, validated before reload.</li></ul><h3>Who, where, when, and why</h3><p><strong>Who:</strong> experts operating applications with documented proxy requirements. <strong>Where:</strong> the Advanced options panel for one Proxy Host. <strong>When:</strong> only after the basic route works. <strong>Why:</strong> to solve a known requirement rather than guessing at settings.</p><h3>Practical example: Jellyfin</h3><p>Use <code>http://192.168.1.20:8096</code> as the upstream, leave the health path at <code>/</code>, keep the default expected range, and enable HSTS only after public HTTPS works. If an internal HTTPS service uses a private certificate, set its upstream SNI name and consider the unverified-certificate option only when the LAN is trusted.</p><h3>How to verify</h3><p>Save one change at a time, watch the cards upstream status, inspect Access Logs, and compare the result with a direct request to the application. If Caddy rejects a custom configuration, Site Gateway retains the last known-good configuration.</p></article>
<article data-doc="danger zone restore defaults factory reset credentials yes countdown setup recovery complete guide"><p class="eyebrow">Administration · Danger Zone</p><h2>Reset preferences or rebuild from zero</h2><p>This page contains the two actions with the greatest impact in Site Gateway. They are intentionally separate so a routine preference correction cannot be confused with a destructive rebuild.</p><h3>Restore Defaults: what it is for</h3><p>Restore Defaults returns gateway preferences to their known starting values: the Default Site response, page heading and explanation, redirect behavior, backup schedule, and certificate-health thresholds. It does not remove hosts, uploaded files, users, groups, Access Lists, certificates, logs, or saved backups.</p><h3>Factory Reset: what it is for</h3><p>Factory Reset removes Site Gateway data under <code>/data</code>, including routes, hosted content, users, groups, Access Lists, certificates, logs, backups, icons, and settings. Files mounted outside <code>/data</code> are not touched. Use it for a lab rebuild, a clean handoff, or recovery from an intentionally abandoned configuration—not to undo one route.</p><h3>Who should use these actions</h3><p>Only an Administrator should use them. Standard Users and Viewers should not see or operate destructive controls. The server verifies the signed-in administrator, the entered username, and the password before showing the final confirmation.</p><h3>What happens when you click the button</h3><p>Validation happens in order: username, password, confirmation phrase, then a second themed dialog requiring <code>YES</code>. Cancel clears every field and changes nothing. Restore Defaults applies immediately and refreshes the page. Factory Reset clears the data, recreates the initial bootstrap state, shows a countdown, and returns to the first-install login/setup flow without requiring a manual container restart.</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, create or restore a complete backup. Factory Reset is not a rollback tool; it intentionally removes the recovery material stored under <code>/data/backups</code>.</p><h3>Practical examples</h3><ul><li>Your Default Site explanation is confusing: use Restore Defaults.</li><li>You are moving the container to a new owner: create a complete backup, verify it, then use Factory Reset.</li><li>A route stopped working: inspect Logs and restore the route or backup; do not factory-reset first.</li></ul><h3>After a Factory Reset</h3><p>Open the management URL, sign in with the installation administrator credentials, and complete the initial administrator setup. Recreate or restore your hosts, certificates, users, groups, and Access Lists only after confirming the empty gateway responds correctly.</p></article>
<article data-doc="common interface controls menus three dots edit disable delete enable icons dashboard icons custom upload initials roles cards"><p class="eyebrow">Common Interface Controls</p><h2>Menus, status controls, and icons</h2><p>The same card language is used throughout Hosted Sites, Proxy Hosts, Redirect Hosts, Access Lists, Groups, and Users so that learning one area transfers to the next.</p><h3>What the three-dot menu is for</h3><p>The three-dot menu contains actions that change or inspect a card. <strong>Edit</strong> opens the full form. <strong>Enable/Disable</strong> changes whether the route or control is active without deleting its saved configuration. <strong>Assignments</strong> shows which hosts use an Access List. <strong>Delete</strong> removes the record after a confirmation.</p><h3>Who can use each action</h3><p>Administrators can manage all cards. Standard Users see only actions allowed by their capability set. Viewers can inspect information but cannot create, edit, disable, assign, or delete configuration. Authorization is enforced by the server, not only by hiding buttons.</p><h3>When to disable instead of delete</h3><p>Disable a route during maintenance or testing when you expect to reuse its settings. Delete only when the route, assignments, and its configuration are no longer needed.</p><h3>Changing a card icon</h3><p>Select the cards icon or choose Icon from its menu to open the icon picker. Search by service name, such as <code>Jellyfin</code>, then select a result. You can also upload a custom PNG, JPEG, WebP, or SVG when the service is not in the catalog. The interface scales icons into the same two-letter tile size and preserves the current initials as a fallback if an icon is removed or unavailable.</p><h3>Practical examples</h3><ul><li>Disable a Proxy Host while upgrading Plex, then enable it after the upstream responds.</li><li>Assign one Access List to several hosts and inspect Assignments before changing its rules.</li><li>Choose a Jellyfin icon for a Proxy Host; if the icon catalog is unavailable, its initials remain visible.</li></ul><h3>Backup and troubleshooting</h3><p>Icons and assignments are included in complete backups. If a custom icon does not appear, verify the upload completed, refresh the card list, and confirm the file format is supported. Changing an icon never changes routing, TLS, or access behavior.</p></article>
<article data-doc="hosted site field reference name primary additional domains upload port tls hsts icon"><p class="eyebrow">Hosted Sites · Field reference</p><h2>What each Hosted Site field means</h2><p><strong>Name</strong> is the label you see in Site Gateway; it does not have to match the domain. <strong>Primary domain</strong> is the main hostname. <strong>Additional domains</strong> are aliases that serve the same files. <strong>Upload</strong> accepts a site folder or ZIP and expects <code>index.html</code> at the web root. <strong>Port</strong> is the direct LAN port and must be inside the configured range. <strong>TLS</strong> controls whether the domain uses automatic public HTTPS. <strong>HSTS</strong> should be enabled only after HTTPS has been tested on every intended client.</p><h3>Novice example</h3><p>Name the route “Family landing page,” use port 9100, upload the ZIP, browse to the LAN address, and add a domain later.</p><h3>Expert example</h3><p>Use additional domains for a canonical and legacy hostname while keeping one file tree. Complete backups preserve both the route metadata and uploaded files.</p></article>
<article data-doc="certificates field reference domain readiness check issuer expiration custom certificate acme"><p class="eyebrow">Certificates · Field reference</p><h2>Read certificate health correctly</h2><p>Each configured hostname receives its own readiness result. DNS shows whether the name resolves, HTTP and HTTPS show listener reachability, and TLS shows certificate coverage and status. Issuer identifies the authority, expiration shows remaining lifetime, and “Waiting for Caddy” means issuance has not completed—not that a certificate was already created.</p><h3>Novice workflow</h3><p>Confirm DNS, forward ports 80 and 443, stop competing proxies, then run the certificate check. Do not troubleshoot an upstream application until the domain and HTTPS checks are healthy.</p><h3>Expert workflow</h3><p>Use custom certificates for externally purchased or wildcard material under the custom certificate area. Keep Caddy-managed ACME material separate and protect private keys.</p></article>
<article data-doc="advanced caddy custom locations headers compression health upstream tls"><p class="eyebrow">Advanced proxy settings</p><h2>Start simple, expand only when needed</h2><p>Custom Locations route selected paths to different upstreams. Request headers are sent upstream; response headers are returned to visitors. Health checks accept individual codes or ranges. Unverified upstream TLS and custom Caddy configuration are expert controls—change one item at a time and rely on validation feedback.</p></article>
<article data-doc="troubleshooting dns ports certificate caddy nginx conflict"><p class="eyebrow">Troubleshooting</p><h2>When HTTPS is not detected</h2><p>Confirm public DNS points to this server, router forwarding reaches ports 80 and 443, and NGINX Proxy Manager or another service is not still using those ports. Then review Certificates and Logs → Gateway events. Site Gateway cannot request a public certificate while another gateway receives the challenge.</p></article>
</div></div><p id="doc-empty" class="quiet-state hidden">No guide matched that search.</p></section>
<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>
<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>
<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>
<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>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>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>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><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>
<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>
<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>
<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>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>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>
<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>
<dialog id="readiness-dialog">
<form method="dialog" class="dialog-card readiness-dialog-card"><div class="dialog-heading"><div><p class="eyebrow">Domain readiness</p><h2 id="readiness-title">Diagnostics</h2></div><button value="cancel" class="icon-button" aria-label="Close">×</button></div><div id="readiness-detail-content"></div><div class="dialog-actions"><button value="cancel" class="button secondary">Close</button></div></form>
</dialog>
<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>
<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>
<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>
<input id="replace-files" type="file" accept=".zip,.html,text/html,application/zip" hidden>
<div id="toast" class="toast" role="status"></div>
<script src="/app.js?v=0.11.28" defer></script><script src="/features.js?v=0.11.28" defer></script>
</body>
</html>