Merge Certificates page into one sticky-header table
This commit is contained in:
+27
-16
@@ -265,26 +265,37 @@ function renderCertificates() {
|
||||
$("#cert-healthy").textContent = data.summary.healthy; $("#cert-30").textContent = data.summary.within30Days; $("#cert-7").textContent = data.summary.within7Days; $("#cert-warning").textContent = data.summary.warning + data.summary.critical + data.summary.expired + data.summary.mismatch; $("#cert-pending").textContent = data.summary.pending;
|
||||
const ageMinutes = (Date.now() - new Date(data.checkedAt).getTime()) / 60000, stale = ageMinutes > (data.thresholds?.staleMinutes || 10);
|
||||
$("#cert-last-checked").textContent = `Last checked ${formatTime(data.checkedAt)} · ${stale ? "data may be stale" : "current"}`;
|
||||
$("#certificate-list").innerHTML = data.certificates.length ? data.certificates.map(cert => `<details class="certificate-row"><summary><span class="status-dot ${cert.status === "healthy" ? "running" : cert.status === "pending" ? "idle" : "error"}"></span><span><strong>${escapeHtml(cert.domain)}</strong><small>${escapeHtml(cert.kind)} · ${escapeHtml(cert.name)} · ${escapeHtml(cert.source)}</small></span><span><strong>${cert.expiresAt ? `${cert.daysRemaining} days remaining` : cert.status === "mismatch" ? "Domain mismatch" : "Not detected"}</strong><small>${cert.expiresAt ? `Expires ${formatTime(cert.expiresAt)}` : cert.mismatch ? `Covers: ${(cert.coveredNames || []).map(escapeHtml).join(", ") || "no DNS names"}` : "No stored certificate was found"}</small></span></summary><dl class="certificate-details"><div><dt>Status</dt><dd>${escapeHtml(cert.status)}</dd></div><div><dt>Valid from</dt><dd>${cert.validFrom ? escapeHtml(formatTime(cert.validFrom)) : "—"}</dd></div><div><dt>Issuer</dt><dd>${escapeHtml(cert.issuer || "—")}</dd></div><div><dt>Covered domains</dt><dd>${escapeHtml((cert.coveredNames || []).join(", ") || "—")}</dd></div><div><dt>Serial number</dt><dd>${escapeHtml(cert.serialNumber || "—")}</dd></div><div><dt>SHA-256 fingerprint</dt><dd>${escapeHtml(cert.fingerprint || "—")}</dd></div><div><dt>Last detected update</dt><dd>${cert.updatedAt ? escapeHtml(formatTime(cert.updatedAt)) : "—"}</dd></div></dl></details>`).join("") : '<p class="quiet-state padded">No HTTPS domains are configured.</p>';
|
||||
renderReadiness();
|
||||
}
|
||||
|
||||
|
||||
// --- Domain readiness (used inside the Certificates view) ---------------------------
|
||||
function renderReadiness() {
|
||||
const routes = state.readiness?.routes || [];
|
||||
$("#readiness-list").innerHTML = routes.length ? routes.map(item => {
|
||||
const dnsOk = item.dns.healthy, portsOk = item.ports.http && item.ports.https !== false;
|
||||
const tlsOk = ["healthy", "warning", "critical", "not-configured"].includes(item.tls.status);
|
||||
const upstreamOk = !item.upstream || item.upstream.status === "healthy";
|
||||
const check = item.upstream;
|
||||
const message = !dnsOk ? `DNS failed${item.dns.error ? ` · ${item.dns.error}` : ""}` : !item.ports.http ? "HTTP port 80 is not responding inside the container" : item.ports.https === false ? "HTTPS port 443 is not responding inside the container" : !tlsOk ? `TLS ${item.tls.status.replaceAll("-", " ")}` : !upstreamOk ? `Upstream ${check?.error || "unavailable"}` : `Ready · DNS ${item.dns.addresses.join(", ")}${check ? ` · upstream ${check.httpStatus || "responding"}` : ""}`;
|
||||
const upstreamDetail = check ? `<div><dt>Upstream</dt><dd>Expected ${escapeHtml(item.upstreamExpected || "200-499")} · received ${check.httpStatus ?? "no response"}${check.responseMs != null ? ` · ${check.responseMs} ms` : ""} · ${check.attempts || 1} attempt${(check.attempts || 1) === 1 ? "" : "s"}</dd></div><div><dt>Last checked</dt><dd>${escapeHtml(formatTime(check.checkedAt))}</dd></div>${check.error ? `<div><dt>Failure detail</dt><dd class="danger-text">${escapeHtml(check.error)}</dd></div>` : ""}` : "<div><dt>Upstream</dt><dd>No upstream health check configured.</dd></div>";
|
||||
return `<details class="certificate-row readiness-row"><summary><span class="status-dot ${dnsOk && portsOk && tlsOk && upstreamOk ? "running" : "error"}"></span><span><strong>${escapeHtml(item.domain)}</strong><small>${escapeHtml(message)}</small></span></summary><dl class="certificate-details"><div><dt>DNS</dt><dd>${item.dns.healthy ? `Resolved${item.dns.addresses.length ? ` · ${escapeHtml(item.dns.addresses.join(", "))}` : ""}` : `Failed${item.dns.error ? ` · ${escapeHtml(item.dns.error)}` : ""}`}</dd></div><div><dt>Gateway ports</dt><dd>HTTP 80 ${item.ports.http ? "responding" : "not responding"} · HTTPS 443 ${item.ports.https === false ? "not responding" : "responding"}</dd></div><div><dt>TLS</dt><dd>${escapeHtml(item.tls.status.replaceAll("-", " "))}</dd></div>${upstreamDetail}</dl></details>`;
|
||||
}).join("") : '<p class="quiet-state">No configured domains to check.</p>';
|
||||
state.certRows = data.certificates.map(cert => ({ cert, readiness: routes.find(item => item.domain === cert.domain) || null }));
|
||||
$("#certificate-list").innerHTML = state.certRows.length ? state.certRows.map((row, index) => {
|
||||
const cert = row.cert, item = row.readiness;
|
||||
const dnsOk = item ? item.dns.healthy : null;
|
||||
const tlsOk = item ? ["healthy", "warning", "critical", "not-configured"].includes(item.tls.status) : null;
|
||||
const upstreamOk = item ? (!item.upstream || item.upstream.status === "healthy") : null;
|
||||
const dnsCell = item ? `<span class="status-dot ${dnsOk ? "running" : "error"}"></span>${dnsOk ? "Resolved" : "Failed"}` : `<span class="status-dot idle"></span>—`;
|
||||
const tlsCell = item ? `<span class="status-dot ${tlsOk ? "running" : "error"}"></span>${escapeHtml(item.tls.status.replaceAll("-", " "))}` : `<span class="status-dot idle"></span>—`;
|
||||
const upstreamCell = item ? (item.upstream ? `<span class="status-dot ${upstreamOk ? "running" : "error"}"></span>${item.upstream.httpStatus ?? "no response"}` : `<span class="status-dot idle"></span>Not configured`) : `<span class="status-dot idle"></span>—`;
|
||||
const statusLabel = cert.status === "mismatch" ? "Domain mismatch" : cert.status.charAt(0).toUpperCase() + cert.status.slice(1);
|
||||
return `<tr class="cert-table-row" data-index="${index}" tabindex="0"><td><strong>${escapeHtml(cert.domain)}</strong><br><small class="muted">${escapeHtml(cert.kind)} · ${escapeHtml(cert.source)}</small></td><td><span class="status-dot ${cert.status === "healthy" ? "running" : cert.status === "pending" ? "idle" : "error"}"></span>${escapeHtml(statusLabel)}</td><td>${cert.expiresAt ? `${cert.daysRemaining} days` : "—"}</td><td>${escapeHtml(cert.issuer || "—")}</td><td>${dnsCell}</td><td>${tlsCell}</td><td>${upstreamCell}</td></tr>`;
|
||||
}).join("") : '<tr><td colspan="7" class="quiet-state">No HTTPS domains are configured.</td></tr>';
|
||||
}
|
||||
|
||||
|
||||
// --- Certificate detail popup (deep fields for a single certificate row) ------------
|
||||
function openCertificateDetail(row) {
|
||||
const cert = row.cert, item = row.readiness;
|
||||
$("#cert-detail-title").textContent = cert.domain;
|
||||
$("#cert-detail-eyebrow").textContent = `${cert.kind} · ${cert.source}`;
|
||||
const certRows = `<div><dt>Status</dt><dd>${escapeHtml(cert.status)}</dd></div><div><dt>Valid from</dt><dd>${cert.validFrom ? escapeHtml(formatTime(cert.validFrom)) : "—"}</dd></div><div><dt>Expires</dt><dd>${cert.expiresAt ? escapeHtml(formatTime(cert.expiresAt)) : "—"}</dd></div><div><dt>Issuer</dt><dd>${escapeHtml(cert.issuer || "—")}</dd></div><div><dt>Covered domains</dt><dd>${escapeHtml((cert.coveredNames || []).join(", ") || "—")}</dd></div><div><dt>Serial number</dt><dd>${escapeHtml(cert.serialNumber || "—")}</dd></div><div><dt>SHA-256 fingerprint</dt><dd>${escapeHtml(cert.fingerprint || "—")}</dd></div><div><dt>Last detected update</dt><dd>${cert.updatedAt ? escapeHtml(formatTime(cert.updatedAt)) : "—"}</dd></div>`;
|
||||
const readinessRows = item ? `<div><dt>DNS</dt><dd>${item.dns.healthy ? `Resolved${item.dns.addresses.length ? ` · ${escapeHtml(item.dns.addresses.join(", "))}` : ""}` : `Failed${item.dns.error ? ` · ${escapeHtml(item.dns.error)}` : ""}`}</dd></div><div><dt>Gateway ports</dt><dd>HTTP 80 ${item.ports.http ? "responding" : "not responding"} · HTTPS 443 ${item.ports.https === false ? "not responding" : "responding"}</dd></div><div><dt>TLS</dt><dd>${escapeHtml(item.tls.status.replaceAll("-", " "))}</dd></div>${item.upstream ? `<div><dt>Upstream</dt><dd>Expected ${escapeHtml(item.upstreamExpected || "200-499")} · received ${item.upstream.httpStatus ?? "no response"}${item.upstream.responseMs != null ? ` · ${item.upstream.responseMs} ms` : ""} · ${item.upstream.attempts || 1} attempt${(item.upstream.attempts || 1) === 1 ? "" : "s"}</dd></div><div><dt>Last checked</dt><dd>${escapeHtml(formatTime(item.upstream.checkedAt))}</dd></div>${item.upstream.error ? `<div><dt>Failure detail</dt><dd class="danger-text">${escapeHtml(item.upstream.error)}</dd></div>` : ""}` : "<div><dt>Upstream</dt><dd>No upstream health check configured.</dd></div>"}` : "<div><dt>Domain readiness</dt><dd>No readiness data available for this domain.</dd></div>";
|
||||
$("#cert-detail-body").innerHTML = certRows + readinessRows;
|
||||
$("#certificate-detail-dialog").showModal();
|
||||
}
|
||||
$("#certificate-list").addEventListener("click", event => { const row = event.target.closest(".cert-table-row"); if (!row) return; const data = state.certRows?.[Number(row.dataset.index)]; if (data) openCertificateDetail(data); });
|
||||
$("#certificate-list").addEventListener("keydown", event => { if (event.key !== "Enter" && event.key !== " ") return; const row = event.target.closest(".cert-table-row"); if (!row) return; event.preventDefault(); const data = state.certRows?.[Number(row.dataset.index)]; if (data) openCertificateDetail(data); });
|
||||
$("#cert-threshold-trigger").addEventListener("click", () => { renderHealthSettings(); $("#health-settings-dialog").showModal(); });
|
||||
|
||||
|
||||
// --- Logs view -------------------------------------------------------------------------
|
||||
function renderLogs() {
|
||||
const data = state.logs; if (!data) return;
|
||||
|
||||
File diff suppressed because one or more lines are too long
+23
-4
@@ -8,7 +8,7 @@
|
||||
<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?v=0.16.50">
|
||||
<link rel="stylesheet" href="/styles.css?v=0.16.51">
|
||||
</head>
|
||||
|
||||
<!-- ================================================================
|
||||
@@ -161,10 +161,29 @@
|
||||
<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">Checking configured domains…</p></div></section><section class="dashboard-panel"><div class="panel-heading"><div><p class="eyebrow">Certificate health</p><h2>Renewal thresholds</h2><p class="muted">Control when a certificate is flagged as renewing soon, critical, or stale on this page and the dashboard.</p></div></div><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>
|
||||
<div class="diagnostic-section-heading"><div><p class="eyebrow">Certificate inventory</p><h2>Certificates</h2><p class="muted">Managed certificates, TLS readiness, and renewal status for every configured domain. Select a row for full detail.</p></div><button type="button" id="cert-threshold-trigger" class="text-button">Configure thresholds →</button></div>
|
||||
<div class="table-wrap performance-table-wrap cert-table-wrap"><table class="performance-table cert-table"><thead><tr><th>Domain</th><th>Status</th><th>Days remaining</th><th>Issuer</th><th>DNS</th><th>TLS</th><th>Upstream</th></tr></thead><tbody id="certificate-list"><tr><td colspan="7" class="quiet-state">Loading certificates…</td></tr></tbody></table></div>
|
||||
</section>
|
||||
|
||||
<dialog id="certificate-detail-dialog">
|
||||
<div class="dialog-card">
|
||||
<div class="dialog-heading"><div><p class="eyebrow" id="cert-detail-eyebrow">Certificate detail</p><h2 id="cert-detail-title">Domain</h2></div></div>
|
||||
<dl class="certificate-details" id="cert-detail-body"></dl>
|
||||
<div class="dialog-actions"><button type="button" class="button secondary close-dialog">Close</button></div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="health-settings-dialog">
|
||||
<form id="health-settings-form" class="dialog-card settings-form">
|
||||
<div class="dialog-heading"><div><p class="eyebrow">Certificate health</p><h2>Renewal thresholds</h2><p class="muted">Control when a certificate is flagged as renewing soon, critical, or stale on this page and the dashboard.</p></div></div>
|
||||
<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 type="button" class="button secondary close-dialog">Cancel</button><button class="button primary">Save health settings</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
|
||||
<!-- Logs view: access log table + gateway event/activity log -->
|
||||
<section id="logs-view" class="feature-view hidden">
|
||||
<section class="dashboard-panel">
|
||||
@@ -432,6 +451,6 @@
|
||||
<div id="toast" class="toast" role="status"></div>
|
||||
<div id="update-banner" class="update-banner hidden" role="status"><span>A new version of Site Gateway is available.</span><div class="update-banner-actions"><button id="update-banner-refresh" class="button primary">Refresh</button><button id="update-banner-dismiss" class="text-button">Dismiss</button></div></div>
|
||||
<!-- App scripts: core (app.js) then extended views/admin (features.js) -->
|
||||
<script src="/app.js?v=0.16.50" defer></script><script src="/features.js?v=0.16.50" defer></script><script src="/select-enhance.js?v=0.16.50" defer></script>
|
||||
<script src="/app.js?v=0.16.51" defer></script><script src="/features.js?v=0.16.51" defer></script><script src="/select-enhance.js?v=0.16.51" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -824,6 +824,15 @@ select{appearance:none!important;-webkit-appearance:none!important;background-re
|
||||
.diagnostic-section-heading p:last-child{margin:var(--space-1) 0 0}
|
||||
.diagnostic-section-heading{margin-bottom:0}
|
||||
.diagnostic-section-heading + .log-table-wrap{border-top:0;border-radius:0 0 15px 15px}
|
||||
.diagnostic-section-heading{display:flex;justify-content:space-between;align-items:flex-start;gap:var(--space-4)}
|
||||
.diagnostic-section-heading + .cert-table-wrap{border-top:0;border-radius:0 0 15px 15px}
|
||||
.cert-table-row{cursor:pointer}
|
||||
.cert-table-row:hover{background:rgba(var(--green-rgb),.06)}
|
||||
.cert-table-row:focus{outline:none}
|
||||
.cert-table-row:focus-visible{outline:2px solid var(--green);outline-offset:-2px}
|
||||
.cert-table td{vertical-align:middle}
|
||||
.cert-table td .status-dot{margin-right:6px;vertical-align:-1px}
|
||||
#cert-detail-body{padding:0;background:transparent;border-top:0;margin-top:var(--space-4)}
|
||||
#certificate-list.diagnostic-list,#readiness-list.diagnostic-list,#gateway-log-list.diagnostic-list,#audit-list.diagnostic-list{border-radius:0 0 15px 15px}
|
||||
.data-list>.quiet-state,.diagnostic-list>.quiet-state{padding:22px}
|
||||
.readiness-panel,.log-activity{background:transparent;border:0;padding:0}
|
||||
|
||||
Reference in New Issue
Block a user