This commit is contained in:
Mateusz Gruszczyński committed 2026-11-08 21:26:30 +01:00
1 parent 213332d0b5
commit 1ac94213e0
13 files changed
+68 -28

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.36.6",
"version": "0.36.7",
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust. The API also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
},
"servers": [
+2 -1
View File
@@ -408,7 +408,7 @@ function cacheOutcomeMix(totals) {
return { counts, percentages: counts.map(value => value / requests * 100), requests };
}
// Percent labels live inside the bar segments; a label that does not fit is hidden
// (the exact count and percentage stay in the segment and legend tooltips).
// (the legend carries the request counts; tooltips give both).
function fitCacheMixLabels() {
const bar = $("cache-mix-bar");
if (!bar) return;
@@ -430,6 +430,7 @@ function renderCacheMix(mix, totalRequests) {
segment.style.width = `${pct}%`;
segment.textContent = mix && pct > 0 ? `${fmt(pct, pct < 10 ? 1 : 0)}%` : "";
const detail = mix ? `${name.toUpperCase()}: ${fmt(mix.counts[i])} (${fmt(pct, 1)}%)` : "";
$(`cache-mix-${name}-count`).textContent = mix ? fmt(mix.counts[i]) : "-";
segment.title = detail; $(`cache-mix-${name}-key`).title = detail;
});
fitCacheMixLabels();
+11 -4
View File
@@ -69,7 +69,9 @@
const reason = value => { const key = `cacheReason_${value}`, text = t(key); return text === key ? value : text; };
// Decision counters: label + count + share bar; top three visible, the rest folded into a details toggle.
function reasonsCell(reasons) {
// Folded counters keep their open/closed state across auto-refresh re-renders.
const openReasonRows = new Set();
function reasonsCell(reasons, rowKey = "") {
const td = el("td", undefined, "cache-reasons"), entries = Object.entries(reasons || {}).sort((a, b) => b[1] - a[1]);
if (!entries.length) { td.append(el("span", "-", "cache-reason-none")); return td; }
const max = entries[0][1] || 1, line = ([key, count]) => {
@@ -78,7 +80,7 @@
item.append(el("span", reason(key), "cache-reason-name"), el("b", number(count), "cache-reason-count"), track); item.title = key; return item;
};
entries.slice(0, 3).forEach(entry => td.append(line(entry)));
if (entries.length > 3) { const more = el("details", undefined, "cache-reason-more"); more.append(el("summary", `+${entries.length - 3}`)); entries.slice(3).forEach(entry => more.append(line(entry))); td.append(more); }
if (entries.length > 3) { const more = el("details", undefined, "cache-reason-more"); more.open = openReasonRows.has(rowKey); more.addEventListener("toggle", () => { if (more.open) openReasonRows.add(rowKey); else openReasonRows.delete(rowKey); }); more.append(el("summary", `+${entries.length - 3}`)); entries.slice(3).forEach(entry => more.append(line(entry))); td.append(more); }
return td;
}
function resetLiveChart(selected = scope()) {
@@ -182,7 +184,7 @@
[[row.hits, "cache-outcome-hit"], [row.misses, "cache-outcome-miss"], [row.bypasses, "cache-outcome-bypass"]].forEach(([count, cls], index) => { if (index) outcomes.append(el("span", "/", "cache-outcome-sep")); outcomes.append(el("span", number(count), cls)); });
const ratioPercent = Math.max(0, Math.min(100, (row.hit_ratio || 0) * 100)), ratio = el("td", undefined, "cache-ratio-cell"), meter = el("span", undefined, "cache-ratio-meter"), fill = el("i"); fill.style.width = `${ratioPercent}%`; meter.append(fill); ratio.append(el("span", `${fmt(ratioPercent, 1)}%`, "cache-ratio-value"), meter);
tr.append(outcomes, ratio);
tr.append(reasonsCell(row.reasons)); return tr;
tr.append(reasonsCell(row.reasons, `${row.vhost || ""}\u0000${row.domain || ""}`)); return tr;
}));
const domainEmpty = $("cache-domains-empty");
domainEmpty.textContent = groups.length ? t("cacheNoDomainsFiltered") : t("cacheNoDomains");
@@ -191,7 +193,12 @@
const tr = el("tr"), resident = Number(row.resident_bytes) || 0, disk = Number(row.storage_bytes) || 0, body = Number(row.body_bytes) || 0, storage = resident || disk;
tr.append(el("td", row.vhost || t("cacheLegacy")), el("td", row.domain));
const targetValue = row.target || row.path || "-", target = makeCopyCell(targetValue);
if (row.transformed) target.append(el("span", t("transformBadge"), "cache-transform-badge"));
if (row.transformed) {
// Keep the badge outside the ellipsised text so a long URL never clips it.
const wrap = el("div", undefined, "cache-target-wrap");
wrap.append(el("span", targetValue, "cache-target-text"), el("span", t("transformBadge"), "cache-transform-badge"));
target.replaceChildren(wrap);
}
tr.append(target);
const keyTags = `${row.cache_key || "-"}${row.tags?.length ? ` · ${row.tags.join(", ")}` : ""}`;
tr.append(makeCopyCell(keyTags));
+6
View File
@@ -2246,6 +2246,12 @@ html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .over
background: var(--chart-secondary);
}
#panel-overview .cache-mix-legend b {
margin-left: 1px;
color: var(--text);
font: 500 9px var(--mono);
}
#panel-overview .system-card .system-uptime-meta,
#panel-overview .connection-card .connection-backends-meta {
white-space: nowrap;
+18
View File
@@ -1184,3 +1184,21 @@ html[data-table-density=ultra] .file-log-inline-detail pre {
.transform-reason-list .cache-reason-line { margin: 0; }
.cache-ratio-meter, .cache-ratio-meter>i { border-radius: var(--rp-progress-radius, 0); }
/* Decision counters (0.36.7): name + count on one line, a thin share bar below (relative to the largest counter) */
.cache-reasons .cache-reason-line { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 10px; align-items: baseline; margin: 0 0 6px; white-space: nowrap; }
.cache-reasons .cache-reason-name { overflow: hidden; text-overflow: ellipsis; }
.cache-reasons .cache-reason-track { grid-column: 1 / -1; display: block; height: 2px; margin-top: 3px; background: var(--line); border-radius: var(--rp-progress-radius, 0); overflow: hidden; }
.cache-reasons .cache-reason-track>i { display: block; height: 100%; background: var(--muted); opacity: .7; border-radius: var(--rp-progress-radius, 0); }
.cache-reasons .cache-reason-more { cursor: default; }
.cache-reasons .cache-reason-more>summary { cursor: pointer; display: inline-flex; align-items: center; gap: 5px; margin: 0 0 6px; padding: 1px 6px; border: 1px solid var(--line); border-radius: var(--rp-radius-control, 0); font: 10px/1.5 var(--mono); color: var(--muted); list-style: none; user-select: none; }
.cache-reasons .cache-reason-more>summary::-webkit-details-marker { display: none; }
.cache-reasons .cache-reason-more>summary::before { content: "›"; display: inline-block; font-size: 12px; line-height: 1; transition: transform .15s ease; }
.cache-reasons .cache-reason-more[open]>summary::before { transform: rotate(90deg); }
.cache-reasons .cache-reason-more>summary:hover { color: var(--text); border-color: var(--muted); }
/* Transformed badge (0.36.7): the URL ellipsises, the badge always stays visible */
.cache-target-wrap { display: flex; align-items: center; gap: 6px; min-width: 0; }
.cache-target-text { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cache-target-wrap .cache-transform-badge { flex: 0 0 auto; margin-left: 0; }
+4 -4
View File
@@ -46,10 +46,10 @@
<span class="cache-mix-other" id="cache-mix-other" style="width:0%"></span>
</div>
<small class="cache-mix-legend">
<span class="cache-mix-hit-key" id="cache-mix-hit-key"><i></i>HIT</span>
<span class="cache-mix-miss-key" id="cache-mix-miss-key"><i></i>MISS</span>
<span class="cache-mix-bypass-key" id="cache-mix-bypass-key"><i></i>BYPASS</span>
<span class="cache-mix-other-key" id="cache-mix-other-key"><i></i><span data-i18n="cacheMixOther">OTHER</span></span>
<span class="cache-mix-hit-key" id="cache-mix-hit-key"><i></i>HIT <b id="cache-mix-hit-count">-</b></span>
<span class="cache-mix-miss-key" id="cache-mix-miss-key"><i></i>MISS <b id="cache-mix-miss-count">-</b></span>
<span class="cache-mix-bypass-key" id="cache-mix-bypass-key"><i></i>BYPASS <b id="cache-mix-bypass-count">-</b></span>
<span class="cache-mix-other-key" id="cache-mix-other-key"><i></i><span data-i18n="cacheMixOther">OTHER</span> <b id="cache-mix-other-count">-</b></span>
</small>
</article>
<article class="stat" data-dashboard-widget="memory"><span data-i18n="processMemory">Process