v0.33.1
This commit is contained in:
1 parent
2ccc598b12
commit
b757b34a31
26 files changed
+671
-116
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
@@ -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(®istry) && 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
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
})();
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() => []);
|
||||
|
||||
@@ -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);
|
||||
}));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user