This commit is contained in:
Mateusz Gruszczyński committed 2026-10-15 10:37:23 +02:00
1 parent 2ccc598b12
commit b757b34a31
26 files changed
+671 -116

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.33.0",
"version": "0.33.1",
"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."
},
"servers": [
+19 -10
View File
@@ -25,20 +25,29 @@ fn default_admin_table_density() -> String {
"normal".to_owned()
}
fn default_admin_dashboard_density() -> String {
"compact".to_owned()
DASHBOARD_DEFAULT_PRESET.dashboard_density.clone()
}
fn default_admin_dashboard_widgets() -> Vec<String> {
DASHBOARD_WIDGETS
.iter()
.map(|widget| widget.id.clone())
.collect()
let mut order = DASHBOARD_DEFAULT_PRESET.dashboard_widgets.clone();
for widget in DASHBOARD_WIDGETS.iter() {
if !order.contains(&widget.id) {
order.push(widget.id.clone());
}
}
order
}
fn default_hidden_dashboard_widgets() -> Vec<String> {
DASHBOARD_WIDGETS
.iter()
.filter(|widget| widget.default_hidden)
.map(|widget| widget.id.clone())
.collect()
let mut hidden = DASHBOARD_DEFAULT_PRESET.dashboard_hidden_widgets.clone();
for widget in DASHBOARD_WIDGETS.iter() {
if !DASHBOARD_DEFAULT_PRESET
.dashboard_widgets
.contains(&widget.id)
&& widget.default_hidden
{
hidden.push(widget.id.clone());
}
}
hidden
}
// Preserve the user's saved order/visibility, drop identifiers unknown to this
+58
View File
@@ -87,3 +87,61 @@ static DASHBOARD_WIDGETS: std::sync::LazyLock<Vec<DashboardWidgetDefinition>> =
serde_json::from_str(include_str!("web/dashboard-widgets.json"))
.expect("invalid bundled Overview Cards registry")
});
// A single operator-curated factory preset feeds both the Rust API defaults
// and the authenticated dashboard. New registry widgets remain opt-in unless
// explicitly added to the preset; saved user preferences are never rewritten.
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct DashboardDefaultPreset {
dashboard_widgets: Vec<String>,
dashboard_hidden_widgets: Vec<String>,
dashboard_density: String,
overview_samples: u16,
}
static DASHBOARD_DEFAULT_PRESET: std::sync::LazyLock<DashboardDefaultPreset> =
std::sync::LazyLock::new(|| {
let preset: DashboardDefaultPreset =
serde_json::from_str(include_str!("web/dashboard-defaults.json"))
.expect("invalid factory Overview layout preset");
let registry = DASHBOARD_WIDGETS
.iter()
.map(|w| w.id.as_str())
.collect::<std::collections::HashSet<_>>();
let order = preset
.dashboard_widgets
.iter()
.map(String::as_str)
.collect::<std::collections::HashSet<_>>();
let hidden = preset
.dashboard_hidden_widgets
.iter()
.map(String::as_str)
.collect::<std::collections::HashSet<_>>();
assert_eq!(
order.len(),
preset.dashboard_widgets.len(),
"duplicate factory widget ID"
);
assert_eq!(
hidden.len(),
preset.dashboard_hidden_widgets.len(),
"duplicate hidden factory widget ID"
);
assert!(
order.is_subset(&registry) && hidden.is_subset(&order),
"unknown widget in factory layout"
);
assert!(
matches!(
preset.dashboard_density.as_str(),
"normal" | "compact" | "ultra"
),
"invalid factory density"
);
assert!(
matches!(preset.overview_samples, 60 | 120 | 300 | 600),
"invalid factory sample count"
);
preset
});
+59 -47
View File
@@ -1,62 +1,42 @@
#[test]
fn overview_registry_is_single_source_of_identifiers_and_visibility_defaults() {
fn overview_factory_preset_matches_registry_and_operator_visibility() {
let order = default_admin_dashboard_widgets();
let hidden = default_hidden_dashboard_widgets();
let unique: std::collections::HashSet<&str> = order.iter().map(String::as_str).collect();
assert_eq!(unique.len(), order.len());
assert_eq!(order.len(), DASHBOARD_WIDGETS.len());
assert_eq!(order.first().map(String::as_str), Some("requests"));
assert!(order.contains(&"http3".to_owned()));
assert!(hidden.contains(&"stream_overview".to_owned()));
assert!(hidden.contains(&"stream_backend_health".to_owned()));
assert_eq!(order, DASHBOARD_DEFAULT_PRESET.dashboard_widgets);
assert_eq!(hidden, DASHBOARD_DEFAULT_PRESET.dashboard_hidden_widgets);
assert_eq!(default_admin_dashboard_density(), "compact");
assert_eq!(DASHBOARD_DEFAULT_PRESET.overview_samples, 600);
for id in [
"backend_health",
"backend_activity",
"backend_errors",
"backend_latency",
"backend_traffic",
"backend_protocols",
"backend_requests",
"backend_capacity",
"backend_http5xx",
"backend_timeouts",
"backend_network",
"backend_retries",
"backend_queue",
"backend_rotations",
] {
assert!(order.contains(&id.to_owned()));
assert!(hidden.contains(&id.to_owned()));
}
for id in [
"extra_process_cpu",
"extra_process_rss",
"extra_process_fds",
"extra_open_connections",
"extra_websocket_active",
"extra_tls_error_rate",
"extra_request_error_rate",
"extra_cache_hit_rate",
"extra_cache_memory",
"extra_h2_share",
"backend_latency",
"extra_response_latency",
"extra_backend_unhealthy",
] {
assert!(order.contains(&id.to_owned()));
assert!(hidden.contains(&id.to_owned()));
}
for id in [
"traffic_chart",
"system_chart",
"connection_chart",
"cache_diagnostics",
"process_diagnostics",
"network_diagnostics",
"wide_transform_activity",
"wide_compression_activity",
"cache",
"backend_table",
] {
assert!(order.contains(&id.to_owned()));
assert!(!hidden.contains(&id.to_owned()));
assert!(order.contains(&id.to_owned()), "not registered: {id}");
assert!(
!hidden.contains(&id.to_owned()),
"should start visible: {id}"
);
}
for id in [
"bandwidth",
"backend_health",
"extra_process_cpu",
"stream_traffic",
"wide_cache_activity",
"www_status",
"www_requests",
"www_top_services",
] {
assert!(hidden.contains(&id.to_owned()), "should start hidden: {id}");
}
assert!(!hidden.contains(&"requests".to_owned()));
assert!(hidden.iter().all(|id| order.contains(id)));
assert!(validate_admin_preferences(&AdminPreferences::default()).is_ok());
}
@@ -180,6 +160,38 @@ fn old_widget_preferences_preserve_order_opt_in_and_survive_unknown_ids() {
assert!(validate_admin_preferences(&prefs).is_ok());
}
#[test]
fn saved_dashboard_order_and_visibility_survive_new_factory_preset() {
let mut prefs: AdminPreferences = serde_json::from_str(
r#"{
"dashboard_density":"normal",
"dashboard_widgets":["errors","requests","backend_traffic"],
"dashboard_hidden_widgets":["backend_traffic"]
}"#,
)
.unwrap();
reconcile_dashboard_widgets(&mut prefs);
assert_eq!(prefs.dashboard_density, "normal");
assert_eq!(
&prefs.dashboard_widgets[..3],
&["errors", "requests", "backend_traffic"]
);
assert!(!prefs
.dashboard_hidden_widgets
.contains(&"errors".to_owned()));
assert!(!prefs
.dashboard_hidden_widgets
.contains(&"requests".to_owned()));
assert!(prefs
.dashboard_hidden_widgets
.contains(&"backend_traffic".to_owned()));
// WWW was absent from the saved document and is introduced only as opt-in.
assert!(prefs
.dashboard_hidden_widgets
.contains(&"www_status".to_owned()));
assert!(validate_admin_preferences(&prefs).is_ok());
}
#[test]
fn explicit_visible_stream_opt_in_is_not_reset_when_normalizing_saved_order() {
let mut prefs = AdminPreferences::default();
+3
View File
@@ -54,6 +54,9 @@ fn admin_app_javascript() -> &'static str {
static ASSET: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
[
"\"use strict\";",
"const ADMIN_DASHBOARD_DEFAULTS = Object.freeze(",
include_str!("web/dashboard-defaults.json"),
");",
// The samples controller is needed by top-level listeners in core.js.
// Define it first; DOM initialization and translations happen later.
include_str!("web/app/overview-samples.js"),
+3 -4
View File
@@ -18,7 +18,8 @@ window.rustProxyBackendDashboard = (() => {
}
function syncVisibility(hiddenOverride = null) {
const prefs = window.rustProxyPreferences?.current?.();
const hidden = new Set(hiddenOverride ?? prefs?.dashboard_hidden_widgets ?? ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets);
const hidden = new Set(hiddenOverride ?? window.rustProxyInlineLayout?.currentDraft?.()?.hidden
?? prefs?.dashboard_hidden_widgets ?? ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets);
for (const id of ids) if (card(id)) card(id).hidden = hidden.has(id);
if (![...charts].some(visible)) reset();
}
@@ -48,8 +49,7 @@ window.rustProxyBackendDashboard = (() => {
window.rustProxyExtraOverview?.meter?.("backend-dashboard-capacity", null);
reset();
}
// Counter deltas are matched to stable (pool, member) IDs, not aggregate
// totals: removed/rebuilt pools must never become negative or spike rates.
function memberCounters(members) {
const values = new Map();
for (const { pool, member } of members) {
@@ -71,7 +71,6 @@ window.rustProxyBackendDashboard = (() => {
for (const [id, next] of current) {
const old = earlier.get(id);
if (!old) continue;
// A generation reset on a member invalidates its entire interval.
if (tracked.some(field => next[field] < old[field])) continue;
for (const field of tracked) total[field] += next[field] - old[field];
}
+6 -2
View File
@@ -145,7 +145,7 @@ window.rustProxyInlineLayout = (() => {
}
async function save() {
if (!draft || saving) return;
if (!dirty()) { finish(true); return; }
if (!dirty()) { finish(false); return; }
saving = true;
saveButton.disabled = cancelButton.disabled = unlock.disabled = true;
try {
@@ -196,5 +196,9 @@ window.rustProxyInlineLayout = (() => {
unlock?.addEventListener("click", () => { if (draft) void save(); else void begin(); });
saveButton?.addEventListener("click", () => { void save(); });
cancelButton?.addEventListener("click", cancel);
return { isEditing: () => !!draft, cancel };
return {
isEditing: () => !!draft,
currentDraft: () => draft ? { order: [...draft.order], hidden: [...draft.hidden] } : null,
cancel,
};
})();
+15 -10
View File
@@ -1,5 +1,4 @@
// Opt-in compact metrics and bounded visualizations. All readings reuse the
// existing Overview live-status payload (no additional requests or timers).
window.rustProxyExtraOverview = (() => {
const limit = () => window.rustProxyOverviewSamples?.getLimit?.() ?? 60;
const names = [
@@ -87,13 +86,17 @@ window.rustProxyExtraOverview = (() => {
}
if (id === "extra_backend_active") {
const members = Array.isArray(status.backend_pools) ? status.backend_pools.flatMap(pool => pool.members || []) : null;
return { value: members ? members.reduce((sum, member) => sum + (positive(member.active) ?? 0), 0) : null,
unit: "count", detail: "" };
return {
value: members ? members.reduce((sum, member) => sum + (positive(member.active) ?? 0), 0) : null,
unit: "count", detail: ""
};
}
if (id === "extra_backend_unhealthy") {
return { value: Array.isArray(status.backend_pools) ? status.backend_pools.reduce((sum, pool) =>
sum + (pool.members || []).filter(member => member.status === "down").length, 0) : null,
unit: "count", detail: "" };
return {
value: Array.isArray(status.backend_pools) ? status.backend_pools.reduce((sum, pool) =>
sum + (pool.members || []).filter(member => member.status === "down").length, 0) : null,
unit: "count", detail: ""
};
}
if (id === "extra_cache_memory") return ramBudget(c.resident_bytes, c.hot_limit_bytes);
if (id === "extra_cold_queue_memory") return ramBudget(c.cold_queue_bytes, c.cold_queue_limit_bytes);
@@ -123,8 +126,10 @@ window.rustProxyExtraOverview = (() => {
const { deltas, seconds } = result;
if (unit === "rate") return { value: deltas[0] / seconds, unit, detail: "" };
const sum = id === "extra_cache_hit_rate" ? deltas[0] + deltas[1] : deltas[1];
return { value: sum > 0 ? Math.min(100, deltas[0] / sum * 100) : null, unit, detail: "",
slices: id === "extra_cache_hit_rate" ? [deltas[0], deltas[1]] : null };
return {
value: sum > 0 ? Math.min(100, deltas[0] / sum * 100) : null, unit, detail: "",
slices: id === "extra_cache_hit_rate" ? [deltas[0], deltas[1]] : null
};
}
function format(value, unit) {
if (value == null) return "-";
@@ -144,7 +149,7 @@ window.rustProxyExtraOverview = (() => {
const now = performance.now();
for (const id of names) {
const tile = node(id);
if (!tile || tile.hidden) { lastCounter.delete(id); continue; }
if (!tile) { lastCounter.delete(id); continue; }
const metric = read(id, status, now, record);
const value = metric.value;
if (lastUnits.get(id) !== metric.unit) {
+2 -1
View File
@@ -1,7 +1,8 @@
window.rustProxyOverviewSamples = (() => {
const allowed = new Set([60, 120, 300, 600]);
const storageKey = "rustproxy.overview.liveChartSamples";
let saved = 60;
const factory = typeof ADMIN_DASHBOARD_DEFAULTS === "object" ? Number(ADMIN_DASHBOARD_DEFAULTS.overview_samples) : 60;
let saved = allowed.has(factory) ? factory : 60;
try {
const stored = Number(localStorage.getItem(storageKey));
if (allowed.has(stored)) saved = stored;
+2 -1
View File
@@ -84,7 +84,8 @@ window.rustProxyWideOverview = (() => {
const now = performance.now();
for (const [id, config] of Object.entries(configurations)) {
const node = tile(id);
if (!node || node.hidden) { baselines.delete(id); continue; }
if (!node) { baselines.delete(id); continue; }
const values = config.counters ? counterRates(id, config.counters(status), now, record)
: config.values(status).map(finite);
const series = history.get(id) || config.names.map(() => []);
+16 -6
View File
@@ -4,12 +4,16 @@ const ADMIN_CHART_STYLES = new Set(["default", "soft", "technical", "high_contra
const ADMIN_DENSITIES = new Set(["normal", "compact", "ultra"]);
const ADMIN_BUTTON_STYLES = new Set(["normal", "compact"]);
const ADMIN_CORNER_STYLES = new Set(["default", "square", "subtle", "rounded", "extra"]);
const ADMIN_DASHBOARD_WIDGETS = Object.freeze(ADMIN_DASHBOARD_WIDGET_REGISTRY.map(widget => widget.id));
const ADMIN_DASHBOARD_BY_ID = new Map(ADMIN_DASHBOARD_WIDGET_REGISTRY.map(widget => [widget.id, widget]));
const ADMIN_DEFAULT_HIDDEN_WIDGETS = Object.freeze(ADMIN_DASHBOARD_WIDGET_REGISTRY.filter(widget => widget.default_hidden).map(widget => widget.id));
const ADMIN_PRESET_ORDER = Object.freeze(ADMIN_DASHBOARD_DEFAULTS.dashboard_widgets.filter(id => ADMIN_DASHBOARD_BY_ID.has(id)));
const ADMIN_DASHBOARD_WIDGETS = Object.freeze([...new Set([...ADMIN_PRESET_ORDER, ...ADMIN_DASHBOARD_WIDGET_REGISTRY.map(widget => widget.id)])]);
const ADMIN_DEFAULT_HIDDEN_WIDGETS = Object.freeze([...new Set([
...ADMIN_DASHBOARD_DEFAULTS.dashboard_hidden_widgets.filter(id => ADMIN_DASHBOARD_BY_ID.has(id)),
...ADMIN_DASHBOARD_WIDGET_REGISTRY.filter(widget => widget.default_hidden && !ADMIN_PRESET_ORDER.includes(widget.id)).map(widget => widget.id),
])]);
const ADMIN_APPEARANCE_DEFAULTS = Object.freeze({
theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", chart_style: "default",
button_style: "normal", corner_style: "default", table_density: "normal", dashboard_density: "compact", dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_DEFAULT_HIDDEN_WIDGETS],
button_style: "normal", corner_style: "default", table_density: "normal", dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_DEFAULT_HIDDEN_WIDGETS],
});
const CHART_STYLE_KEYS = Object.freeze({ default: "chartStyleDefault", soft: "chartStyleSoft", technical: "chartStyleTechnical", high_contrast: "chartStyleHighContrast", neon: "chartStyleNeon", monochrome: "chartStyleMonochrome", warm: "chartStyleWarm", cool: "chartStyleCool" });
let adminPreferences = null, adminPreferencesLoading = null, adminPreferencesWrite = Promise.resolve(), adminPreferencesUnavailable = false;
@@ -215,13 +219,19 @@ function formatDateTime(value) { const date = value instanceof Date ? value : ne
function formatTime(value) { const date = value instanceof Date ? value : new Date(value); return date.toLocaleTimeString(locale(), { hour12: (adminPreferences?.time_format || "24h") === "12h" }); }
function applyAdminPreferences(value, cache = true) {
if (!value) return value;
const dashboard = applyDashboardLayout(value.dashboard_widgets, normalizeHiddenDashboardWidgets(value.dashboard_hidden_widgets, value.dashboard_widgets));
// A live inline edit is a draft, not a saved preference. Background updates
// (including an unrelated preference save) must not repaint the old layout.
const editing = window.rustProxyInlineLayout?.currentDraft?.();
const savedOrder = normalizeDashboardWidgets(value.dashboard_widgets);
const savedHidden = normalizeHiddenDashboardWidgets(value.dashboard_hidden_widgets, value.dashboard_widgets);
if (editing) applyDashboardLayout(editing.order, editing.hidden);
else applyDashboardLayout(savedOrder, savedHidden);
const normalized = {
...value,
refresh: normalizeRefreshPreferences(value.refresh),
theme: applyAdminTheme(value.theme, cache), language: applyAdminLanguage(value.language, cache, false), ui_scale: applyAdminUiScale(value.ui_scale, cache),
time_format: applyAdminTimeFormat(value.time_format), chart_style: applyAdminChartStyle(value.chart_style), button_style: applyAdminButtonStyle(value.button_style), corner_style: applyAdminCornerStyle(value.corner_style, cache), table_density: applyAdminTableDensity(value.table_density),
dashboard_density: applyAdminDashboardDensity(value.dashboard_density), dashboard_widgets: dashboard.order, dashboard_hidden_widgets: dashboard.hidden,
dashboard_density: applyAdminDashboardDensity(value.dashboard_density), dashboard_widgets: savedOrder, dashboard_hidden_widgets: savedHidden,
};
adminPreferences = normalized;
window.rustProxyRefresh?.update(normalized.refresh);
@@ -376,7 +386,7 @@ async function resetAppearancePatch(patch, control, messageKey = "appearanceSect
document.querySelectorAll("[data-appearance-reset]").forEach(control => control.addEventListener("click", () => {
const patches = {
interface: { theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", button_style: "normal", corner_style: "default" }, charts: { chart_style: "default" }, tables: { table_density: "normal" },
dashboard: { dashboard_density: "compact", dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets] },
dashboard: { dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets] },
};
void resetAppearancePatch(patches[control.dataset.appearanceReset] || {}, control);
}));
+2 -1
View File
@@ -16,7 +16,8 @@ window.rustProxyStreamDashboard = (() => {
}
function syncVisibility(hiddenOverride = null) {
const prefs = window.rustProxyPreferences?.current?.();
const hidden = new Set(hiddenOverride ?? prefs?.dashboard_hidden_widgets ?? ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets);
const hidden = new Set(hiddenOverride ?? window.rustProxyInlineLayout?.currentDraft?.()?.hidden
?? prefs?.dashboard_hidden_widgets ?? ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets);
for (const id of ids) if (card(id)) card(id).hidden = hidden.has(id);
if (card("stream_traffic")?.hidden && wasVisible) clearTraffic();
wasVisible = !card("stream_traffic")?.hidden;
+3 -3
View File
@@ -88,7 +88,7 @@ window.rustProxyWebOverview = (() => {
}
function smallPath(values) {
const peak = Math.max(1, ...values.filter(v => v != null));
return values.map((value, i) => `${i * 300 / Math.max(1, values.length - 1)},${55 - (value || 0) / peak * 50}`).join(" ");
return values.filter(value => value !== null).map((value, i, present) => `${i * 300 / Math.max(1, present.length - 1)},${55 - value / peak * 50}`).join(" ");
}
function ring(id, amounts) {
const total = amounts.reduce((n, value) => n + value, 0);
@@ -154,7 +154,7 @@ window.rustProxyWebOverview = (() => {
meter("www_error_rate", r?.errorRate);
show("www_error_rate-detail", t("webMetricErrorRateHelp"));
show("www_php_requests-value", fmtRate(r?.phpRps));
if (record && !tile("www_php_requests")?.hidden) {
if (record) {
const values = histories.get("www_php_requests") || [];
values.push(r?.phpRps ?? null); window.rustProxyOverviewSamples.trim(values);
histories.set("www_php_requests", values);
@@ -166,7 +166,7 @@ window.rustProxyWebOverview = (() => {
};
for (const [id, names] of Object.entries(seriesNames)) {
const series = histories.get(id) || names.map(() => []);
if (record && !tile(id)?.hidden) {
if (record) {
names.forEach((_, i) => { series[i].push(set[id]?.[i] ?? null); window.rustProxyOverviewSamples.trim(series[i]); });
histories.set(id, series);
}
+137
View File
@@ -0,0 +1,137 @@
{
"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"
],
"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"
],
"dashboard_density": "compact",
"overview_samples": 600
}