v0.34.3
This commit is contained in:
1 parent
8c0de520d7
commit
8f41b39c26
32 files changed
+934
-128
No files matched your search
@@ -174,6 +174,7 @@ $("reload-certificates").addEventListener("click", async () => {
|
||||
$("reset-certificate").addEventListener("click", () => { $("certificate-form").reset(); $("certificate-file-source").value = "managed"; syncCertificateFileSource(); });
|
||||
syncCertificateFileSource();
|
||||
|
||||
let cacheMixLastCounters = null, cacheMixLastShares = null, cacheMixLastTotal = 0;
|
||||
let systemSamples = [], connectionOpenSamples = [], connectionExchangeSamples = [], cacheMemorySamples = [], cacheTransientSamples = [], cacheFillSamples = [], cacheColdQueueSamples = [], cacheColdIoSamples = [], cacheColdStorageSamples = [], cacheHitSamples = [], cacheMissSamples = [], cacheBypassSamples = [], lastCacheCounters = null, lastCacheOutcomeRates = null,
|
||||
processCpuSamples = [], processRssSamples = [], networkRxSamples = [], networkTxSamples = [], responseLatencySamples = [], networkInterfaceSamples = new Map(), settingsDirty = new Set();
|
||||
const hiddenNetworkInterfaces = (() => {
|
||||
@@ -330,6 +331,34 @@ function renderNetworkInterfaceCharts(interfaces, appendSample) {
|
||||
target.replaceChildren(...children);
|
||||
}
|
||||
}
|
||||
|
||||
function cacheOutcomeMix(deltas) {
|
||||
if (!Array.isArray(deltas) || deltas.length !== 4 ||
|
||||
deltas.some(value => !Number.isFinite(value) || value < 0)) return null;
|
||||
const [requests, hits, misses, bypasses] = deltas;
|
||||
const cacheTotal = hits + misses + bypasses;
|
||||
if (requests <= 0 || cacheTotal > requests) return null;
|
||||
const counts = [hits, misses, bypasses, requests - cacheTotal];
|
||||
return { counts, percentages: counts.map(value => value / requests * 100), requests };
|
||||
}
|
||||
function renderCacheMix(mix) {
|
||||
const names = ["hit", "miss", "bypass", "other"];
|
||||
names.forEach((name, i) => {
|
||||
const pct = mix ? mix.percentages[i] : 0;
|
||||
$(`cache-mix-${name}`).style.width = `${pct}%`;
|
||||
$(`cache-mix-${name}-percent`).textContent = mix ? `${fmt(pct, 1)}%` : "-";
|
||||
});
|
||||
$("cache-mix-headline").textContent = mix ? `${fmt(mix.percentages[0], 1)}% HIT` : "-";
|
||||
$("cache-mix-caption").textContent = mix
|
||||
? t("cacheMixSample").replace("{count}", fmt(mix.requests))
|
||||
: t("cacheMixWaiting");
|
||||
$("cache-mix-bar").setAttribute("aria-label", mix
|
||||
? t("cacheMixAria").replace("{hit}", fmt(mix.counts[0]))
|
||||
.replace("{miss}", fmt(mix.counts[1]))
|
||||
.replace("{bypass}", fmt(mix.counts[2]))
|
||||
.replace("{other}", fmt(mix.counts[3]))
|
||||
: t("cacheMixWaiting"));
|
||||
}
|
||||
function renderDiagnostics(status, appendSample = true, systemSample = appendSample) {
|
||||
const d = status.system || {}, live = status.live_system || d, c = status.cache || {}, m = status.metrics || {};
|
||||
$("metric-h3").textContent = fmt(m.http3_requests);
|
||||
@@ -337,6 +366,14 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
const cacheEvents = cacheHits !== null && cacheMisses !== null ? cacheHits + cacheMisses : 0;
|
||||
$("metric-cache-ratio").textContent = cacheEvents > 0 ? `${fmt(cacheHits / cacheEvents * 100, 1)}%` : "-";
|
||||
window.rustProxyExtraOverview?.donut?.("cache-ratio", cacheHits, cacheMisses);
|
||||
if (appendSample) {
|
||||
const counters = [m.requests, c.hits, c.misses, c.bypasses].map(value => value == null ? NaN : Number(value));
|
||||
const delta = rustProxyRateSampling.difference(cacheMixLastCounters, counters,
|
||||
performance.now(), rustProxyRateSampling.cadence("overview_seconds"));
|
||||
cacheMixLastCounters = counters.every(Number.isFinite) ? delta.next : null;
|
||||
cacheMixLastShares = delta.deltas ? cacheOutcomeMix(delta.deltas) : null;
|
||||
}
|
||||
renderCacheMix(cacheMixLastShares);
|
||||
if (systemSample) {
|
||||
if (d.process_rss_bytes != null) $("metric-rss").textContent = fmtBytes(d.process_rss_bytes);
|
||||
$("process-summary").hidden = d.process_threads == null;
|
||||
|
||||
@@ -1,43 +1,139 @@
|
||||
{
|
||||
"dashboard_widgets": [
|
||||
"requests", "active", "bandwidth", "errors", "memory", "websocket",
|
||||
"backend_traffic", "extra_response_latency", "http_protocol", "tls",
|
||||
"websocket_protocol", "backend_latency", "stream_overview", "stream_traffic",
|
||||
"stream_sessions", "stream_backend_health", "backend_health", "backend_activity",
|
||||
"backend_errors", "backend_retries", "http2", "backend_protocols",
|
||||
"backend_requests", "backend_capacity", "backend_http5xx", "backend_timeouts",
|
||||
"backend_network", "http3", "backend_queue", "extra_open_connections",
|
||||
"extra_process_cpu", "extra_process_rss", "extra_process_fds", "backend_rotations",
|
||||
"extra_websocket_active", "extra_tls_error_rate", "extra_request_error_rate",
|
||||
"extra_cache_hit_rate", "extra_backend_active", "extra_h2_share",
|
||||
"extra_cache_memory", "extra_backend_unhealthy", "extra_total_rps",
|
||||
"extra_active_exchanges", "extra_h1_share", "extra_h3_share",
|
||||
"extra_cache_miss_rate", "extra_cold_storage", "wide_protocol_rates",
|
||||
"wide_cpu", "wide_memory", "wide_cache_activity", "wide_connections",
|
||||
"wide_backend_health", "wide_latency", "cache", "traffic_chart",
|
||||
"system_chart", "connection_chart", "wide_transform_activity",
|
||||
"wide_compression_activity", "cache_diagnostics", "extra_cold_queue_memory",
|
||||
"process_diagnostics", "network_diagnostics", "backend_table",
|
||||
"www_status", "www_kind", "www_capacity", "www_methods", "www_error_rate",
|
||||
"www_php_requests", "www_requests", "www_latency", "www_failures",
|
||||
"www_traffic", "www_top_services", "www_status_trend"
|
||||
"requests",
|
||||
"active",
|
||||
"bandwidth",
|
||||
"errors",
|
||||
"memory",
|
||||
"websocket",
|
||||
"backend_traffic",
|
||||
"extra_response_latency",
|
||||
"http_protocol",
|
||||
"tls",
|
||||
"websocket_protocol",
|
||||
"backend_latency",
|
||||
"stream_overview",
|
||||
"stream_traffic",
|
||||
"stream_sessions",
|
||||
"stream_backend_health",
|
||||
"backend_health",
|
||||
"backend_activity",
|
||||
"backend_errors",
|
||||
"backend_retries",
|
||||
"http2",
|
||||
"backend_protocols",
|
||||
"backend_requests",
|
||||
"backend_capacity",
|
||||
"backend_http5xx",
|
||||
"backend_timeouts",
|
||||
"backend_network",
|
||||
"http3",
|
||||
"backend_queue",
|
||||
"extra_open_connections",
|
||||
"extra_process_cpu",
|
||||
"extra_process_rss",
|
||||
"extra_process_fds",
|
||||
"backend_rotations",
|
||||
"extra_websocket_active",
|
||||
"extra_tls_error_rate",
|
||||
"extra_request_error_rate",
|
||||
"extra_cache_hit_rate",
|
||||
"extra_backend_active",
|
||||
"extra_h2_share",
|
||||
"extra_cache_memory",
|
||||
"extra_backend_unhealthy",
|
||||
"extra_total_rps",
|
||||
"extra_active_exchanges",
|
||||
"extra_h1_share",
|
||||
"extra_h3_share",
|
||||
"extra_cache_miss_rate",
|
||||
"extra_cold_storage",
|
||||
"wide_protocol_rates",
|
||||
"wide_cpu",
|
||||
"wide_memory",
|
||||
"wide_cache_activity",
|
||||
"wide_connections",
|
||||
"wide_backend_health",
|
||||
"wide_latency",
|
||||
"cache",
|
||||
"cache_outcomes",
|
||||
"traffic_chart",
|
||||
"system_chart",
|
||||
"connection_chart",
|
||||
"wide_transform_activity",
|
||||
"wide_compression_activity",
|
||||
"cache_diagnostics",
|
||||
"extra_cold_queue_memory",
|
||||
"process_diagnostics",
|
||||
"network_diagnostics",
|
||||
"backend_table",
|
||||
"www_status",
|
||||
"www_kind",
|
||||
"www_capacity",
|
||||
"www_methods",
|
||||
"www_error_rate",
|
||||
"www_php_requests",
|
||||
"www_requests",
|
||||
"www_latency",
|
||||
"www_failures",
|
||||
"www_traffic",
|
||||
"www_top_services",
|
||||
"www_status_trend"
|
||||
],
|
||||
"dashboard_hidden_widgets": [
|
||||
"stream_overview", "stream_backend_health", "tls", "stream_sessions",
|
||||
"backend_health", "backend_activity", "backend_errors", "backend_protocols",
|
||||
"backend_requests", "backend_timeouts", "backend_network", "extra_process_cpu",
|
||||
"extra_process_rss", "extra_process_fds", "extra_websocket_active",
|
||||
"extra_tls_error_rate", "extra_request_error_rate", "extra_h2_share",
|
||||
"extra_backend_unhealthy", "extra_total_rps", "extra_h1_share",
|
||||
"extra_h3_share", "wide_memory", "wide_connections", "wide_backend_health",
|
||||
"wide_protocol_rates", "extra_active_exchanges", "extra_cache_miss_rate",
|
||||
"extra_cache_hit_rate", "backend_capacity", "stream_traffic", "backend_http5xx",
|
||||
"bandwidth", "backend_queue", "wide_cpu", "wide_latency", "www_status",
|
||||
"www_kind", "www_capacity", "www_methods", "www_error_rate",
|
||||
"www_php_requests", "www_requests", "www_latency", "www_failures",
|
||||
"www_traffic", "www_top_services", "www_status_trend",
|
||||
"extra_cold_queue_memory", "wide_cache_activity", "backend_rotations"
|
||||
"cache_outcomes",
|
||||
"stream_overview",
|
||||
"stream_backend_health",
|
||||
"tls",
|
||||
"stream_sessions",
|
||||
"backend_health",
|
||||
"backend_activity",
|
||||
"backend_errors",
|
||||
"backend_protocols",
|
||||
"backend_requests",
|
||||
"backend_timeouts",
|
||||
"backend_network",
|
||||
"extra_process_cpu",
|
||||
"extra_process_rss",
|
||||
"extra_process_fds",
|
||||
"extra_websocket_active",
|
||||
"extra_tls_error_rate",
|
||||
"extra_request_error_rate",
|
||||
"extra_h2_share",
|
||||
"extra_backend_unhealthy",
|
||||
"extra_total_rps",
|
||||
"extra_h1_share",
|
||||
"extra_h3_share",
|
||||
"wide_memory",
|
||||
"wide_connections",
|
||||
"wide_backend_health",
|
||||
"wide_protocol_rates",
|
||||
"extra_active_exchanges",
|
||||
"extra_cache_miss_rate",
|
||||
"extra_cache_hit_rate",
|
||||
"backend_capacity",
|
||||
"stream_traffic",
|
||||
"backend_http5xx",
|
||||
"bandwidth",
|
||||
"backend_queue",
|
||||
"wide_cpu",
|
||||
"wide_latency",
|
||||
"www_status",
|
||||
"www_kind",
|
||||
"www_capacity",
|
||||
"www_methods",
|
||||
"www_error_rate",
|
||||
"www_php_requests",
|
||||
"www_requests",
|
||||
"www_latency",
|
||||
"www_failures",
|
||||
"www_traffic",
|
||||
"www_top_services",
|
||||
"www_status_trend",
|
||||
"extra_cold_queue_memory",
|
||||
"wide_cache_activity",
|
||||
"backend_rotations"
|
||||
],
|
||||
"dashboard_density": "compact",
|
||||
"overview_samples": 600
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,17 @@
|
||||
"default_hidden": false,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "cache_outcomes",
|
||||
"label_key": "cacheMixTitle",
|
||||
"description_key": "cacheMixHelp",
|
||||
"element_selector": "[data-dashboard-widget=\"cache_outcomes\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "overview",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "memory",
|
||||
"label_key": "processMemory",
|
||||
@@ -857,4 +868,4 @@
|
||||
"default_hidden": true,
|
||||
"display_type": "chart"
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -2028,10 +2028,11 @@
|
||||
<form class="edit-form embedded-settings-form" id="http3-form">
|
||||
<div class="card-heading">
|
||||
<h3 data-i18n="http3Settings">HTTP/3 / QUIC</h3><span class="pill"
|
||||
data-i18n="restartLabel">RESTART</span>
|
||||
data-i18n="http3ApplyPolicy">LIVE / RESTART</span>
|
||||
</div>
|
||||
<p class="fine" data-i18n="http3SettingsHelp">HTTP/3 enablement is controlled in
|
||||
Listeners. Tune advertisement, connection and stream limits here.</p>
|
||||
Listeners. Tune live advertisement/stream limits and startup UDP socket
|
||||
buffers here.</p>
|
||||
<p class="pending-note" hidden="" id="http3-listeners-dirty"
|
||||
data-i18n="http3ListenersDirty">Save pending listener changes before editing
|
||||
HTTP/3 settings.</p>
|
||||
@@ -3053,7 +3054,8 @@
|
||||
role="tabpanel">
|
||||
<article class="card edit-form">
|
||||
<div class="card-heading">
|
||||
<div><span class="small-label" data-i18n="configStorageLabel">CONFIGURATION SOURCE</span>
|
||||
<div><span class="small-label" data-i18n="configStorageLabel">CONFIGURATION
|
||||
SOURCE</span>
|
||||
<h2 data-i18n="manualConfig">Manual config</h2>
|
||||
</div>
|
||||
<div class="manual-config-tools"><button class="quiet" data-i18n="configHistory"
|
||||
@@ -3062,7 +3064,8 @@
|
||||
JSON</button></div>
|
||||
</div>
|
||||
<div class="manual-config-source" role="status" aria-live="polite">
|
||||
<strong id="config-storage-mode" data-i18n="configStorageLoading">Checking storage...</strong>
|
||||
<strong id="config-storage-mode" data-i18n="configStorageLoading">Checking
|
||||
storage...</strong>
|
||||
<p id="config-storage-location" class="manual-config-path"></p>
|
||||
<p id="config-storage-description" class="fine"></p>
|
||||
<p id="config-storage-history" class="fine"></p>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"use strict";
|
||||
// Shared controls for named listeners, HTTP/2/HTTP/3 and the certificate inventory.
|
||||
// All labels are resolved from discovered language packs.
|
||||
(() => {
|
||||
let uploadBusy = false;
|
||||
const label = (tag, key, cls) => { const node = el(tag, t(key), cls); node.dataset.i18n = key; return node; };
|
||||
@@ -10,7 +8,7 @@
|
||||
if (type === "select") for (const [value, key] of options.values || []) { const option = label("option", key); option.value = value; input.append(option); }
|
||||
else if (type !== "textarea") input.type = type;
|
||||
if (type === "checkbox") input.checked = !!value; else input.value = value ?? "";
|
||||
for (const key of ["min", "max", "maxLength", "required", "pattern", "rows"]) if (Object.hasOwn(options, key)) input[key] = options[key];
|
||||
for (const key of ["min", "max", "maxLength", "required", "pattern", "rows", "step"]) if (Object.hasOwn(options, key)) input[key] = options[key];
|
||||
if (type === "checkbox") wrapper.append(input, label("span", key)); else wrapper.append(label("span", key), input);
|
||||
parent.append(wrapper); return input;
|
||||
}
|
||||
@@ -87,15 +85,33 @@
|
||||
};
|
||||
return { read, sync };
|
||||
}
|
||||
const h3Defaults = { alt_svc_max_age: 3600, max_concurrent_connections: 512, max_concurrent_bidistream: 100, max_concurrent_unistream: 100, max_idle_timeout: 10 };
|
||||
const h3Defaults = { alt_svc_max_age: 3600, max_concurrent_connections: 512, max_concurrent_bidistream: 100, max_concurrent_unistream: 100, max_idle_timeout: 10, udp_receive_buffer_bytes: 4194304, udp_send_buffer_bytes: 4194304 };
|
||||
function http3SettingsControls(parent, binding) {
|
||||
const box = el("div", undefined, "h3-editor vh-wide"); parent.append(box);
|
||||
const grid = el("div", undefined, "vh-grid"); box.append(grid); const inputs = {};
|
||||
for (const [name, min, max] of [["alt_svc_max_age", 0, 604800], ["max_concurrent_connections", 1, 4096], ["max_concurrent_bidistream", 1, 1024], ["max_concurrent_unistream", 3, 1024], ["max_idle_timeout", 0, 86400]]) inputs[name] = field(grid, name, `h3_${name}`, binding.http3?.[name] ?? h3Defaults[name], "number", { min, max, required: true });
|
||||
for (const direction of ["receive", "send"]) {
|
||||
const name = `udp_${direction}_buffer_bytes`;
|
||||
const mib = (binding.http3?.[name] ?? h3Defaults[name]) / 1048576;
|
||||
inputs[name] = field(grid, name, `h3_udp_${direction}_buffer_mib`, mib, "number", { min: 0, max: 16, step: "any", required: true });
|
||||
}
|
||||
box.append(label("p", "h3TuningHelp", "fine"));
|
||||
const sync = (blocked = false) => { for (const input of Object.values(inputs)) input.disabled = blocked; };
|
||||
sync(false);
|
||||
const read = () => { const value = {}; for (const [name, input] of Object.entries(inputs)) { value[name] = Number(input.value); if (!Number.isSafeInteger(value[name])) throw fieldError(input, t("vhJsonInvalid")); } return value; };
|
||||
const read = () => {
|
||||
const value = {};
|
||||
for (const [name, input] of Object.entries(inputs)) {
|
||||
let number = Number(input.value);
|
||||
if (!input.value.trim() || !Number.isFinite(number)) throw fieldError(input, t("vhJsonInvalid"));
|
||||
if (name.startsWith("udp_") && name.endsWith("_buffer_bytes")) {
|
||||
number = Math.round(number * 1048576);
|
||||
if (number !== 0 && (number < 65536 || number > 16777216)) throw fieldError(input, t("vhJsonInvalid"));
|
||||
}
|
||||
if (!Number.isSafeInteger(number)) throw fieldError(input, t("vhJsonInvalid"));
|
||||
value[name] = number;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
return { read, sync };
|
||||
}
|
||||
function addRow(item = { id: `listener-${$("named-listener-rows").children.length + 1}`, name: "", scope: "internal", enabled: true, binding: { address: "127.0.0.1:8181", tls: false, http2: false, http2_settings: null, h2c: false, http3: null } }) {
|
||||
|
||||
@@ -31,6 +31,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stat stat-cache-outcomes" data-dashboard-widget="cache_outcomes">
|
||||
<span data-i18n="cacheMixTitle">Cache outcomes (recent)</span>
|
||||
<strong id="cache-mix-headline">-</strong>
|
||||
<div class="cache-mix-bar" id="cache-mix-bar" role="img" aria-label="Recent request cache outcomes">
|
||||
<span class="cache-mix-hit" id="cache-mix-hit" style="width:0%"></span>
|
||||
<span class="cache-mix-miss" id="cache-mix-miss" style="width:0%"></span>
|
||||
<span class="cache-mix-bypass" id="cache-mix-bypass" style="width:0%"></span>
|
||||
<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"><i></i>HIT <b id="cache-mix-hit-percent">-</b></span>
|
||||
<span class="cache-mix-miss-key"><i></i>MISS <b id="cache-mix-miss-percent">-</b></span>
|
||||
<span class="cache-mix-bypass-key"><i></i>BYPASS <b id="cache-mix-bypass-percent">-</b></span>
|
||||
<span class="cache-mix-other-key"><i></i><span data-i18n="cacheMixOther">OTHER</span> <b
|
||||
id="cache-mix-other-percent">-</b></span>
|
||||
</small>
|
||||
<small class="cache-mix-caption" id="cache-mix-caption" data-i18n="cacheMixWaiting">Waiting for request
|
||||
samples</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="memory"><span data-i18n="processMemory">Process
|
||||
memory</span><strong id="metric-rss">-</strong><small id="process-summary">-</small>
|
||||
</article>
|
||||
|
||||
@@ -12193,4 +12193,87 @@ html[data-dashboard-density] #panel-overview .dashboard-overview-grid>.card.pool
|
||||
|
||||
.certificate-bulk-list .check {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
#panel-overview .stat-cache-outcomes .cache-mix-bar {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
margin: 7px 0 9px;
|
||||
border-radius: var(--rp-progress-radius);
|
||||
background: var(--rp-progress-track);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-bar>span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-hit {
|
||||
background: var(--chart-accent);
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-miss {
|
||||
background: var(--chart-secondary);
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-bypass {
|
||||
background: var(--chart-tertiary);
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-other {
|
||||
background: var(--muted);
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-legend {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 5px 8px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-legend>span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-legend i {
|
||||
display: inline-block;
|
||||
flex: 0 0 7px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-hit-key i {
|
||||
background: var(--chart-accent);
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-miss-key i {
|
||||
background: var(--chart-secondary);
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-bypass-key i {
|
||||
background: var(--chart-tertiary);
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-other-key i {
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
#panel-overview .cache-mix-legend b {
|
||||
margin-left: auto;
|
||||
color: var(--text);
|
||||
font: 500 10px var(--mono);
|
||||
}
|
||||
|
||||
#panel-overview .stat-cache-outcomes .cache-mix-caption {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: var(--muted);
|
||||
}
|
||||
Reference in new issue
Block a user