Compare commits

...
3 Commits
Author SHA1 Message Date
mrhid6 3e4ccc9720 feat: Added more debug logging
Chart Release / chart (push) Successful in 16s
Server Deploy / deploy (push) Successful in 7m1s
2026-08-25 13:26:23 +00:00
mrhid6 e5947489e4 fix: Fixed status page published switch 2026-08-25 13:26:10 +00:00
mrhid6 0a7a10aeed feat: Added status pages to license page 2026-08-25 13:10:28 +00:00
6 changed files with 50 additions and 13 deletions
+28 -7
View File
@@ -2,6 +2,7 @@ package api
import (
"errors"
"log"
"net/http"
"strconv"
"time"
@@ -79,17 +80,24 @@ func RateLimitPublicStatus() gin.HandlerFunc {
// @Failure 404 {object} ErrorResponse
// @Failure 429 {object} ErrorResponse
func getPublicStatusPage(c *gin.Context) {
pageID := c.Param("pageId")
inst, ok := publicStatusInstance(c)
if !ok {
// Every 404 on this route is indistinguishable to the caller by
// design, so the log is the only place the three reasons are told
// apart. It carries no monitor data and no page contents.
log.Printf("public status: 404 page=%q reason=no_instance", pageID)
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
snap, err := services.PublicStatusSnapshot(inst.InstanceID, c.Param("pageId"))
snap, err := services.PublicStatusSnapshot(inst.InstanceID, pageID)
if errors.Is(err, services.ErrPageNotFound) {
log.Printf("public status: 404 page=%q instance=%s reason=page_missing_or_unpublished", pageID, inst.InstanceID)
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
if err != nil {
log.Printf("public status: 500 page=%q instance=%s: %v", pageID, inst.InstanceID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
return
}
@@ -114,21 +122,34 @@ func getPublicStatusPage(c *gin.Context) {
// without this every self-hosted status page 404s forever. More than one is a
// refusal rather than a guess.
func publicStatusInstance(c *gin.Context) (*models.Instance, bool) {
// Host resolution is where this route fails silently: an untrusted peer
// means X-Forwarded-Host is ignored and the request host is the Go
// service's own name, which names no slug. Log the inputs and the branch
// taken, so the 404 says which of the four it was.
host := c.Request.Host
if trustedPeer(c) {
if h := firstForwarded(c.GetHeader("X-Forwarded-Host")); h != "" {
host = h
}
xfh := firstForwarded(c.GetHeader("X-Forwarded-Host"))
trusted := trustedPeer(c)
if trusted && xfh != "" {
host = xfh
}
log.Printf("public status: resolve peer=%s trusted=%t request_host=%q x_forwarded_host=%q using_host=%q slug=%q",
c.RemoteIP(), trusted, c.Request.Host, xfh, host, auth.HostSlug(host))
if inst, ok := auth.InstanceForHost(host); ok {
return inst, true
}
if auth.HostSlug(host) != "" {
if slug := auth.HostSlug(host); slug != "" {
// The host named an instance and that instance does not exist.
log.Printf("public status: no instance for slug=%q (host=%q)", slug, host)
return nil, false
}
if services.DeploymentMode() == license.DeploymentCloud {
log.Printf("public status: host %q names no slug and deployment is cloud, refusing to guess", host)
return nil, false
}
return auth.SoleInstance()
inst, ok := auth.SoleInstance()
if !ok {
log.Printf("public status: host %q names no slug and this deployment has no single instance", host)
}
return inst, ok
}
@@ -405,6 +405,7 @@ func unavailableSnapshot(reason, title string) *StatusSnapshot {
// unpublished" would confirm it exists.
func PublicStatusSnapshot(instanceID, pageID string) (*StatusSnapshot, error) {
if err := ValidatePageID(pageID); err != nil {
log.Printf("public status: page id %q is not a valid id: %v", pageID, err)
return nil, ErrPageNotFound
}
if snap := cachedSnapshot(instanceID, pageID); snap != nil {
@@ -413,9 +414,11 @@ func PublicStatusSnapshot(instanceID, pageID string) (*StatusSnapshot, error) {
page, err := GetStatusPage(instanceID, pageID)
if err != nil {
log.Printf("public status: instance=%s page=%q lookup: %v", instanceID, pageID, err)
return nil, err
}
if !page.Published {
log.Printf("public status: instance=%s page=%q exists but published=false", instanceID, pageID)
return nil, ErrPageNotFound
}
+6 -1
View File
@@ -171,7 +171,11 @@ function RecordPanel({ license }: { license: LicenseInfo }) {
<Keyed label="Instance ID">
<div className="flex items-center gap-2">
<code className="truncate font-mono text-xs text-text-primary">{license.instance_id}</code>
<button type="button" onClick={copyId} className="flex-shrink-0 rounded-sm border border-border px-1.5 py-0.5 font-mono text-[0.6rem] uppercase tracking-[0.1em] text-text-secondary transition-colors hover:border-text-tertiary hover:text-text-primary">
<button
type="button"
onClick={copyId}
className="flex-shrink-0 rounded-sm border border-border px-1.5 py-0.5 font-mono text-[0.6rem] uppercase tracking-[0.1em] text-text-secondary transition-colors hover:border-text-tertiary hover:text-text-primary"
>
{copied ? "Copied" : "Copy"}
</button>
</div>
@@ -257,6 +261,7 @@ export default function LicensePage() {
<Feature label="Browser console" included={Boolean(license.features.console)} />
<Feature label="Single sign-on" included={Boolean(license.features.oidc)} />
<Feature label="Vulnerability Scanning" included={Boolean(license.features.vuln_scanning)} />
<Feature label="Status Pages" included={Boolean(license.features.status_pages)} />
</div>
</Card>
</Group>
+2 -2
View File
@@ -107,10 +107,10 @@ function Switch({ checked, onChange, label }: { checked: boolean; onChange: (v:
aria-checked={checked}
aria-label={label}
onClick={() => onChange(!checked)}
className={`relative h-[21px] w-[38px] flex-shrink-0 rounded-full transition-colors ${checked ? "bg-success" : "bg-border"}`}
className={`relative h-[21px] w-[38px] flex-shrink-0 rounded-full border-0 p-0 transition-colors ${checked ? "bg-success" : "bg-border"}`}
>
<span
className={`absolute top-[2px] h-[17px] w-[17px] rounded-full transition-transform ${
className={`absolute left-0 top-[2px] h-[17px] w-[17px] rounded-full transition-transform ${
checked ? "translate-x-[19px] bg-accent-ink" : "translate-x-[2px] bg-text-tertiary"
}`}
/>
+10 -2
View File
@@ -32,17 +32,25 @@ async function fetchSnapshot(host: string, forwardedFor: string, pageId: string)
// intact.
if (forwardedFor) outbound["X-Forwarded-For"] = forwardedFor;
const url = `${base}/public/status/${encodeURIComponent(pageId)}`;
// This call is container-to-container and never appears in the front
// proxy's access log, which is why a 404 here reads as "Next 404'd it".
// Log both halves so the upstream status is visible in the web logs.
console.log(`[status] GET ${url} host=${host} xff=${forwardedFor || "-"}`);
let res: Response;
try {
res = await fetch(`${base}/public/status/${encodeURIComponent(pageId)}`, {
res = await fetch(url, {
headers: outbound,
cache: "no-store",
});
} catch {
} catch (e) {
// The control plane is unreachable. That is not "no such page".
console.error(`[status] upstream unreachable ${url}:`, e);
return { kind: "unavailable" };
}
console.log(`[status] upstream ${res.status} for ${url}`);
if (res.status === 404) return { kind: "not-found" };
// A 429, a 500 or anything else is a page that exists and cannot be read
// right now. Telling a customer mid-outage that their status page does not
File diff suppressed because one or more lines are too long