`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>
@@ -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.