`v0.11.101` is a stable, day-to-day release. The product has moved well past the original alpha creation flow described in earlier versions of this document — Hosted Sites, Proxy Hosts, Redirect Hosts, and Streaming Hosts are all implemented, along with authentication, access control, certificates, backups, and full dashboard reporting. This document reflects what's actually shipped and what's genuinely still ahead.
`v0.11.112` is a stable, day-to-day release. The product has moved well past the original alpha creation flow described in earlier versions of this document — Hosted Sites, Proxy Hosts, Redirect Hosts, and Streaming Hosts are all implemented, along with authentication, access control, certificates, backups, and full dashboard reporting. This document reflects what's actually shipped and what's genuinely still ahead.
## Product direction
@@ -39,6 +39,7 @@ Site Gateway stays simpler than a general-purpose proxy manager: one dashboard,
- Performance view with request throughput, response times, and per-route breakdowns.
- Rotating access and activity logs.
- Update-available banner when a newer image is deployed.
- A redacted support-report export exists (version, config health, certificate readiness, upstream checks, recent events) but its UI entry point is currently hidden pending a readability rewrite of the report's output format.
### Data and operations
@@ -49,6 +50,7 @@ Site Gateway stays simpler than a general-purpose proxy manager: one dashboard,
### Brand and docs
- Current icon and wordmark (v0.11.99) used consistently across the login screen, sidebar, themed default pages, and this README.
- A sitewide design-token system (colors, spacing, radius, and type scale defined once and reused everywhere) underpins the interface, so new UI stays visually consistent by default.
- Integrated, searchable in-app documentation covering every configurable field, including 2FA and the update-notification banner.
- Companion marketing site with an installation guide covering Docker Compose, plain `docker run`, and Unraid.
@@ -56,7 +58,7 @@ Site Gateway stays simpler than a general-purpose proxy manager: one dashboard,
Roughly in priority order:
- **Richer certificate diagnostics** — on-demand checks that distinguish DNS, inbound port, TLS, and upstream failures per domain, plus a redacted support-report export.
- **Richer certificate diagnostics** — on-demand checks that distinguish DNS, inbound port, TLS, and upstream failures per domain.
- **Wildcard/DNS-challenge certificates** — selected DNS-provider integrations for domains that can't use HTTP-01 validation. Needs encrypted secret storage for provider API credentials before it ships.
- **Browsable backup/restore history** — today a restore validates and rolls back safely, but there's no UI history of past backups beyond what's on disk.
- **Container picker for Proxy/Streaming targets** — letting a target be selected from a list of running Docker containers instead of typed as an IP/hostname, gated behind an opt-in Docker-socket mount since it needs real access to the Engine API. Also needs a shared Docker network between Site Gateway and the target container to actually be reachable, not just discoverable.
if(!points.length)$("#performance-sparkline-labels").innerHTML='<span class="axis-label" style="left:0;width:100%;top:45%;text-align:center">No request data for this window yet.</span>';
$("#performance-rows").innerHTML=routes.length?routes.map(route=>`<tr class="${selected&&route.host===selected?"row-highlight":""}"><td title="${escapeHtml(route.host)}">${escapeHtml(route.host)}</td><td>${countCell(route.hourRequests,route.hourErrors)}</td><td>${countCell(route.dayRequests,route.dayErrors,route.errorBreakdown,route.host)}</td><td>${formatAvgMs(route.dayAvgMs)}</td></tr>`).join(""):'<tr><td colspan="4" class="quiet-state">No requests have been logged yet.</td></tr>';
document.querySelectorAll("#user-list .user-card").forEach(card=>{constuser=state.users.find(item=>item.id===card.dataset.userId);constold=card.querySelector('[data-user-action="role"]');if(!user||!old)return;constselect=document.createElement("select");select.className="user-role-select";select.setAttribute("aria-label",`Role for ${user.username}`);select.innerHTML='<option value="administrator">Administrator</option><option value="standard">Standard User</option><option value="viewer">Viewer</option>';select.value=user.role;select.addEventListener("change",async()=>{try{awaitapi(`/api/users/${user.id}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({role:select.value})});awaitloadFeatureView();toast("User role updated.");}catch(error){select.value=user.role;toast(error.message);}});old.replaceWith(select);});
document.querySelectorAll("#user-list .user-card").forEach(card=>{constuser=state.users.find(item=>item.id===card.dataset.userId);constold=card.querySelector('[data-user-action="role"]');if(!user||!old)return;constselect=document.createElement("select");select.className="user-role-select";select.setAttribute("aria-label",`Role for ${user.username}`);select.innerHTML='<option value="administrator">Administrator</option><option value="standard">Standard User</option><option value="viewer">Viewer</option>';select.value=user.role;select.addEventListener("change",async()=>{try{awaitapi(`/api/users/${user.id}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({role:select.value})});awaitloadFeatureView();toast("User role updated.");}catch(error){select.value=user.role;toast(error.message,"error");}});old.replaceWith(select);});
$("#port-range").textContent=`${state.config.minPort}–${state.config.maxPort}`;$("#port-help").textContent=`Direct LAN access range: ${state.config.minPort}–${state.config.maxPort}`;
// --- Performance: themed error-breakdown popup, replacing the old hover tooltip ------------
functionshowErrorBreakdown(host,total,breakdown){letdialog=document.querySelector("#error-breakdown-dialog");if(!dialog){dialog=document.createElement("dialog");dialog.id="error-breakdown-dialog";document.body.append(dialog);}constrows=breakdown.map(item=>`<div class="error-breakdown-row"><span>${escapeHtml(item.status)}</span><span>${item.count.toLocaleString()}</span></div>`).join("");dialog.innerHTML=`<form method="dialog" class="dialog-card compact"><div class="dialog-heading"><div><p class="eyebrow">Performance · Last 24h</p><h2>${escapeHtml(host)}</h2></div></div><p class="muted">${total.toLocaleString()} error response${total===1?"":"s"} in the last 24 hours, by status code.</p><div class="error-breakdown-list">${rows}</div><div class="dialog-actions"><button value="cancel" class="button secondary">Close</button></div></form>`;dialog.showModal();}
$("#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=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";}});
if(action==="toggle"){consttoggleButton=event.target.closest(".toggle"),wasOn=toggleButton.classList.contains("on");toggleButton.classList.toggle("on",!wasOn);toggleButton.disabled=true;constbase=kind==="proxy"?"proxies":"sites";try{awaitapi(`/api/${base}/${card.dataset.id}/toggle`,{method:"POST"});awaitrefresh();toast("Status and gateway configuration updated.");}catch(error){toggleButton.classList.toggle("on",wasOn);toggleButton.disabled=false;toast(error.message||"Could not update status.");}}
if(action==="toggle"){consttoggleButton=event.target.closest(".toggle"),wasOn=toggleButton.classList.contains("on");toggleButton.classList.toggle("on",!wasOn);toggleButton.disabled=true;constbase=kind==="proxy"?"proxies":"sites";try{awaitapi(`/api/${base}/${card.dataset.id}/toggle`,{method:"POST"});awaitrefresh();toast("Status and gateway configuration updated.");}catch(error){toggleButton.classList.toggle("on",wasOn);toggleButton.disabled=false;toast(error.message||"Could not update status.","error");}}
if(action==="delete"){state.pendingDelete={kind,id:card.dataset.id};$("#confirm-title").textContent=kind==="proxy"?"Delete this proxy host?":"Delete this hosted site?";$("#confirm-copy").textContent=kind==="proxy"?"Its domain route will be removed from the gateway.":"Its route and uploaded files will be permanently removed.";$("#confirm-dialog").showModal();}
// --- Delete confirmation dialog and replace-files handler ------------------------------------
$("#confirm-dialog").addEventListener("close",async()=>{if($("#confirm-dialog").returnValue==="confirm"&&state.pendingDelete){constbase=state.pendingDelete.kind==="proxy"?"proxies":"sites";awaitapi(`/api/${base}/${state.pendingDelete.id}`,{method:"DELETE"});awaitrefresh();toast("Entry deleted and gateway updated.");}state.pendingDelete=null;});
if(mode==="abort"){frame.closest(".default-site-preview-frame-wrap").classList.add("hidden");note.classList.remove("hidden");note.textContent="No page is shown for this mode \u2014 the connection is closed immediately.";return;}
if(mode==="redirect"){frame.closest(".default-site-preview-frame-wrap").classList.add("hidden");note.classList.remove("hidden");constdestination=form.elements.redirectUrl.value.trim();note.textContent=destination?`No page is shown for this mode \u2014 visitors are redirected to ${destination}.`:"No page is shown for this mode \u2014 enter a redirect destination above.";return;}
if(mode==="abort"){frame.closest(".default-site-preview-frame-wrap").classList.add("hidden");note.classList.remove("hidden");note.textContent="No page is shown for this mode \u2014 the connection is closed immediately.";caption?.classList.add("hidden");return;}
if(mode==="redirect"){frame.closest(".default-site-preview-frame-wrap").classList.add("hidden");note.classList.remove("hidden");constdestination=form.elements.redirectUrl.value.trim();note.textContent=destination?`No page is shown for this mode \u2014 visitors are redirected to ${destination}.`:"No page is shown for this mode \u2014 enter a redirect destination above.";caption?.classList.add("hidden");return;}
if(mode==="custom"){frame.srcdoc=form.elements.customHtml.value||"<p style=\"font:14px sans-serif;color:#95a4ba;padding:20px\">Enter Custom HTML above to preview it here.</p>";return;}
consttitle=form.elements.title.value||(mode==="welcome"?"Gateway ready":"Route not found");
constmessage=form.elements.message.value||"The gateway is responding, but this address has not been configured.";
<small>Updates as you type. Scripts do not run in this preview.</small>
<smallid="default-site-preview-caption">Updates as you type. Scripts do not run in this preview.</small>
</div>
</div>
</section>
<!-- Backup & restore tab -->
<sectiondata-admin-panel="backups"class="hidden settings-panel"><divclass="panel-heading"><div><h2>Backup & restore</h2><pclass="muted">Create a copy, restore a previous version, or schedule automatic backups.</p></div><divclass="row-actions"><buttonid="import-backup"class="button secondary">Import backup</button><buttonid="create-backup"class="button primary">Create backup</button></div></div><inputid="backup-upload"type="file"accept=".sgbackup,application/zip"hidden><formid="backup-settings-form"class="settings-form backup-settings"><divclass="form-section form-section-wide"><pclass="eyebrow">Scheduled backups</p><divclass="form-grid"><labelclass="check-control"><inputname="enabled"type="checkbox"><span>Enable scheduled backups</span></label><label>Backup type<selectname="type"><optionvalue="complete">Complete (Recommended) — configuration + hosted files</option><optionvalue="configuration">Configuration only</option></select><smallid="backup-type-help">Configuration only includes settings and metadata, not uploaded Hosted Site files.</small></label><pid="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<selectname="frequency"><optionvalue="daily">Daily</option><optionvalue="weekly">Weekly</option><optionvalue="monthly">Monthly</option></select></label><label>Hour<selectname="hour"></select></label><label>Keep<inputname="retention"type="number"min="1"max="100"value="7"></label><labelclass="check-control"><inputname="includeLogs"type="checkbox"><span>Include logs</span></label></div></div><divclass="form-section form-section-wide"><pclass="eyebrow">Encryption</p><divclass="form-grid encryption-grid"><labelclass="backup-password-field"><spanclass="field-label">Backup encryption password <spanclass="optional">Optional</span></span><inputname="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><labelclass="check-control encryption-toggle"><inputname="encrypt"type="checkbox"><span>Encrypt scheduled backups<small>Uses the container’s <code>BACKUP_PASSWORD</code> value. Enable only after configuring that value.</small></span></label></div></div><divclass="dialog-actions"><buttonclass="button secondary">Save schedule</button></div></form><divclass="callout"><strong>Storage guidance</strong><spanid="backup-path">Backups are stored in /data/backups. Mount /backups separately to protect against appdata disk failure.</span><spanid="backup-summary"class="backup-summary"></span></div><divid="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>
<sectiondata-admin-panel="backups"class="hidden settings-panel"><divclass="panel-heading"><div><h2>Backup & restore</h2><pclass="muted">Create a copy, restore a previous version, or schedule automatic backups.</p></div><divclass="row-actions"><buttonid="import-backup"class="button secondary">Import backup</button><buttonid="create-backup"class="button primary">Create backup</button></div></div><inputid="backup-upload"type="file"accept=".sgbackup,application/zip"hidden><formid="backup-settings-form"class="settings-form backup-settings"><divclass="form-section form-section-wide"><pclass="eyebrow">Scheduled backups</p><divclass="form-grid"><labelclass="check-control"><inputname="enabled"type="checkbox"><span>Enable scheduled backups</span></label><label>Backup type<selectname="type"><optionvalue="complete">Complete (Recommended) — configuration + hosted files</option><optionvalue="configuration">Configuration only</option></select><smallid="backup-type-help">Configuration only includes settings and metadata, not uploaded Hosted Site files.</small></label><pid="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<selectname="frequency"><optionvalue="daily">Daily</option><optionvalue="weekly">Weekly</option><optionvalue="monthly">Monthly</option></select></label><label>Hour<selectname="hour"></select></label><label>Keep<inputname="retention"type="number"min="1"max="100"value="7"></label><labelclass="check-control"><inputname="includeLogs"type="checkbox"><span>Include logs</span></label></div></div><divclass="form-section form-section-wide"><pclass="eyebrow">Encryption</p><divclass="form-grid encryption-grid"><labelclass="backup-password-field"><spanclass="field-label">Backup encryption password <spanclass="optional">Optional</span></span><inputname="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><labelclass="check-control encryption-toggle"><inputname="encrypt"type="checkbox"><span>Encrypt scheduled backups<small>Uses the container’s <code>BACKUP_PASSWORD</code> value. Enable only after configuring that value.</small></span></label></div></div><divclass="dialog-actions"><buttonclass="button primary">Save schedule</button></div></form><divclass="callout"><strong>Storage guidance</strong><spanid="backup-path">Backups are stored in /data/backups. Mount /backups separately to protect against appdata disk failure.</span><spanid="backup-summary"class="backup-summary"></span></div><divid="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 -->
<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>
@@ -430,6 +430,6 @@
<divid="toast"class="toast"role="status"></div>
<divid="update-banner"class="update-banner hidden"role="status"><span>A new version of Site Gateway is available.</span><divclass="update-banner-actions"><buttonid="update-banner-refresh"class="button primary">Refresh</button><buttonid="update-banner-dismiss"class="text-button">Dismiss</button></div></div>
<!-- App scripts: core (app.js) then extended views/admin (features.js) -->
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user
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.