- Redirect Hosts' "Create your first redirect" panel now hides during
the loading window on refresh, matching Hosted Sites/Proxy Hosts
instead of staying visible the whole time (or briefly showing the
wrong empty state when redirects do exist)
- Fix a crash when any dialog (Hosted Site, Proxy Host, User, Password,
or the shared settings dialog) is submitted implicitly, e.g. by
pressing Enter in a field instead of clicking the button.
event.submitter is null in that case, which threw on
`button.disabled = true` before the save request was ever sent,
silently dropping the whole save with no visible error. Added a
resolveSubmitter() fallback that finds the dialog's real submit
button when event.submitter is null.
- Bump version to 0.11.32
2026-09-12 21:53:27 -04:00
4 changed files with 43 additions and 18 deletions
functioncertificateStatusLabel(status){return({healthy:"Healthy",warning:"Renewal due soon",critical:"Renewal required urgently",expired:"Expired",pending:"Awaiting Caddy / ACME certificate",mismatch:"Certificate does not cover this domain"}[status]||String(status||"Unknown")).replaceAll("-"," ");}
functioncertificateStatusLabel(status){return({healthy:"Healthy",warning:"Renewal due soon",critical:"Renewal required urgently",expired:"Expired",pending:"Awaiting Caddy / ACME certificate",mismatch:"Certificate does not cover this domain"}[status]||String(status||"Unknown")).replaceAll("-"," ");}
$("#open-create").textContent=state.view==="administration"?"+ Create user":state.view==="redirects"?"+ New redirect host":state.view==="access"?"+ New Access List":$("#open-create").textContent;
$("#open-create").textContent=state.view==="administration"?"+ Create user":state.view==="redirects"?"+ New redirect host":state.view==="access"?"+ New Access List":$("#open-create").textContent;
if(state.view==="redirects")$("#redirect-empty .create-trigger").textContent="Create a redirect host";
if(state.view==="redirects")$("#redirect-empty .create-trigger").textContent="Create a redirect host";
$("#create-form").addEventListener("submit",asyncevent=>{event.preventDefault();constbutton=event.submitter;button.disabled=true;button.textContent="Publishing…";$("#create-error").textContent="";try{awaitapi("/api/sites",{method:"POST",body:newFormData(event.target)});$("#create-dialog").close();awaitrefresh();toast("Hosted site created and gateway applied.");}catch(error){$("#create-error").textContent=error.message;}finally{button.disabled=false;button.textContent="Create & publish";}});
$("#create-form").addEventListener("submit",asyncevent=>{event.preventDefault();constbutton=resolveSubmitter(event);button.disabled=true;button.textContent="Publishing…";$("#create-error").textContent="";try{awaitapi("/api/sites",{method:"POST",body:newFormData(event.target)});$("#create-dialog").close();awaitrefresh();toast("Hosted site created and gateway applied.");}catch(error){$("#create-error").textContent=error.message;}finally{button.disabled=false;button.textContent="Create & publish";}});
$("#proxy-form").addEventListener("submit",asyncevent=>{event.preventDefault();constbutton=event.submitter;button.disabled=true;button.textContent="Publishing…";$("#proxy-error").textContent="";constform=newFormData(event.target),certificate=form.get("certificateFile"),privateKey=form.get("privateKeyFile"),wantsCustom=form.get("tls")==="custom";if(wantsCustom&&(!certificate?.size||!privateKey?.size)){$("#proxy-error").textContent="Choose both the certificate and private key for Custom HTTPS.";button.disabled=false;button.textContent="Create & publish";return;}constbody=advancedFormBody(form,Object.fromEntries(form));deletebody.certificateFile;deletebody.privateKeyFile;if(wantsCustom)body.tls="http";try{constcreated=awaitapi("/api/proxies",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});if(wantsCustom){constfiles=newFormData();files.append("certificate",certificate);files.append("privateKey",privateKey);awaitapi(`/api/proxies/${created.id}/certificate`,{method:"POST",body:files});}$("#proxy-dialog").close();awaitrefresh();toast(wantsCustom?"Proxy host created with its custom certificate.":"Proxy host created. Certificate provisioning runs automatically.");}catch(error){$("#proxy-error").textContent=error.message;}finally{button.disabled=false;button.textContent="Create & publish";}});
$("#proxy-form").addEventListener("submit",asyncevent=>{event.preventDefault();constbutton=resolveSubmitter(event);button.disabled=true;button.textContent="Publishing…";$("#proxy-error").textContent="";constform=newFormData(event.target),certificate=form.get("certificateFile"),privateKey=form.get("privateKeyFile"),wantsCustom=form.get("tls")==="custom";if(wantsCustom&&(!certificate?.size||!privateKey?.size)){$("#proxy-error").textContent="Choose both the certificate and private key for Custom HTTPS.";button.disabled=false;button.textContent="Create & publish";return;}constbody=advancedFormBody(form,Object.fromEntries(form));deletebody.certificateFile;deletebody.privateKeyFile;if(wantsCustom)body.tls="http";try{constcreated=awaitapi("/api/proxies",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});if(wantsCustom){constfiles=newFormData();files.append("certificate",certificate);files.append("privateKey",privateKey);awaitapi(`/api/proxies/${created.id}/certificate`,{method:"POST",body:files});}$("#proxy-dialog").close();awaitrefresh();toast(wantsCustom?"Proxy host created with its custom certificate.":"Proxy host created. Certificate provisioning runs automatically.");}catch(error){$("#proxy-error").textContent=error.message;}finally{button.disabled=false;button.textContent="Create & publish";}});
$("#settings-form").addEventListener("submit",asyncevent=>{event.preventDefault();constbutton=event.submitter;button.disabled=true;button.textContent="Applying…";$("#settings-error").textContent="";constform=newFormData(event.target),certificate=form.get("certificateFile"),privateKey=form.get("privateKeyFile");letbody=Object.fromEntries(form);deletebody.certificateFile;deletebody.privateKeyFile;body=state.editing.kind==="proxy"?advancedFormBody(form,body):{domain:body.domain,tls:body.tls,hsts:form.has("hsts")};constuploadCustom=state.editing.kind==="proxy"&&body.tls==="custom"&&certificate?.size&&privateKey?.size;if(state.editing.kind==="proxy"&&body.tls==="custom"&&!uploadCustom){constexisting=state.proxies.find(item=>item.id===state.editing.id);if(!existing?.certificatePath){$("#settings-error").textContent="Choose both the certificate and private key for Custom HTTPS.";button.disabled=false;button.textContent="Save & apply";return;}}try{constbase=state.editing.kind==="proxy"?"proxies":"sites";awaitapi(`/api/${base}/${state.editing.id}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});if(uploadCustom){constfiles=newFormData();files.append("certificate",certificate);files.append("privateKey",privateKey);awaitapi(`/api/proxies/${state.editing.id}/certificate`,{method:"POST",body:files});}$("#settings-dialog").close();awaitrefresh();toast("Gateway settings applied.");}catch(error){$("#settings-error").textContent=error.message;}finally{button.disabled=false;button.textContent="Save & apply";}});
$("#settings-form").addEventListener("submit",asyncevent=>{event.preventDefault();constbutton=resolveSubmitter(event);button.disabled=true;button.textContent="Applying…";$("#settings-error").textContent="";constform=newFormData(event.target),certificate=form.get("certificateFile"),privateKey=form.get("privateKeyFile");letbody=Object.fromEntries(form);deletebody.certificateFile;deletebody.privateKeyFile;body=state.editing.kind==="proxy"?advancedFormBody(form,body):{domain:body.domain,tls:body.tls,hsts:form.has("hsts")};constuploadCustom=state.editing.kind==="proxy"&&body.tls==="custom"&&certificate?.size&&privateKey?.size;if(state.editing.kind==="proxy"&&body.tls==="custom"&&!uploadCustom){constexisting=state.proxies.find(item=>item.id===state.editing.id);if(!existing?.certificatePath){$("#settings-error").textContent="Choose both the certificate and private key for Custom HTTPS.";button.disabled=false;button.textContent="Save & apply";return;}}try{constbase=state.editing.kind==="proxy"?"proxies":"sites";awaitapi(`/api/${base}/${state.editing.id}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});if(uploadCustom){constfiles=newFormData();files.append("certificate",certificate);files.append("privateKey",privateKey);awaitapi(`/api/proxies/${state.editing.id}/certificate`,{method:"POST",body:files});}$("#settings-dialog").close();awaitrefresh();toast("Gateway settings applied.");}catch(error){$("#settings-error").textContent=error.message;}finally{button.disabled=false;button.textContent="Save & apply";}});
<divclass="diagnostic-section-heading"><pclass="eyebrow">Certificate inventory</p><h2>Certificates</h2><pclass="muted">Managed and uploaded certificates assigned to configured domains.</p></div><divid="certificate-list"class="data-list diagnostic-list"><pclass="quiet-state">Loading certificates…</p></div>
<divclass="diagnostic-section-heading"><pclass="eyebrow">Certificate inventory</p><h2>Certificates</h2><pclass="muted">Managed and uploaded certificates assigned to configured domains.</p></div><divid="certificate-list"class="data-list diagnostic-list"><pclass="quiet-state">Loading certificates…</p></div>
<sectionclass="dashboard-panel readiness-panel"><divclass="panel-heading"><div><pclass="eyebrow">Guided diagnostics</p><h2>Domain readiness</h2><pclass="muted">DNS, listener, TLS, and upstream checks for every configured domain.</p></div></div><divid="readiness-list"class="dashboard-list diagnostic-list"><pclass="quiet-state">Run a check to inspect configured domains.</p></div></section>
<sectionclass="dashboard-panel readiness-panel"><divclass="panel-heading"><div><pclass="eyebrow">Guided diagnostics</p><h2>Domain readiness</h2><pclass="muted">DNS, listener, TLS, and upstream checks for every configured domain.</p></div></div><divid="readiness-list"class="dashboard-list diagnostic-list"><pclass="quiet-state">Checking configured domains…</p></div></section>
<sectiondata-admin-panel="security"class="hidden settings-panel"><h2>Security, health & updates</h2><divclass="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><sectionclass="support-panel"><div><pclass="eyebrow">Troubleshooting & support</p><h3>Gateway diagnostics</h3><pclass="muted">Run checks and download a redacted report when you need to investigate a gateway issue.</p></div><divclass="row-actions"><buttonid="download-support"class="button secondary admin-only">Download support report</button></div><pclass="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><formid="health-settings-form"class="settings-form"><label>Renewing-soon warning<inputname="warningDays"type="number"min="8"max="120"value="30"><small>Days remaining before a certificate is highlighted.</small></label><label>Critical warning<inputname="criticalDays"type="number"min="1"max="119"value="7"><small>Must be lower than the renewing-soon threshold.</small></label><label>Stale health data<inputname="staleMinutes"type="number"min="2"max="1440"value="10"><small>Minutes before a displayed check is considered old.</small></label><divclass="dialog-actions"><buttonclass="button primary">Save health settings</button></div></form></section>
<sectiondata-admin-panel="security"class="hidden settings-panel"><h2>Security, health & updates</h2><divclass="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><sectionclass="support-panel"><div><pclass="eyebrow">Troubleshooting & support</p><h3>Gateway diagnostics</h3><pclass="muted">Run checks and download a redacted report when you need to investigate a gateway issue.</p></div><divclass="row-actions"><buttonid="download-support"class="button secondary admin-only">Download support report</button></div><pclass="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><formid="health-settings-form"class="settings-form"><label>Renewing-soon warning<inputname="warningDays"type="number"min="8"max="120"value="30"><small>Days remaining before a certificate is highlighted.</small></label><label>Critical warning<inputname="criticalDays"type="number"min="1"max="119"value="7"><small>Must be lower than the renewing-soon threshold.</small></label><label>Stale health data<inputname="staleMinutes"type="number"min="2"max="1440"value="10"><small>Minutes before a displayed check is considered old.</small></label><divclass="dialog-actions"><buttonclass="button primary">Save health settings</button></div></form></section>
<sectiondata-admin-panel="danger"class="hidden settings-panel danger-zone"><h2>Danger Zone</h2><pclass="muted">These actions can permanently remove Site Gateway data. Review each warning carefully before continuing.</p><divclass="danger-card"><pclass="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><buttonid="restore-defaults"class="button secondary">Restore default settings</button></div><divclass="danger-card destructive"><pclass="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><formid="factory-reset-form"class="danger-form"><label>Administrator username<inputname="username"autocomplete="username"required></label><label>Administrator password<inputname="password"type="password"autocomplete="current-password"required></label><label>Type <strong>FACTORY RESET</strong> to confirm<inputname="confirmation"requiredautocomplete="off"></label><pid="factory-reset-error"class="error"></p><divclass="danger-actions"><buttonclass="button secondary"type="button"id="factory-reset-cancel">Cancel</button><buttonclass="button danger"type="submit">Erase all data and reset</button></div></form></div></section>
<sectiondata-admin-panel="danger"class="hidden settings-panel danger-zone"><h2>Danger Zone</h2><pclass="muted">These actions can permanently remove Site Gateway data. Review each warning carefully before continuing.</p><divclass="danger-card"><pclass="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><buttonid="restore-defaults"class="button secondary">Restore default settings</button></div><divclass="danger-card destructive"><pclass="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><formid="factory-reset-form"class="danger-form"><label>Administrator username<inputname="username"autocomplete="username"required></label><label>Administrator password<inputname="password"type="password"autocomplete="current-password"required></label><label>Type <strong>FACTORY RESET</strong> to confirm<inputname="confirmation"requiredautocomplete="off"></label><pid="factory-reset-error"class="error"></p><divclass="danger-actions"><buttonclass="button secondary"type="button"id="factory-reset-cancel">Cancel</button><buttonclass="button danger"type="submit">Erase all data and reset</button></div></form></div></section>
</section>
</section>
<sectionid="redirects-view"class="feature-view hidden"><divid="redirect-list"class="site-grid"></div><sectionid="redirect-empty"class="empty"><divclass="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><buttonclass="button primary create-trigger">Create a redirect host</button></section></section>
<sectionid="redirects-view"class="feature-view hidden"><divid="redirect-list"class="site-grid"></div><sectionid="redirect-empty"class="empty hidden"><divclass="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><buttonclass="button primary create-trigger">Create a redirect host</button></section></section>
<sectionid="documentation-view"class="feature-view hidden docs"><divclass="docs-intro"><pclass="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><divclass="docs-search-panel"><labelclass="doc-search"><span>Search the complete manual</span><inputid="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><divclass="docs-layout"><asideclass="docs-nav"aria-label="Documentation sections"><pclass="eyebrow">Contents</p><buttondata-doc-jump="introduction">Introduction</button><buttondata-doc-jump="dashboard">Dashboard</button><buttondata-doc-jump="hosted">Hosted Sites</button><buttondata-doc-jump="proxy">Proxy Hosts</button><buttondata-doc-jump="redirect">Redirect Hosts</button><buttondata-doc-jump="certificate">Certificates</button><buttondata-doc-jump="logs">Logs</button><buttondata-doc-jump="access">Access Lists</button><buttondata-doc-jump="administration">Administration</button><buttondata-doc-jump="backup">Backup & Restore</button><buttondata-doc-jump="danger">Danger Zone</button><buttondata-doc-jump="common">Common Controls</button></aside><divid="docs-content">
<sectionid="documentation-view"class="feature-view hidden docs"><divclass="docs-intro"><pclass="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><divclass="docs-search-panel"><labelclass="doc-search"><span>Search the complete manual</span><inputid="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><divclass="docs-layout"><asideclass="docs-nav"aria-label="Documentation sections"><pclass="eyebrow">Contents</p><buttondata-doc-jump="introduction">Introduction</button><buttondata-doc-jump="dashboard">Dashboard</button><buttondata-doc-jump="hosted">Hosted Sites</button><buttondata-doc-jump="proxy">Proxy Hosts</button><buttondata-doc-jump="redirect">Redirect Hosts</button><buttondata-doc-jump="certificate">Certificates</button><buttondata-doc-jump="logs">Logs</button><buttondata-doc-jump="access">Access Lists</button><buttondata-doc-jump="administration">Administration</button><buttondata-doc-jump="backup">Backup & Restore</button><buttondata-doc-jump="danger">Danger Zone</button><buttondata-doc-jump="common">Common Controls</button></aside><divid="docs-content">
<articledata-doc="introduction why built philosophy caddy novice expert"><pclass="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>
<articledata-doc="introduction why built philosophy caddy novice expert"><pclass="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>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.