This commit is contained in:
Mateusz Gruszczyński committed 2026-10-15 13:00:44 +02:00
1 parent 5a36bb0c4b
commit 78ad28ef4d
27 files changed
+1448 -234

No files matched your search

+47 -5
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.33.2",
"version": "0.33.3",
"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": [
@@ -3413,7 +3413,7 @@
"dashboard_widgets": {
"type": "array",
"minItems": 1,
"maxItems": 62,
"maxItems": 78,
"uniqueItems": true,
"items": {
"type": "string",
@@ -3479,7 +3479,23 @@
"cache_diagnostics",
"process_diagnostics",
"network_diagnostics",
"backend_table"
"backend_table",
"extra_cold_queue_memory",
"extra_cold_storage",
"wide_transform_activity",
"wide_compression_activity",
"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"
]
},
"default": [
@@ -3548,9 +3564,19 @@
],
"description": "Account-scoped order for configurable Overview telemetry cards."
},
"dashboard_widget_sizes": {
"type": "object",
"nullable": true,
"maxProperties": 78,
"additionalProperties": {
"type": "integer",
"enum": [2, 3, 4, 6, 12]
},
"description": "Nullable account-scoped per-card widths (12-column grid). Null preserves the legacy grouped visual arrangement; an object, including an empty one, enables freely ordered cards. Key must be a registered Overview widget ID. Panels require width >= 4, charts >= 3 and backend table >= 6."
},
"dashboard_hidden_widgets": {
"type": "array",
"maxItems": 62,
"maxItems": 78,
"uniqueItems": true,
"items": {
"type": "string",
@@ -3616,7 +3642,23 @@
"cache_diagnostics",
"process_diagnostics",
"network_diagnostics",
"backend_table"
"backend_table",
"extra_cold_queue_memory",
"extra_cold_storage",
"wide_transform_activity",
"wide_compression_activity",
"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"
]
},
"default": [
+18 -23
View File
@@ -27,29 +27,6 @@ fn default_admin_table_density() -> String {
fn default_admin_dashboard_density() -> String {
DASHBOARD_DEFAULT_PRESET.dashboard_density.clone()
}
fn default_admin_dashboard_widgets() -> Vec<String> {
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> {
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
// binary, and append new registry widgets according to their opt-in policy.
// Stored preferences from older/newer releases are normalized on read.
@@ -62,6 +39,13 @@ fn reconcile_dashboard_widgets(prefs: &mut AdminPreferences) {
prefs.dashboard_hidden_widgets.retain(|id| {
DASHBOARD_WIDGETS.iter().any(|widget| widget.id == *id) && seen_hidden.insert(id.clone())
});
if let Some(sizes) = prefs.dashboard_widget_sizes.as_mut() {
sizes.retain(|id, span| {
DASHBOARD_WIDGETS
.iter()
.any(|widget| widget.id == *id && valid_dashboard_span(&widget.display_type, *span))
});
}
for widget in DASHBOARD_WIDGETS.iter() {
if !seen.contains(&widget.id) {
prefs.dashboard_widgets.push(widget.id.clone());
@@ -147,6 +131,10 @@ struct AdminPreferences {
dashboard_widgets: Vec<String>,
#[serde(default = "default_hidden_dashboard_widgets")]
dashboard_hidden_widgets: Vec<String>,
// Null/absent: retain legacy visual grouping for existing installations.
// A map (including {}) means that order is fully user-controlled.
#[serde(default)]
dashboard_widget_sizes: Option<std::collections::BTreeMap<String, u8>>,
#[serde(default = "default_file_log_columns")]
file_log_columns: Vec<String>,
#[serde(default = "default_file_log_sort")]
@@ -200,6 +188,7 @@ impl Default for AdminPreferences {
dashboard_density: default_admin_dashboard_density(),
dashboard_widgets: default_admin_dashboard_widgets(),
dashboard_hidden_widgets: default_hidden_dashboard_widgets(),
dashboard_widget_sizes: None,
file_log_columns: default_file_log_columns(),
file_log_sort: default_file_log_sort(),
file_log_descending: default_file_log_descending(),
@@ -385,6 +374,12 @@ fn validate_admin_preferences(preferences: &AdminPreferences) -> Result<(), ApiE
));
}
}
if !valid_dashboard_sizes(&preferences.dashboard_widget_sizes) {
return Err(ApiError(
StatusCode::UNPROCESSABLE_ENTITY,
"invalid dashboard card width preference".into(),
));
}
if preferences.backend_table_columns.is_empty()
|| preferences.backend_table_columns.len() > BACKEND_TABLE_COLUMNS.len()
{
+47
View File
@@ -81,6 +81,7 @@ const STREAM_LIST_SORT_COLUMNS: &[&str] = &[
struct DashboardWidgetDefinition {
id: String,
default_hidden: bool,
display_type: String,
}
static DASHBOARD_WIDGETS: std::sync::LazyLock<Vec<DashboardWidgetDefinition>> =
std::sync::LazyLock::new(|| {
@@ -145,3 +146,49 @@ static DASHBOARD_DEFAULT_PRESET: std::sync::LazyLock<DashboardDefaultPreset> =
);
preset
});
// None denotes the legacy grouped layout. Some({}) switches to freely
// ordered 12-column CSS Grid while retaining every historical card width.
fn valid_dashboard_span(display_type: &str, span: u8) -> bool {
match display_type {
"table" => matches!(span, 6 | 12),
"panel" => matches!(span, 4 | 6 | 12),
"chart" => matches!(span, 3 | 4 | 6 | 12),
_ => matches!(span, 2 | 3 | 4 | 6 | 12),
}
}
fn valid_dashboard_sizes(sizes: &Option<std::collections::BTreeMap<String, u8>>) -> bool {
sizes.as_ref().is_none_or(|sizes| {
sizes.len() <= DASHBOARD_WIDGETS.len()
&& sizes.iter().all(|(id, span)| {
DASHBOARD_WIDGETS.iter().any(|widget| {
widget.id == *id && valid_dashboard_span(&widget.display_type, *span)
})
})
})
}
fn default_hidden_dashboard_widgets() -> Vec<String> {
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
}
fn default_admin_dashboard_widgets() -> Vec<String> {
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
}
+53
View File
@@ -202,3 +202,56 @@ fn explicit_visible_stream_opt_in_is_not_reset_when_normalizing_saved_order() {
assert_eq!(prefs.dashboard_widgets, default_admin_dashboard_widgets());
assert!(validate_admin_preferences(&prefs).is_ok());
}
#[test]
fn dashboard_width_preferences_preserve_legacy_and_allow_custom_grids() {
let old: AdminPreferences = serde_json::from_str(r#"{"theme":"dark"}"#).unwrap();
assert!(old.dashboard_widget_sizes.is_none());
let mut prefs = AdminPreferences::default();
prefs.dashboard_widget_sizes = Some(std::collections::BTreeMap::new());
assert!(validate_admin_preferences(&prefs).is_ok());
for (id, span) in [
("http_protocol", 4),
("http_protocol", 3),
("requests", 4),
("www_requests", 3),
("backend_table", 6),
] {
prefs
.dashboard_widget_sizes
.as_mut()
.unwrap()
.insert(id.into(), span);
}
assert!(validate_admin_preferences(&prefs).is_ok());
let saved = serde_json::to_string(&prefs).unwrap();
let restored: AdminPreferences = serde_json::from_str(&saved).unwrap();
assert_eq!(
restored.dashboard_widget_sizes,
prefs.dashboard_widget_sizes
);
prefs
.dashboard_widget_sizes
.as_mut()
.unwrap()
.insert("backend_table".into(), 2);
assert!(validate_admin_preferences(&prefs).is_err());
prefs
.dashboard_widget_sizes
.as_mut()
.unwrap()
.insert("backend_table".into(), 12);
prefs
.dashboard_widget_sizes
.as_mut()
.unwrap()
.insert("not_a_card".into(), 3);
assert!(validate_admin_preferences(&prefs).is_err());
reconcile_dashboard_widgets(&mut prefs);
assert!(!prefs
.dashboard_widget_sizes
.as_ref()
.unwrap()
.contains_key("not_a_card"));
assert!(validate_admin_preferences(&prefs).is_ok());
}
+55 -18
View File
@@ -15,14 +15,20 @@ window.rustProxyInlineLayout = (() => {
let opening = false;
let saving = false;
const groupFor = id => window.rustProxyMasonry?.groupFor?.(id) ?? 0;
const visibleIds = () => draft.order.filter(id => !draft.hidden.has(id))
.sort((a, b) => groupFor(a) - groupFor(b));
const visibleIds = () => dashboardVisualOrder(draft.order, draft.sizes)
.filter(id => !draft.hidden.has(id));
function enterFreeLayout() {
if (draft.sizes === null) {
draft.order = dashboardVisualOrder(draft.order, null);
draft.sizes = {};
}
}
const nodes = () => [...grid.children].filter(node => node.dataset.dashboardWidget);
const nodeFor = id => nodes().find(node => node.dataset.dashboardWidget === id);
const label = id => dashboardWidgetLabel(id);
const dirty = () => draft && original && (draft.order.some((id, i) => id !== original.order[i])
|| [...draft.hidden].some(id => !original.hidden.has(id)) || [...original.hidden].some(id => !draft.hidden.has(id)));
|| [...draft.hidden].some(id => !original.hidden.has(id)) || [...original.hidden].some(id => !draft.hidden.has(id))
|| JSON.stringify(draft.sizes) !== JSON.stringify(original.sizes));
function renderHidden() {
if (!hiddenList || !draft) return;
@@ -46,16 +52,24 @@ window.rustProxyInlineLayout = (() => {
if (hiddenShelf) { hiddenShelf.open = true; hiddenShelf.querySelector("summary")?.focus(); }
}
function paint() {
applyDashboardLayout(draft.order, [...draft.hidden]);
applyDashboardLayout(draft.order, [...draft.hidden], draft.sizes);
renderHidden();
const visible = visibleIds();
for (const node of nodes()) {
const i = visible.indexOf(node.dataset.dashboardWidget);
const up = node.querySelector(".overview-inline-up");
const down = node.querySelector(".overview-inline-down");
if (up) up.disabled = i <= 0 || groupFor(visible[i - 1]) !== groupFor(node.dataset.dashboardWidget);
if (down) down.disabled = i < 0 || i === visible.length - 1 ||
groupFor(visible[i + 1]) !== groupFor(node.dataset.dashboardWidget);
if (up) up.disabled = i <= 0;
if (down) down.disabled = i < 0 || i === visible.length - 1;
const widths = dashboardAllowedWidths(node.dataset.dashboardWidget);
const span = dashboardWidgetWidth(node.dataset.dashboardWidget, draft.sizes);
const at = widths.indexOf(span);
const value = node.querySelector(".overview-inline-width-value");
if (value) value.textContent = `${span}/12`;
const narrow = node.querySelector(".overview-inline-narrow");
const widen = node.querySelector(".overview-inline-widen");
if (narrow) narrow.disabled = at <= 0;
if (widen) widen.disabled = at === widths.length - 1;
}
}
function clearDrag() {
@@ -63,19 +77,31 @@ window.rustProxyInlineLayout = (() => {
for (const node of nodes()) node.classList.remove("is-overview-dragging", "is-overview-drop-before", "is-overview-drop-after");
}
function move(id, target, after = false) {
if (!draft || id === target || groupFor(id) !== groupFor(target)) return;
if (!draft || id === target) return;
if (!visibleIds().includes(id) || !visibleIds().includes(target)) return;
enterFreeLayout();
const visible = visibleIds();
if (!visible.includes(id) || !visible.includes(target)) return;
visible.splice(visible.indexOf(id), 1);
visible.splice(visible.indexOf(target) + (after ? 1 : 0), 0, id);
const iter = visible.values();
draft.order = draft.order.map(item => draft.hidden.has(item) ? item : iter.next().value);
paint();
}
function resize(id, delta) {
if (!draft) return;
const widths = dashboardAllowedWidths(id);
const next = widths[widths.indexOf(dashboardWidgetWidth(id, draft.sizes)) + delta];
if (next === undefined) return;
enterFreeLayout();
if (next === dashboardDefaultWidth(id)) delete draft.sizes[id];
else draft.sizes[id] = next;
paint();
nodeFor(id)?.querySelector(delta < 0 ? ".overview-inline-narrow" : ".overview-inline-widen")?.focus();
}
function nudge(id, delta) {
const visible = visibleIds();
const index = visible.indexOf(id), target = visible[index + delta];
if (!target || groupFor(id) !== groupFor(target)) return;
if (!target) return;
move(id, target, delta > 0);
nodeFor(id)?.querySelector(delta < 0 ? ".overview-inline-up" : ".overview-inline-down")?.focus();
}
@@ -94,7 +120,14 @@ window.rustProxyInlineLayout = (() => {
const grip = control("⠿", `${t("overviewLayoutInlineDrag")}: ${title}`, "overview-inline-grip", () => { });
grip.draggable = true;
grip.dataset.overviewDragHandle = id;
bar.append(grip, dashboardWidgetTypeBadge(id),
const sizeControls = el("span", undefined, "overview-inline-width");
const widthValue = el("span", "", "overview-inline-width-value");
widthValue.setAttribute("aria-label", t("overviewLayoutWidth"));
sizeControls.append(
control("−", `${t("overviewLayoutNarrow")}: ${title}`, "overview-inline-narrow", () => resize(id, -1)),
widthValue,
control("+", `${t("overviewLayoutWiden")}: ${title}`, "overview-inline-widen", () => resize(id, 1)));
bar.append(grip, dashboardWidgetTypeBadge(id), sizeControls,
control("←", `${t("moveUp")}: ${title}`, "overview-inline-up", () => nudge(id, -1)),
control("→", `${t("moveDown")}: ${title}`, "overview-inline-down", () => nudge(id, 1)),
control("×", `${t("overviewLayoutInlineHide")}: ${title}`, "overview-inline-hide", () => hide(id)));
@@ -125,7 +158,7 @@ window.rustProxyInlineLayout = (() => {
if (hiddenShelf) { hiddenShelf.open = false; hiddenShelf.hidden = true; }
if (hiddenList) hiddenList.replaceChildren();
if (modalButton) modalButton.disabled = false;
if (!preserveSaved && original) applyDashboardLayout(original.order, [...original.hidden]);
if (!preserveSaved && original) applyDashboardLayout(original.order, [...original.hidden], original.sizes);
draft = original = null;
}
async function begin() {
@@ -137,8 +170,9 @@ window.rustProxyInlineLayout = (() => {
const saved = window.rustProxyPreferences?.current?.() || ADMIN_APPEARANCE_DEFAULTS;
const order = normalizeDashboardWidgets(saved.dashboard_widgets);
const hidden = new Set(normalizeHiddenDashboardWidgets(saved.dashboard_hidden_widgets, saved.dashboard_widgets));
original = { order: [...order], hidden: new Set(hidden) };
draft = { order: [...order], hidden: new Set(hidden) };
const sizes = normalizeDashboardWidgetSizes(saved.dashboard_widget_sizes);
original = { order: [...order], hidden: new Set(hidden), sizes: sizes === null ? null : { ...sizes } };
draft = { order: [...order], hidden: new Set(hidden), sizes: sizes === null ? null : { ...sizes } };
showEditing();
} catch (error) { if (error?.status !== 401) toast(error.message, true); }
finally { opening = false; unlock.disabled = false; }
@@ -151,6 +185,7 @@ window.rustProxyInlineLayout = (() => {
try {
await window.rustProxyPreferences.update({
dashboard_widgets: [...draft.order], dashboard_hidden_widgets: [...draft.hidden],
dashboard_widget_sizes: draft.sizes === null ? null : { ...draft.sizes },
});
finish(true);
toast(t("overviewLayoutEditorSaved"));
@@ -177,8 +212,7 @@ window.rustProxyInlineLayout = (() => {
grid?.addEventListener("dragover", event => {
if (!draft || !dragging) return;
const target = dropTarget(event);
if (!target || target.dataset.dashboardWidget === dragging ||
groupFor(target.dataset.dashboardWidget) !== groupFor(dragging)) return;
if (!target || target.dataset.dashboardWidget === dragging) return;
event.preventDefault();
for (const node of nodes()) node.classList.remove("is-overview-drop-before", "is-overview-drop-after");
target.classList.add(positionAfter(target, event) ? "is-overview-drop-after" : "is-overview-drop-before");
@@ -198,7 +232,10 @@ window.rustProxyInlineLayout = (() => {
cancelButton?.addEventListener("click", cancel);
return {
isEditing: () => !!draft,
currentDraft: () => draft ? { order: [...draft.order], hidden: [...draft.hidden] } : null,
currentDraft: () => draft ? {
order: [...draft.order], hidden: [...draft.hidden],
sizes: draft.sizes === null ? null : { ...draft.sizes },
} : null,
cancel,
};
})();
+53 -13
View File
@@ -20,43 +20,60 @@ window.rustProxyDashboardLayout = (() => {
function initialDraft() {
const prefs = window.rustProxyPreferences?.current?.() || ADMIN_APPEARANCE_DEFAULTS;
const order = normalizeDashboardWidgets(prefs.dashboard_widgets);
return { order, hidden: new Set(normalizeHiddenDashboardWidgets(prefs.dashboard_hidden_widgets, prefs.dashboard_widgets)) };
return { order, hidden: new Set(normalizeHiddenDashboardWidgets(prefs.dashboard_hidden_widgets, prefs.dashboard_widgets)),
sizes: normalizeDashboardWidgetSizes(prefs.dashboard_widget_sizes) };
}
function defaultDraft() {
return {
order: [...ADMIN_DASHBOARD_WIDGETS],
hidden: new Set(ADMIN_DEFAULT_HIDDEN_WIDGETS),
sizes: null,
};
}
function label(id) {
const widget = ADMIN_DASHBOARD_BY_ID.get(id);
return widget ? t(widget.label_key) : id;
}
const groupFor = id => window.rustProxyMasonry?.groupFor?.(id) ?? 0;
function visibleIds() {
return draft.order.filter(id => !draft.hidden.has(id))
.sort((a, b) => groupFor(a) - groupFor(b));
return dashboardVisualOrder(draft.order, draft.sizes)
.filter(id => !draft.hidden.has(id));
}
function enterFreeLayout() {
if (draft.sizes === null) {
draft.order = dashboardVisualOrder(draft.order, null);
draft.sizes = {};
}
}
function reorderVisible(nextVisible) {
enterFreeLayout();
const iter = nextVisible.values();
draft.order = draft.order.map(id => draft.hidden.has(id) ? id : iter.next().value);
render();
}
function move(id, target, after = false) {
const next = visibleIds();
if (id === target || !next.includes(id) || !next.includes(target) || groupFor(id) !== groupFor(target)) return;
if (id === target || !next.includes(id) || !next.includes(target)) return;
next.splice(next.indexOf(id), 1);
next.splice(next.indexOf(target) + (after ? 1 : 0), 0, id);
reorderVisible(next);
}
function nudge(id, direction) {
const next = visibleIds(), index = next.indexOf(id), other = index + direction;
if (index < 0 || other < 0 || other >= next.length || groupFor(id) !== groupFor(next[other])) return;
if (index < 0 || other < 0 || other >= next.length) return;
[next[index], next[other]] = [next[other], next[index]];
reorderVisible(next);
visibleRoot.querySelectorAll(".overview-layout-tile").forEach(node => {
if (node.dataset.previewWidget === id) node.querySelector(".overview-layout-up")?.focus();
});
visibleRoot.querySelector(`[data-preview-widget="${id}"] .overview-layout-up`)?.focus();
}
function resize(id, delta) {
if (!draft) return;
const widths = dashboardAllowedWidths(id);
const next = widths[widths.indexOf(dashboardWidgetWidth(id, draft.sizes)) + delta];
if (next === undefined) return;
enterFreeLayout();
if (next === dashboardDefaultWidth(id)) delete draft.sizes[id];
else draft.sizes[id] = next;
render();
visibleRoot.querySelector(`[data-preview-widget="${id}"] .${delta < 0 ? "overview-layout-narrow" : "overview-layout-widen"}`)?.focus();
}
function preview(id) {
const original = document.querySelector(`#panel-overview [data-dashboard-widget="${id}"]`);
@@ -94,6 +111,7 @@ window.rustProxyDashboardLayout = (() => {
const tiles = visible.map((id, index) => {
const tile = el("div", undefined, "overview-layout-tile");
tile.dataset.previewWidget = id;
tile.dataset.dashboardSpan = String(dashboardWidgetWidth(id, draft.sizes));
if (ADMIN_DASHBOARD_BY_ID.get(id)?.category === "panels") tile.classList.add("is-panel");
tile.draggable = true;
tile.setAttribute("role", "listitem");
@@ -102,19 +120,39 @@ window.rustProxyDashboardLayout = (() => {
const grip = el("span", "⠿", "overview-layout-grip");
grip.setAttribute("aria-hidden", "true");
const up = el("button", "←", "quiet overview-layout-up");
up.type = "button"; up.disabled = index === 0 || groupFor(id) !== groupFor(visible[index - 1]);
up.type = "button"; up.disabled = index === 0;
up.setAttribute("aria-label", `${t("moveUp")}: ${label(id)}`);
up.addEventListener("click", () => nudge(id, -1));
const down = el("button", "→", "quiet overview-layout-down");
down.type = "button"; down.disabled = index === visible.length - 1 || groupFor(id) !== groupFor(visible[index + 1]);
down.type = "button"; down.disabled = index === visible.length - 1;
down.setAttribute("aria-label", `${t("moveDown")}: ${label(id)}`);
down.addEventListener("click", () => nudge(id, 1));
const hide = el("button", t("overviewLayoutEditorHide"), "quiet overview-layout-hide");
hide.type = "button";
hide.setAttribute("aria-label", `${t("overviewLayoutEditorHide")}: ${label(id)}`);
hide.addEventListener("click", () => { draft.hidden.add(id); render(); });
const widths = dashboardAllowedWidths(id);
const width = dashboardWidgetWidth(id, draft.sizes);
const at = widths.indexOf(width);
const narrow = el("button", "−", "quiet overview-layout-narrow");
narrow.type = "button"; narrow.disabled = at <= 0;
narrow.setAttribute("aria-label", `${t("overviewLayoutNarrow")}: ${label(id)}`);
narrow.addEventListener("click", () => resize(id, -1));
const widen = el("button", "+", "quiet overview-layout-widen");
widen.type = "button"; widen.disabled = at === widths.length - 1;
widen.setAttribute("aria-label", `${t("overviewLayoutWiden")}: ${label(id)}`);
widen.addEventListener("click", () => resize(id, 1));
const widthValue = el("span", `${width}/12`, "overview-layout-width-value");
widthValue.title = t("overviewLayoutWidth");
// The visual editor intentionally keeps uniformly sized previews.
// Group sizing separately from ordering/hiding so both sets wrap
// inside a narrow dialog rather than spilling into adjacent tiles.
const sizeActions = el("span", undefined, "overview-layout-size-actions");
sizeActions.append(narrow, widthValue, widen);
const orderActions = el("span", undefined, "overview-layout-order-actions");
orderActions.append(up, down, hide);
const controls = el("span", undefined, "overview-layout-controls");
controls.append(up, down, hide);
controls.append(sizeActions, orderActions);
toolbar.append(grip, dashboardWidgetTypeBadge(id), controls);
tile.append(toolbar, preview(id));
tile.addEventListener("dragstart", event => {
@@ -125,7 +163,7 @@ window.rustProxyDashboardLayout = (() => {
tile.classList.add("is-dragging");
});
tile.addEventListener("dragover", event => {
if (!dragging || dragging === id || groupFor(dragging) !== groupFor(id)) return;
if (!dragging || dragging === id) return;
event.preventDefault();
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
tile.classList.add("is-drop-target");
@@ -201,6 +239,7 @@ window.rustProxyDashboardLayout = (() => {
try {
await window.rustProxyPreferences.update({
dashboard_widgets: [...defaults.order], dashboard_hidden_widgets: [...defaults.hidden],
dashboard_widget_sizes: null,
});
resetConfirm.close();
dialog.close();
@@ -215,6 +254,7 @@ window.rustProxyDashboardLayout = (() => {
try {
await window.rustProxyPreferences.update({
dashboard_widgets: [...draft.order], dashboard_hidden_widgets: [...draft.hidden],
dashboard_widget_sizes: draft.sizes === null ? null : { ...draft.sizes },
});
dialog.close();
toast(t("overviewLayoutEditorSaved"));
+7 -13
View File
@@ -87,17 +87,13 @@ 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);
@@ -127,10 +123,8 @@ 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 "-";
+6 -3
View File
@@ -9,16 +9,19 @@ window.rustProxyMasonry = (() => {
function refresh() {
const previous = widgets();
const ordered = previous.map((node, index) => ({ node, index }))
// Existing layouts (without a width map) retain their historical rows.
// Once a user moves/resizes a block, DOM order is the actual order.
const legacy = grid.dataset.overviewLayoutMode !== "custom";
const ordered = legacy ? previous.map((node, index) => ({ node, index }))
.sort((a, b) => group(a.node) - group(b.node) || a.index - b.index)
.map(item => item.node);
.map(item => item.node) : previous;
if (ordered.some((node, i) => node !== previous[i])) grid.append(...ordered);
let lastGroup = -1;
for (const node of ordered) {
for (const key of ["position", "left", "top", "width", "minHeight", "gridColumnStart",
"gridColumnEnd", "gridRowStart", "gridRowEnd", "alignSelf"]) node.style[key] = "";
node.classList.remove("overview-row-start");
if (node.hidden) continue;
if (node.hidden || !legacy) continue;
const current = group(node);
if (current !== lastGroup) node.classList.add("overview-row-start");
lastGroup = current;
+71 -18
View File
@@ -13,7 +13,7 @@ const ADMIN_DEFAULT_HIDDEN_WIDGETS = Object.freeze([...new Set([
])]);
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: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, 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], dashboard_widget_sizes: null,
});
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;
@@ -41,6 +41,47 @@ function normalizeHiddenDashboardWidgets(value, savedOrder = ADMIN_DASHBOARD_WID
}
return hidden;
}
// An absent map preserves the previous grouped visual order after upgrading.
// An explicit object (even empty) opts into free ordering and card resizing.
const DASHBOARD_WIDTHS = Object.freeze([2, 3, 4, 6, 12]);
function dashboardAllowedWidths(id) {
switch (dashboardWidgetType(id)) {
case "table": return [6, 12];
case "panel": return [4, 6, 12];
case "chart": return [3, 4, 6, 12];
default: return DASHBOARD_WIDTHS;
}
}
function dashboardDefaultWidth(id) {
const node = document.querySelector(`#dashboard-overview-grid [data-dashboard-widget="${id}"]`);
if (!node) return 2;
if (node.classList.contains("pool-card")) return 12;
if (node.classList.contains("overview-wide-chart")) return 6;
if (node.classList.contains("stat")) return 2;
return 4;
}
function normalizeDashboardWidgetSizes(value) {
if (value === null || value === undefined) return null;
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const result = {};
for (const [id, span] of Object.entries(value)) {
if (ADMIN_DASHBOARD_BY_ID.has(id) && dashboardAllowedWidths(id).includes(span)
&& span !== dashboardDefaultWidth(id)) result[id] = span;
}
return result;
}
function dashboardWidgetWidth(id, sizes) {
return sizes?.[id] ?? dashboardDefaultWidth(id);
}
function dashboardVisualOrder(order, sizes = null) {
const normalized = normalizeDashboardWidgets(order);
if (sizes !== null) return normalized;
// Preserve the old grouped appearance until the first intentional edit.
return normalized.map((id, index) => ({ id, index }))
.sort((a, b) => (window.rustProxyMasonry?.groupFor?.(a.id) ?? 0) -
(window.rustProxyMasonry?.groupFor?.(b.id) ?? 0) || a.index - b.index)
.map(item => item.id);
}
function browserLanguage() {
const packs = [...languagePacks.values()], candidates = (navigator.languages || [navigator.language]).filter(Boolean).map(value => value.replaceAll("_", "-").toLowerCase());
return candidates.map(value => packs.find(pack => pack.meta.locale.toLowerCase() === value || pack.id.replaceAll("_", "-").toLowerCase() === value) || packs.find(pack => pack.meta.locale.toLowerCase().split("-")[0] === value.split("-")[0])).find(Boolean)?.id || defaultLanguage;
@@ -96,19 +137,25 @@ function applyAdminDashboardDensity(value) {
const density = normalizeAdminDensity(value, "compact"); document.documentElement.dataset.dashboardDensity = density;
const select = $("ui-dashboard-density"); if (select) select.value = density; return density;
}
function applyDashboardLayout(order, hidden) {
function applyDashboardLayout(order, hidden, sizes = null) {
const normalizedOrder = normalizeDashboardWidgets(order), hiddenSet = new Set(normalizeHiddenDashboardWidgets(hidden));
const grid = document.querySelector("#panel-overview .stats-grid"); if (!grid) return { order: normalizedOrder, hidden: [...hiddenSet] };
const widths = normalizeDashboardWidgetSizes(sizes);
const grid = document.querySelector("#panel-overview .stats-grid");
if (!grid) return { order: normalizedOrder, hidden: [...hiddenSet], sizes: widths };
grid.dataset.overviewLayoutMode = widths === null ? "legacy" : "custom";
const nodes = new Map([...grid.querySelectorAll("[data-dashboard-widget]")].map(node => [node.dataset.dashboardWidget, node]));
const orderedNodes = normalizedOrder.map(id => nodes.get(id)).filter(Boolean);
for (const node of orderedNodes) node.hidden = hiddenSet.has(node.dataset.dashboardWidget);
for (const node of orderedNodes) {
node.hidden = hiddenSet.has(node.dataset.dashboardWidget);
node.dataset.dashboardSpan = String(dashboardWidgetWidth(node.dataset.dashboardWidget, widths));
}
const currentNodes = [...grid.querySelectorAll("[data-dashboard-widget]")];
if (orderedNodes.some((node, index) => node !== currentNodes[index])) grid.append(...orderedNodes);
window.rustProxyStreamDashboard?.syncVisibility?.([...hiddenSet]);
window.rustProxyBackendDashboard?.syncVisibility?.([...hiddenSet]);
window.rustProxyMasonry?.refresh?.();
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") { window.rustProxyStreamDashboard?.render?.(latestStatus, [...hiddenSet], false); window.rustProxyBackendDashboard?.render?.(latestStatus, [...hiddenSet], false); window.rustProxyExtraOverview?.render?.(latestStatus, false); window.rustProxyWideOverview?.render?.(latestStatus, false); window.rustProxyWebOverview?.render?.(latestStatus, false); }
return { order: normalizedOrder, hidden: [...hiddenSet] };
return { order: normalizedOrder, hidden: [...hiddenSet], sizes: widths };
}
function dashboardWidgetLabel(id) {
const widget = ADMIN_DASHBOARD_BY_ID.get(id);
@@ -135,7 +182,7 @@ function renderDashboardWidgetSettings() {
const focusedRow = document.activeElement?.closest?.("[data-dashboard-widget-setting]");
const focusedId = focusedRow && list.contains?.(focusedRow) ? focusedRow.dataset.dashboardWidgetSetting : null;
const focusedAction = focusedId ? document.activeElement?.dataset.dashboardWidgetAction : null;
const current = adminPreferences || ADMIN_APPEARANCE_DEFAULTS, order = normalizeDashboardWidgets(current.dashboard_widgets), hidden = new Set(normalizeHiddenDashboardWidgets(current.dashboard_hidden_widgets));
const current = adminPreferences || ADMIN_APPEARANCE_DEFAULTS, order = dashboardVisualOrder(current.dashboard_widgets, current.dashboard_widget_sizes), hidden = new Set(normalizeHiddenDashboardWidgets(current.dashboard_hidden_widgets));
const rows = order.map((id, index) => {
const row = el("div", undefined, "dashboard-widget-setting"); row.dataset.dashboardWidgetSetting = id;
const visible = !hidden.has(id);
@@ -152,14 +199,19 @@ function renderDashboardWidgetSettings() {
const down = el("button", "→", "quiet"); down.type = "button"; down.dataset.dashboardWidgetAction = "down"; down.disabled = index === order.length - 1; down.setAttribute("aria-label", t("dashboardWidgetsMoveLater"));
toggle.addEventListener("click", () => {
const previous = [...hidden], next = new Set(hidden); if (visible) next.add(id); else next.delete(id);
if (adminPreferences) adminPreferences.dashboard_hidden_widgets = [...next]; applyDashboardLayout(order, [...next]); renderDashboardWidgetSettings();
void saveAppearancePreference({ dashboard_hidden_widgets: [...next] }, toggle, () => { if (adminPreferences) adminPreferences.dashboard_hidden_widgets = previous; applyDashboardLayout(order, previous); renderDashboardWidgetSettings(); });
if (adminPreferences) adminPreferences.dashboard_hidden_widgets = [...next]; applyDashboardLayout(order, [...next], current.dashboard_widget_sizes); renderDashboardWidgetSettings();
void saveAppearancePreference({ dashboard_hidden_widgets: [...next] }, toggle, () => { if (adminPreferences) adminPreferences.dashboard_hidden_widgets = previous; applyDashboardLayout(order, previous, current.dashboard_widget_sizes); renderDashboardWidgetSettings(); });
});
const move = delta => {
const previous = [...order], next = [...order], at = next.indexOf(id), target = at + delta; if (at < 0 || target < 0 || target >= next.length) return;
[next[at], next[target]] = [next[target], next[at]]; if (adminPreferences) adminPreferences.dashboard_widgets = next;
applyDashboardLayout(next, [...hidden]); renderDashboardWidgetSettings();
void saveAppearancePreference({ dashboard_widgets: next }, null, () => { if (adminPreferences) adminPreferences.dashboard_widgets = previous; applyDashboardLayout(previous, [...hidden]); renderDashboardWidgetSettings(); });
[next[at], next[target]] = [next[target], next[at]];
const previousSizes = current.dashboard_widget_sizes;
if (adminPreferences) { adminPreferences.dashboard_widgets = next; adminPreferences.dashboard_widget_sizes = normalizeDashboardWidgetSizes(previousSizes) ?? {}; }
applyDashboardLayout(next, [...hidden], current.dashboard_widget_sizes); renderDashboardWidgetSettings();
void saveAppearancePreference({ dashboard_widgets: next, dashboard_widget_sizes: current.dashboard_widget_sizes }, null, () => {
if (adminPreferences) { adminPreferences.dashboard_widgets = previous; adminPreferences.dashboard_widget_sizes = previousSizes; }
applyDashboardLayout(previous, [...hidden], previousSizes); renderDashboardWidgetSettings();
});
};
up.addEventListener("click", () => move(-1)); down.addEventListener("click", () => move(1)); actions.append(up, down); row.append(toggle, actions); return row;
});
@@ -224,14 +276,15 @@ function applyAdminPreferences(value, cache = true) {
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 savedSizes = normalizeDashboardWidgetSizes(value.dashboard_widget_sizes);
if (editing) applyDashboardLayout(editing.order, editing.hidden, editing.sizes);
else applyDashboardLayout(savedOrder, savedHidden, savedSizes);
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: savedOrder, dashboard_hidden_widgets: savedHidden,
dashboard_density: applyAdminDashboardDensity(value.dashboard_density), dashboard_widgets: savedOrder, dashboard_hidden_widgets: savedHidden, dashboard_widget_sizes: savedSizes,
};
adminPreferences = normalized;
window.rustProxyRefresh?.update(normalized.refresh);
@@ -279,7 +332,7 @@ function updateAdminPreferences(patch) {
next.refresh = normalizeRefreshPreferences(next.refresh);
next.theme = normalizeAdminTheme(next.theme); next.language = normalizeAdminLanguage(next.language); next.ui_scale = normalizeAdminUiScale(next.ui_scale);
next.time_format = normalizeAdminTimeFormat(next.time_format); next.chart_style = normalizeAdminChartStyle(next.chart_style); next.button_style = normalizeAdminButtonStyle(next.button_style); next.corner_style = normalizeAdminCornerStyle(next.corner_style); next.table_density = normalizeAdminDensity(next.table_density);
next.dashboard_density = normalizeAdminDensity(next.dashboard_density, "compact"); next.dashboard_widgets = normalizeDashboardWidgets(next.dashboard_widgets); next.dashboard_hidden_widgets = normalizeHiddenDashboardWidgets(next.dashboard_hidden_widgets);
next.dashboard_density = normalizeAdminDensity(next.dashboard_density, "compact"); next.dashboard_widgets = normalizeDashboardWidgets(next.dashboard_widgets); next.dashboard_hidden_widgets = normalizeHiddenDashboardWidgets(next.dashboard_hidden_widgets); next.dashboard_widget_sizes = normalizeDashboardWidgetSizes(next.dashboard_widget_sizes);
const saved = await api("/api/preferences", { method: "PUT", body: JSON.stringify(next) });
if (!authenticated || epoch !== authEpoch) return adminPreferences;
return applyAdminPreferences(saved);
@@ -378,7 +431,7 @@ $("ui-dashboard-density")?.addEventListener("change", () => {
async function resetAppearancePatch(patch, control, messageKey = "appearanceSectionResetDone") {
if (control) control.disabled = true; let previous = null;
try {
const current = await loadAdminPreferences(); previous = current ? { ...current, dashboard_widgets: [...current.dashboard_widgets], dashboard_hidden_widgets: [...current.dashboard_hidden_widgets] } : null;
const current = await loadAdminPreferences(); previous = current ? { ...current, dashboard_widgets: [...current.dashboard_widgets], dashboard_hidden_widgets: [...current.dashboard_hidden_widgets], dashboard_widget_sizes: current.dashboard_widget_sizes === null ? null : { ...current.dashboard_widget_sizes } } : null;
if (current) applyAdminPreferences({ ...current, ...patch }); await updateAdminPreferences(patch); toast(t(messageKey));
} catch (error) { if (previous) applyAdminPreferences(previous); if (error.status !== 401) toast(error.message, true); }
finally { if (control) control.disabled = false; }
@@ -386,9 +439,9 @@ 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: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, 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], dashboard_widget_sizes: null },
};
void resetAppearancePatch(patches[control.dataset.appearanceReset] || {}, control);
}));
$("appearance-reset")?.addEventListener("click", () => void resetAppearancePatch({ ...ADMIN_APPEARANCE_DEFAULTS, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets] }, $("appearance-reset"), "appearanceResetDone"));
$("appearance-reset")?.addEventListener("click", () => void resetAppearancePatch({ ...ADMIN_APPEARANCE_DEFAULTS, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets], dashboard_widget_sizes: null }, $("appearance-reset"), "appearanceResetDone"));
refreshAppearanceUi();
+1 -1
View File
@@ -256,7 +256,7 @@ function renderNetworkInterfaceCharts(interfaces, appendSample) {
const actions = el("span", undefined, "network-interface-head-actions");
actions.append(rate, hide);
head.append(el("b", name, "mono"), actions);
const svg = svgNode("svg", { viewBox: "0 0 300 60", role: "img", "aria-label": `${name} ${t("networkInterfacesHistory")}` });
const svg = svgNode("svg", { viewBox: "0 0 300 60", preserveAspectRatio: "none", role: "img", "aria-label": `${name} ${t("networkInterfacesHistory")}` });
svg.append(svgNode("path", { class: "chart-grid", d: "M0 15H300 M0 30H300 M0 45H300" }));
svg.append(svgNode("polyline", { class: "diagnostic-line network-rx-line", points: "0,55 300,55" }));
svg.append(svgNode("polyline", { class: "diagnostic-line network-tx-line", points: "0,55 300,55" }));
+88 -63
View File
@@ -154,7 +154,8 @@
</section>
<section class="panel" id="panel-overview">
<p class="fine overview-inline-hint" id="overview-inline-hint" role="status" aria-live="polite"
hidden data-i18n="overviewLayoutInlineHint">Layout unlocked: reorder blocks within size groups,
hidden data-i18n="overviewLayoutInlineHint">Layout unlocked: move cards freely, resize with − /
+,
hide with × and save or cancel.</p>
<details class="overview-inline-hidden" id="overview-inline-hidden" hidden>
<summary><span data-i18n="overviewLayoutInlineHidden">Hidden blocks</span> <span
@@ -226,8 +227,9 @@
<article class="stat stat-chart" data-dashboard-widget="http_protocol"><span
data-i18n="httpProtocolMix">HTTP protocol
mix</span><strong id="metric-http-proto-total">0.0/s</strong><svg
aria-label="HTTP protocol activity" data-i18n-aria="httpProtocolMix"
id="http-protocol-chart" role="img" viewBox="0 0 300 60">
preserveAspectRatio="none" aria-label="HTTP protocol activity"
data-i18n-aria="httpProtocolMix" id="http-protocol-chart" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h1-line" id="http-protocol-h1"
points="0,55 300,55"></polyline>
@@ -241,7 +243,7 @@
id="metric-http-proto-h3">0.0/s</b></span></small></article>
<article class="stat stat-chart" data-dashboard-widget="websocket_protocol"><span
data-i18n="wsProtocolMix">WebSocket protocol
mix</span><strong id="metric-ws-proto-total">0</strong><svg
mix</span><strong id="metric-ws-proto-total">0</strong><svg preserveAspectRatio="none"
aria-label="WebSocket protocol activity" data-i18n-aria="wsProtocolMix"
id="ws-protocol-chart" role="img" viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
@@ -264,8 +266,8 @@
<article class="stat stat-chart" data-dashboard-widget="stream_traffic" hidden>
<span data-i18n="streamWidgetTraffic">Stream traffic</span><strong
id="stream-dashboard-traffic">-</strong>
<svg aria-label="Stream traffic RX/TX" data-i18n-aria="streamWidgetTraffic" role="img"
viewBox="0 0 300 60">
<svg preserveAspectRatio="none" aria-label="Stream traffic RX/TX"
data-i18n-aria="streamWidgetTraffic" role="img" viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line stream-chart-rx" id="stream-dashboard-rx"
points="0,55 300,55"></polyline>
@@ -297,7 +299,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_activity" hidden>
<span data-i18n="backendWidgetActivity">Active backend requests</span><strong
id="backend-dashboard-activity">-</strong>
<svg data-i18n-aria="backendWidgetActivity" role="img" viewBox="0 0 300 60">
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetActivity" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h1-line" id="backend-dashboard-active-line"
points="0,55 300,55"></polyline>
@@ -306,7 +309,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_errors" hidden>
<span data-i18n="backendWidgetErrors">Backend errors</span><strong
id="backend-dashboard-errors">-</strong>
<svg data-i18n-aria="backendWidgetErrors" role="img" viewBox="0 0 300 60">
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetErrors" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h3-line" id="backend-dashboard-error-line"
points="0,55 300,55"></polyline>
@@ -315,7 +319,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_latency" hidden>
<span data-i18n="backendWidgetLatency">Health probe RTT</span><strong
id="backend-dashboard-latency">-</strong>
<svg data-i18n-aria="backendWidgetLatency" role="img" viewBox="0 0 300 60">
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetLatency" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-latency-line"
points="0,55 300,55"></polyline>
@@ -324,7 +329,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_traffic" hidden>
<span data-i18n="backendWidgetTraffic">Backend traffic</span><strong
id="backend-dashboard-traffic">-</strong>
<svg data-i18n-aria="backendWidgetTraffic" role="img" viewBox="0 0 300 60">
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetTraffic" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line stream-chart-rx" id="backend-dashboard-in-line"
points="0,55 300,55"></polyline>
@@ -348,7 +354,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_requests" hidden>
<span data-i18n="backendWidgetRequests">Backend request rate</span><strong
id="backend-dashboard-requests">-</strong>
<svg data-i18n-aria="backendWidgetRequests" role="img" viewBox="0 0 300 60">
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetRequests" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h1-line" id="backend-dashboard-request-line"
points="0,55 300,55"></polyline>
@@ -366,7 +373,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_http5xx" hidden>
<span data-i18n="backendWidgetHttp5xx">backendWidgetHttp5xx</span><strong
id="backend-dashboard-http5xx">-</strong>
<svg data-i18n-aria="backendWidgetHttp5xx" role="img" viewBox="0 0 300 60">
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetHttp5xx" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-5xx-line"
points="0,55 300,55"></polyline>
@@ -375,7 +383,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_timeouts" hidden>
<span data-i18n="backendWidgetTimeouts">backendWidgetTimeouts</span><strong
id="backend-dashboard-timeouts">-</strong>
<svg data-i18n-aria="backendWidgetTimeouts" role="img" viewBox="0 0 300 60">
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetTimeouts" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-timeout-line"
points="0,55 300,55"></polyline>
@@ -384,7 +393,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_network" hidden>
<span data-i18n="backendWidgetNetwork">backendWidgetNetwork</span><strong
id="backend-dashboard-network">-</strong>
<svg data-i18n-aria="backendWidgetNetwork" role="img" viewBox="0 0 300 60">
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetNetwork" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-network-line"
points="0,55 300,55"></polyline>
@@ -393,7 +403,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_retries" hidden>
<span data-i18n="backendWidgetRetries">backendWidgetRetries</span><strong
id="backend-dashboard-retries">-</strong>
<svg data-i18n-aria="backendWidgetRetries" role="img" viewBox="0 0 300 60">
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetRetries" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-retry-line"
points="0,55 300,55"></polyline>
@@ -402,7 +413,8 @@
<article class="stat stat-chart" data-dashboard-widget="backend_queue" hidden>
<span data-i18n="backendWidgetQueue">backendWidgetQueue</span><strong
id="backend-dashboard-queue">-</strong>
<svg data-i18n-aria="backendWidgetQueue" role="img" viewBox="0 0 300 60">
<svg preserveAspectRatio="none" data-i18n-aria="backendWidgetQueue" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-queue-line"
points="0,55 300,55"></polyline>
@@ -416,7 +428,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_process_cpu" hidden>
<span data-i18n="overviewExtraProcessCpu">overviewExtraProcessCpu</span><strong
id="overview-extra-extra_process_cpu">-</strong>
<svg data-i18n-aria="overviewExtraProcessCpu" role="img" viewBox="0 0 300 60">
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraProcessCpu" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_process_cpu-line"
points="0,55 300,55"></polyline>
@@ -425,7 +438,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_process_rss" hidden>
<span data-i18n="overviewExtraProcessRss">overviewExtraProcessRss</span><strong
id="overview-extra-extra_process_rss">-</strong>
<svg data-i18n-aria="overviewExtraProcessRss" role="img" viewBox="0 0 300 60">
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraProcessRss" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_process_rss-line"
points="0,55 300,55"></polyline>
@@ -444,7 +458,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_open_connections" hidden>
<span data-i18n="overviewExtraOpenConnections">overviewExtraOpenConnections</span><strong
id="overview-extra-extra_open_connections">-</strong>
<svg data-i18n-aria="overviewExtraOpenConnections" role="img" viewBox="0 0 300 60">
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraOpenConnections" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_open_connections-line"
points="0,55 300,55"></polyline>
@@ -453,7 +468,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_websocket_active" hidden>
<span data-i18n="overviewExtraWebsocketActive">overviewExtraWebsocketActive</span><strong
id="overview-extra-extra_websocket_active">-</strong>
<svg data-i18n-aria="overviewExtraWebsocketActive" role="img" viewBox="0 0 300 60">
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraWebsocketActive" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_websocket_active-line"
points="0,55 300,55"></polyline>
@@ -462,7 +478,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_tls_error_rate" hidden>
<span data-i18n="overviewExtraTlsErrorRate">overviewExtraTlsErrorRate</span><strong
id="overview-extra-extra_tls_error_rate">-</strong>
<svg data-i18n-aria="overviewExtraTlsErrorRate" role="img" viewBox="0 0 300 60">
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraTlsErrorRate" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_tls_error_rate-line"
points="0,55 300,55"></polyline>
@@ -471,7 +488,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_request_error_rate" hidden>
<span data-i18n="overviewExtraRequestErrorRate">overviewExtraRequestErrorRate</span><strong
id="overview-extra-extra_request_error_rate">-</strong>
<svg data-i18n-aria="overviewExtraRequestErrorRate" role="img" viewBox="0 0 300 60">
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraRequestErrorRate" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_request_error_rate-line"
points="0,55 300,55"></polyline>
@@ -544,7 +562,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_response_latency" hidden>
<span data-i18n="overviewExtraResponseLatency">overviewExtraResponseLatency</span><strong
id="overview-extra-extra_response_latency">-</strong>
<svg data-i18n-aria="overviewExtraResponseLatency" role="img" viewBox="0 0 300 60">
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraResponseLatency" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_response_latency-line"
points="0,55 300,55"></polyline>
@@ -553,7 +572,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_backend_unhealthy" hidden>
<span data-i18n="overviewExtraBackendUnhealthy">overviewExtraBackendUnhealthy</span><strong
id="overview-extra-extra_backend_unhealthy">-</strong>
<svg data-i18n-aria="overviewExtraBackendUnhealthy" role="img" viewBox="0 0 300 60">
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraBackendUnhealthy" role="img"
viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_backend_unhealthy-line"
points="0,55 300,55"></polyline>
@@ -562,7 +582,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_total_rps" hidden>
<span data-i18n="overviewExtraTotalRps">HTTP requests / s</span><strong
id="overview-extra-extra_total_rps">-</strong>
<svg viewBox="0 0 300 60" role="img" aria-label="HTTP requests / s">
<svg preserveAspectRatio="none" viewBox="0 0 300 60" role="img"
aria-label="HTTP requests / s">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_total_rps-line"
points="0,55 300,55"></polyline>
@@ -572,7 +593,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_active_exchanges" hidden>
<span data-i18n="overviewExtraActiveExchanges">Active HTTP exchanges</span><strong
id="overview-extra-extra_active_exchanges">-</strong>
<svg viewBox="0 0 300 60" role="img" aria-label="Active HTTP exchanges">
<svg preserveAspectRatio="none" viewBox="0 0 300 60" role="img"
aria-label="Active HTTP exchanges">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_active_exchanges-line"
points="0,55 300,55"></polyline>
@@ -602,7 +624,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_cache_miss_rate" hidden>
<span data-i18n="overviewExtraCacheMissRate">Cache misses / s</span><strong
id="overview-extra-extra_cache_miss_rate">-</strong>
<svg viewBox="0 0 300 60" role="img" aria-label="Cache misses / s">
<svg preserveAspectRatio="none" viewBox="0 0 300 60" role="img"
aria-label="Cache misses / s">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_cache_miss_rate-line"
points="0,55 300,55"></polyline>
@@ -612,7 +635,8 @@
<article class="stat stat-chart" data-dashboard-widget="extra_backend_active" hidden>
<span data-i18n="overviewExtraBackendActive">In-flight backend requests</span><strong
id="overview-extra-extra_backend_active">-</strong>
<svg viewBox="0 0 300 60" role="img" aria-label="In-flight backend requests">
<svg preserveAspectRatio="none" viewBox="0 0 300 60" role="img"
aria-label="In-flight backend requests">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_backend_active-line"
points="0,55 300,55"></polyline>
@@ -625,7 +649,7 @@
<span class="overview-wide-summary"
id="overview-wide-wide_protocol_rates-summary">-</span>
</div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="HTTP/1.1 · HTTP/2 · HTTP/3 traffic">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0"
@@ -649,7 +673,7 @@
<h2 data-i18n="overviewWideCpu">System and process CPU</h2>
<span class="overview-wide-summary" id="overview-wide-wide_cpu-summary">-</span>
</div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="System and process CPU">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0"
@@ -671,7 +695,7 @@
<h2 data-i18n="overviewWideMemory">Process RAM and cache memory</h2>
<span class="overview-wide-summary" id="overview-wide-wide_memory-summary">-</span>
</div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Process RAM and cache memory">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0"
@@ -694,7 +718,7 @@
<span class="overview-wide-summary"
id="overview-wide-wide_cache_activity-summary">-</span>
</div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Cache hits / misses per second">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0"
@@ -715,7 +739,7 @@
<h2 data-i18n="overviewWideConnections">Connections and WebSocket tunnels</h2>
<span class="overview-wide-summary" id="overview-wide-wide_connections-summary">-</span>
</div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Connections and WebSocket tunnels">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0"
@@ -738,7 +762,7 @@
<span class="overview-wide-summary"
id="overview-wide-wide_backend_health-summary">-</span>
</div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Backend availability over time">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0"
@@ -760,7 +784,7 @@
<h2 data-i18n="overviewWideLatency">Response and backend probe latency</h2>
<span class="overview-wide-summary" id="overview-wide-wide_latency-summary">-</span>
</div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Response and backend probe latency">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0"
@@ -783,7 +807,7 @@
<span class="overview-wide-summary"
id="overview-wide-wide_transform_activity-summary">-</span>
</div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
data-i18n-aria="overviewWideTransformActivity" aria-label="Transformation activity">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0"
@@ -816,7 +840,7 @@
<span class="overview-wide-summary"
id="overview-wide-wide_compression_activity-summary">-</span>
</div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
data-i18n-aria="overviewWideCompressionActivity" aria-label="Compression activity">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0"
@@ -915,8 +939,8 @@
<article class="stat stat-chart" data-dashboard-widget="www_php_requests" hidden>
<span data-i18n="webMetricPhpRequests">PHP requests/s</span><strong
id="www_php_requests-value">-</strong>
<svg role="img" viewBox="0 0 300 60" aria-label="PHP requests per second"
data-i18n-aria="webMetricPhpRequests">
<svg preserveAspectRatio="none" role="img" viewBox="0 0 300 60"
aria-label="PHP requests per second" data-i18n-aria="webMetricPhpRequests">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line proto-h2-line" id="www_php_requests-line"
points="0,55 300,55"></polyline>
@@ -928,7 +952,7 @@
<h2 data-i18n="webMetricRequests">webMetricRequests</h2><span
class="overview-wide-summary" id="www_requests-value">-</span>
</div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Web Server request rate" data-i18n-aria="webMetricRequests">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" id="www_requests-all-line"
@@ -949,7 +973,7 @@
<h2 data-i18n="webMetricLatency">webMetricLatency</h2><span
class="overview-wide-summary" id="www_latency-value">-</span>
</div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Web Server header latency" data-i18n-aria="webMetricLatency">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" id="www_latency-web-line"
@@ -970,7 +994,7 @@
<h2 data-i18n="webMetricFailures">webMetricFailures</h2><span
class="overview-wide-summary" id="www_failures-value">-</span>
</div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="PHP FastCGI failures" data-i18n-aria="webMetricFailures">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" id="www_failures-timeout-line"
@@ -1001,7 +1025,7 @@
<h2 data-i18n="webMetricTraffic">webMetricTraffic</h2><span
class="overview-wide-summary" id="www_traffic-value">-</span>
</div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Web Server body byte rate" data-i18n-aria="webMetricTraffic">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" id="www_traffic-nonphp-line"
@@ -1022,7 +1046,7 @@
<h2 data-i18n="webMetricStatusTrend">webMetricStatusTrend</h2><span
class="overview-wide-summary" id="www_status_trend-value">-</span>
</div>
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Web Server HTTP statuses" data-i18n-aria="webMetricStatusTrend">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="overview-wide-series overview-wide-series-0" id="www_status_trend-2xx-line"
@@ -1062,8 +1086,8 @@
<div><span class="small-label" data-i18n="liveTraffic">LIVE TRAFFIC</span>
<h2 data-i18n="requestsPerSecond">Requests per second</h2>
</div><span class="pill live" data-i18n="live">LIVE</span>
</div><svg aria-label="Recent request rate" id="traffic-chart" role="img"
viewbox="0 0 600 130">
</div><svg preserveAspectRatio="none" aria-label="Recent request rate" id="traffic-chart"
role="img" viewbox="0 0 600 130">
<path class="chart-grid" d="M0 25H600 M0 65H600 M0 105H600"></path>
<polyline class="traffic-line traffic-line-total" id="sparkline" points="0,120 600,120">
</polyline>
@@ -1095,8 +1119,8 @@
<div><span class="small-label" data-i18n="systemLoad">SYSTEM LOAD</span>
<h2><span id="system-cpu">-</span> <small>CPU</small></h2>
</div><span class="meta" id="system-source">-</span>
</div><svg aria-label="System CPU history" data-i18n-aria="systemChart" id="system-chart"
role="img" viewbox="0 0 300 100">
</div><svg preserveAspectRatio="none" aria-label="System CPU history"
data-i18n-aria="systemChart" id="system-chart" role="img" viewbox="0 0 300 100">
<path class="chart-grid" d="M0 25H300 M0 50H300 M0 75H300"></path>
<polyline id="system-sparkline" points="0,95 300,95"></polyline>
</svg>
@@ -1121,8 +1145,9 @@
connections</small></h2>
</div><span class="pill live" data-i18n="live">LIVE</span>
</div>
<svg aria-label="Connection activity history" data-i18n-aria="connectionActivity"
id="connection-chart" role="img" viewbox="0 0 300 100">
<svg preserveAspectRatio="none" aria-label="Connection activity history"
data-i18n-aria="connectionActivity" id="connection-chart" role="img"
viewbox="0 0 300 100">
<path class="chart-grid" d="M0 25H300 M0 50H300 M0 75H300"></path>
<polyline class="connection-open-line" id="connection-open-sparkline"
points="0,95 300,95"></polyline>
@@ -1171,8 +1196,8 @@
<div class="mini-live-chart">
<div class="mini-live-chart-head"><span data-i18n="cacheOutcomeHistory">HIT / MISS /
BYPASS activity</span><b id="cache-outcome-chart-value">-</b></div>
<svg aria-label="HIT MISS BYPASS activity" data-i18n-aria="cacheOutcomeHistory"
role="img" viewbox="0 0 300 60">
<svg preserveAspectRatio="none" aria-label="HIT MISS BYPASS activity"
data-i18n-aria="cacheOutcomeHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line cache-hit-line" id="cache-hit-sparkline"
points="0,55 300,55"></polyline>
@@ -1189,8 +1214,8 @@
<div class="mini-live-chart">
<div class="mini-live-chart-head"><span data-i18n="cacheHotMemoryHistory">HOT RAM
usage / limit</span><b id="cache-chart-value">-</b></div>
<svg aria-label="HOT cache RAM usage history" data-i18n-aria="cacheHotMemoryHistory"
role="img" viewbox="0 0 300 60">
<svg preserveAspectRatio="none" aria-label="HOT cache RAM usage history"
data-i18n-aria="cacheHotMemoryHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line cache-hot-line" id="cache-memory-sparkline"
points="0,55 300,55"></polyline>
@@ -1200,7 +1225,7 @@
<div class="mini-live-chart-head"><span
data-i18n="cacheTransientMemoryHistory">Transient
RAM usage / limit</span><b id="cache-transient-chart-value">-</b></div>
<svg aria-label="Transient cache RAM usage history"
<svg preserveAspectRatio="none" aria-label="Transient cache RAM usage history"
data-i18n-aria="cacheTransientMemoryHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line cache-transient-total-line"
@@ -1223,7 +1248,7 @@
<div class="mini-live-chart-head"><span data-i18n="cacheColdStorageHistory">COLD
storage
usage / limit</span><b id="cache-cold-chart-value">-</b></div>
<svg aria-label="COLD storage usage history"
<svg preserveAspectRatio="none" aria-label="COLD storage usage history"
data-i18n-aria="cacheColdStorageHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line cache-cold-line" id="cache-cold-sparkline"
@@ -1325,8 +1350,8 @@
<div class="mini-live-chart">
<div class="mini-live-chart-head"><span data-i18n="processCpuHistory">Process CPU
history</span><b id="process-cpu-chart-value">-</b></div>
<svg aria-label="Process CPU history" data-i18n-aria="processCpuHistory" role="img"
viewbox="0 0 300 60">
<svg preserveAspectRatio="none" aria-label="Process CPU history"
data-i18n-aria="processCpuHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="process-cpu-sparkline"
points="0,55 300,55"></polyline>
@@ -1335,8 +1360,8 @@
<div class="mini-live-chart">
<div class="mini-live-chart-head"><span data-i18n="processRssHistory">Process RSS
history</span><b id="process-rss-chart-value">-</b></div>
<svg aria-label="Process RSS history" data-i18n-aria="processRssHistory" role="img"
viewbox="0 0 300 60">
<svg preserveAspectRatio="none" aria-label="Process RSS history"
data-i18n-aria="processRssHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line process-rss-line" id="process-rss-sparkline"
points="0,55 300,55"></polyline>
@@ -1367,8 +1392,8 @@
<div class="mini-live-chart">
<div class="mini-live-chart-head"><span data-i18n="networkHistory">RX / TX
history</span><b id="network-chart-value">-</b></div>
<svg aria-label="Network throughput history" data-i18n-aria="networkHistory"
role="img" viewbox="0 0 300 60">
<svg preserveAspectRatio="none" aria-label="Network throughput history"
data-i18n-aria="networkHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line network-rx-line" id="network-rx-sparkline"
points="0,55 300,55"></polyline>
@@ -1395,7 +1420,7 @@
</div>
</details><b id="latency-chart-value">-</b>
</span></div>
<svg aria-label="Mean response latency history"
<svg preserveAspectRatio="none" aria-label="Mean response latency history"
data-i18n-aria="responseLatencyHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="latency-sparkline" points="0,55 300,55">
+246 -44
View File
@@ -72,6 +72,9 @@
--rp-progress-fill: var(--chart-accent);
--rp-progress-track: var(--line);
--rp-progress-radius: 3px;
--rp-nav-active-color: var(--brand-secondary);
--rp-nav-active-bg: var(--nav-active-bg);
--rp-nav-hover-bg: var(--nav-hover-bg);
--traffic-h1: var(--chart-accent);
--traffic-h2: var(--chart-secondary);
--traffic-h3: var(--chart-tertiary);
@@ -135,6 +138,10 @@ html[data-theme=light] {
--scrollbar-thumb-hover: #879684
}
html:is([data-theme=light], [data-theme=cream]) {
--rp-nav-active-color: color-mix(in srgb, var(--brand-secondary) 45%, var(--text) 55%);
}
html[data-theme=ultra_dark] {
color-scheme: dark;
--bg: #030404;
@@ -389,16 +396,6 @@ html.app-startup-failed .app-startup-recovery {
}
}
@media(prefers-reduced-motion:reduce) {
.app-startup-spinner {
animation: none
}
* {
scroll-behavior: auto !important
}
}
body {
margin: 0;
background: var(--bg);
@@ -533,7 +530,8 @@ nav {
text-align: left;
background: transparent;
color: var(--muted);
border-left: 2px solid transparent
border-left: 2px solid transparent;
border-radius: var(--rp-radius-control, 0)
}
.nav span {
@@ -546,13 +544,18 @@ nav {
}
.nav.active {
background: var(--nav-active-bg);
color: var(--green);
border-left-color: var(--green)
background: var(--rp-nav-active-bg);
color: var(--rp-nav-active-color);
border-left-color: var(--rp-nav-active-color)
}
.nav:hover {
background: var(--nav-hover-bg)
.nav:not(.active):hover {
background: var(--rp-nav-hover-bg)
}
.sidebar .main-navigation .nav:focus-visible {
outline: 2px solid var(--rp-nav-active-color);
outline-offset: 2px
}
.sidebar-bottom {
@@ -1213,7 +1216,7 @@ button.is-saving {
}
.nav.active {
border-bottom-color: var(--green)
border-bottom-color: var(--rp-nav-active-color)
}
h1 {
@@ -1270,7 +1273,8 @@ button.is-saving {
letter-spacing: .4px
}
.breadcrumb span {
.breadcrumb span,
.login-footer span {
margin: 0 5px
}
@@ -1360,10 +1364,6 @@ button.is-saving {
padding: 20px
}
.login-footer span {
margin: 0 5px
}
.toast {
right: 17px;
bottom: 17px
@@ -2077,7 +2077,7 @@ td {
}
[dir=rtl] .nav.active {
border-right-color: var(--green)
border-right-color: var(--rp-nav-active-color)
}
[dir=rtl] table {
@@ -7879,9 +7879,9 @@ button.config-history-day.is-selected {
.mobile-menu-toggle:hover,
.mobile-menu-toggle[aria-expanded=true] {
color: var(--text);
border-color: var(--border-hover);
background: var(--nav-active-bg);
color: var(--rp-nav-active-color);
border-color: var(--rp-nav-active-color);
background: var(--rp-nav-active-bg);
}
.mobile-menu-icon {
@@ -7921,8 +7921,13 @@ button.config-history-day.is-selected {
.sidebar .main-navigation .nav.active {
border-color: var(--line);
border-left-color: var(--green);
background: var(--nav-active-bg);
border-left-color: var(--rp-nav-active-color);
background: var(--rp-nav-active-bg);
}
[dir=rtl] .sidebar .main-navigation .nav.active {
border-left-color: var(--line);
border-right-color: var(--rp-nav-active-color);
}
}
@@ -8789,7 +8794,7 @@ html[data-dashboard-density=ultra] #panel-overview .stats-grid {
}
html[data-dashboard-density=normal] #panel-overview .stat {
padding: 14px 14px;
padding: 14px;
}
html[data-dashboard-density=normal] #panel-overview .stat>span {
@@ -8854,8 +8859,6 @@ html[data-dashboard-density=ultra] #panel-overview .stat-chart svg {
height: 24px;
}
/* Dashboard density applies to charts and panels as well as number tiles.
Keep the Overview grid gutters independent of dashboard density. */
html[data-dashboard-density=normal] #panel-overview .stat-chart svg {
height: 34px;
}
@@ -10220,12 +10223,10 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
.overview-layout-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--ui-gap, 10px);
}
.overview-layout-tile {
min-width: 0;
border: 1px solid var(--line);
background: var(--panel);
cursor: grab;
@@ -10246,7 +10247,6 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
.overview-layout-tile-toolbar {
display: flex;
align-items: center;
gap: 3px;
padding: 4px;
border-bottom: 1px solid var(--line);
@@ -10817,11 +10817,11 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
}
html[data-dashboard-density=normal] #panel-overview .dashboard-overview-grid.is-aligned>.stat strong {
font-size: clamp(20px, 5.8vw, 26px);
font-size: calc(clamp(20px, 5.8vw, 26px));
}
html[data-dashboard-density=compact] #panel-overview .dashboard-overview-grid.is-aligned>.stat strong {
font-size: clamp(19px, 5vw, 22px);
font-size: calc(clamp(19px, 5vw, 22px));
}
#panel-overview .dashboard-overview-grid.is-aligned>.card:not(.pool-card) {
@@ -10883,6 +10883,7 @@ html[data-dashboard-density=ultra] #panel-overview .dashboard-overview-grid>.car
}
#panel-overview .overview-wide-svg .chart-grid {
vector-effect: non-scaling-stroke;
stroke: var(--line);
fill: none;
stroke-width: 1;
@@ -10891,6 +10892,7 @@ html[data-dashboard-density=ultra] #panel-overview .dashboard-overview-grid>.car
#panel-overview .overview-wide-series {
fill: none;
vector-effect: non-scaling-stroke;
stroke-width: 2;
stroke-linejoin: round;
stroke-linecap: round;
@@ -10964,7 +10966,6 @@ html[data-dashboard-density=ultra] #panel-overview .dashboard-overview-grid>.car
.overview-layout-tile-toolbar .overview-layout-controls {
display: inline-flex;
align-items: center;
gap: 3px;
margin-left: auto;
}
@@ -11260,11 +11261,9 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
height: 100%;
border-radius: inherit;
background: var(--rp-progress-fill);
transition: width 180ms ease;
transition: width .18s;
}
/* System Load uses the same track/fill component as other Overview meters,
while retaining its compact 5px height and existing vertical spacing. */
#panel-overview .system-card .system-memory-meter {
display: block;
width: 100%;
@@ -11374,7 +11373,15 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
}
}
@media (prefers-reduced-motion: reduce) {
@media(prefers-reduced-motion:reduce) {
.app-startup-spinner {
animation: none
}
* {
scroll-behavior: auto !important
}
#panel-overview .overview-meter-fill {
transition: none;
}
@@ -11423,7 +11430,7 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
white-space: nowrap;
}
@media (max-width: 700px) {
@media(max-width:700px) {
.page-intro .overview-samples-toolbar {
margin-left: 0;
justify-content: flex-start;
@@ -11514,7 +11521,7 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
#panel-overview .www-segmented span {
display: block;
height: 100%;
transition: width 180ms ease;
transition: width .18s;
}
#panel-overview .www-segment-0 {
@@ -11639,8 +11646,203 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
margin-top: 4px;
}
@media (prefers-reduced-motion: reduce) {
@media(prefers-reduced-motion:reduce) {
#panel-overview .www-segmented span {
transition: none;
}
}
#panel-overview .dashboard-overview-grid.is-aligned[data-overview-layout-mode=custom] {
grid-auto-flow: row dense;
}
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="2"] {
grid-column: auto/span 2;
}
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="3"] {
grid-column: auto/span 3;
}
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="4"] {
grid-column: auto/span 4;
}
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="6"] {
grid-column: auto/span 6;
}
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="12"] {
grid-column: 1/-1;
}
#panel-overview .dashboard-overview-grid.is-aligned[data-overview-layout-mode=custom]>[data-dashboard-widget].overview-row-start {
grid-column-start: auto;
}
#panel-overview .dashboard-overview-grid.is-aligned[data-overview-layout-mode=custom] .overview-wide-legend,
#panel-overview .dashboard-overview-grid.is-aligned[data-overview-layout-mode=custom] .stat-chart-legend {
flex-wrap: wrap;
white-space: normal;
}
#panel-overview .overview-inline-width,
.overview-layout-width-value {
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
font: 11px var(--mono);
color: var(--text);
}
#panel-overview .overview-inline-width {
gap: 3px;
flex: 0 0 auto;
}
#panel-overview .overview-inline-width-value {
min-width: 31px;
text-align: center;
}
#panel-overview .dashboard-overview-grid.is-unlocked>[data-dashboard-widget] {
padding-top: 101px;
}
#panel-overview .overview-inline-toolbar .overview-widget-type {
margin-right: auto;
}
.overview-layout-tile-toolbar .overview-layout-width-value {
flex: 0 0 auto;
min-width: 30px;
text-align: center;
}
.overview-layout-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-auto-flow: row;
}
.overview-layout-grid>.overview-layout-tile.is-panel {
grid-column: auto;
}
.overview-layout-tile {
min-width: 0;
}
.overview-layout-tile-toolbar {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
gap: 5px 6px;
align-items: center;
min-width: 0;
}
.overview-layout-tile-toolbar .overview-widget-type {
min-width: 0;
max-width: 100%;
grid-column: 2;
}
.overview-layout-tile-toolbar .overview-layout-controls {
grid-column: 1/-1;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 4px 8px;
min-width: 0;
width: 100%;
margin: 0;
}
.overview-layout-order-actions,
.overview-layout-size-actions {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 3px;
min-width: 0;
}
.overview-layout-tile-toolbar .overview-layout-hide {
margin-inline-start: 0;
white-space: normal;
overflow-wrap: anywhere;
}
.overview-layout-grid .overview-layout-card-preview,
.overview-layout-grid .overview-layout-panel-preview {
min-width: 0;
max-width: 100%;
}
.overview-layout-dialog .overview-layout-footer,
.overview-layout-dialog .overview-layout-hidden-item {
flex-wrap: wrap;
}
.overview-layout-dialog .overview-layout-hidden-item>span:first-child {
flex: 1 1 125px;
}
.overview-layout-dialog .overview-layout-hidden-item>button {
flex: 0 1 auto;
max-width: 100%;
white-space: normal;
overflow-wrap: anywhere;
}
@container overview-canvas (max-width: 1080px) {
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="2"] {
grid-column: auto/span 2;
}
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="3"] {
grid-column: auto/span 3;
}
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="4"] {
grid-column: auto/span 4;
}
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="12"],
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="6"] {
grid-column: 1/-1;
}
}
@container overview-canvas (max-width: 700px) {
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="2"].stat {
grid-column: auto/span 1;
}
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="12"],
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="2"]:not(.stat),
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="3"],
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="4"],
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="6"] {
grid-column: 1/-1;
}
}
@media(max-width:900px) {
.overview-layout-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media(max-width:620px) {
.overview-layout-grid {
grid-template-columns: minmax(0, 1fr);
}
}
@media(max-width:550px) {
#panel-overview .dashboard-overview-grid.is-unlocked>[data-dashboard-widget] {
padding-top: 135px;
}
}